Make ORMLite use proper serialization for structs

2019-05-11 04:19发布

问题:

tl;dr:

I am registering a serializer and a deserializer on a struct.
The serializer is not called, but the deserializer is.

How can I fix this?
It works properly on reference types, and doing JsConfig<Position>.TreatValueAsRefType = true; did not help either.


Long version:

I am storing two complex types using ORMLite: Position (a struct, from external library DotSpatial which we do not control) and Tuple.

In order to be able to properly store/read them from the database, I defined their serializers and deserializers:

// Struct. Called by position.ToJsv(), NOT called by ORMLite's connection.Insert() .
JsConfig<Position>.SerializeFn = position =>
{
    string str = position.ToString(null, CultureInfo.InvariantCulture);
    return str; // Breakpoint here.
};
// Struct. Called in both.
JsConfig<Position>.DeSerializeFn = position => Position.Parse(position, CultureInfo.InvariantCulture);

// Reference type. Works fine.
JsConfig<Tuple<double, double>>.SerializeFn = tuple => string.Format(CultureInfo.InvariantCulture,
    "{0}{1}{2}",
    tuple.Item1, CultureInfo.InvariantCulture.TextInfo.ListSeparator, tuple.Item2
    );
// Works fine too.
JsConfig<Tuple<double, double>>.DeSerializeFn = tuple =>
{
    var values = tuple.Split(new[] { CultureInfo.InvariantCulture.TextInfo.ListSeparator }, StringSplitOptions.None);
    double item1, item2;
    if (values.Length == 2
        && double.TryParse(values[0], out item1)
        && double.TryParse(values[1], out item2))
    {
        var result = new Tuple<double, double>(item1, item2);
        return result;
    }
    throw new ArgumentException("Could not parse easting and northing from database; malformatted?", "tuple");
};

Debugging

A break-point in the deserializer is hit when reading from the DB with ORMLite: connection.Where<T>(item => item.Foo == bar).
 break-point in the serializer is not hit when writing to the DB with ORMLite: connection.Insert(item).

I thought maybe the serializer was not being registered properly, so I called .ToJsv() on the object.

var lat = Latitude.Parse("00°00'02.7451\"N", CultureInfo.InvariantCulture);
var lon = Longitude.Parse("013°29'17.3270\"W", CultureInfo.InvariantCulture);
Position pos = new Position(lat, lon);
string foo = pos.ToJsv(); // Works, hits the breakpoint.

When hitting the breakpoint, str = 00°00'02.7451"N,013°29'17.3270"W.
But when inserting with ORMLite, the breakpoint is not hit and I get values in the database such as 00°00'02,7451"N;013°29'17,3270"W - note the commas, due to the culture.

The database is saving culture-dependent values! :(

Attempts

Since this happens only on structs, I tried to register the type to be treated as a reference type, but that did not seem to work.

JsConfig<Position>.TreatValueAsRefType = true;

Update:

I am using the ORMLite.PostgreSQL Nuget package (v 3.9.70). It includes ServiceStack.Text (v 3.9.70) and Npgsql (v 2.0.11).

I want to try getting the code from source control and debugging it directly, but for now I don't have time.

The Position struct is defined in an external library, which I cannot change.

Minimalist sample

I have uploaded a minimalist sample at https://gist.github.com/aneves/7830776 , which shows the following output:

Thing, current culture: 12,6;10,9
Thing, invariant culture: 12.6,10.9
Thing, from Jsv: "12,6;10,9"
>> deserializing 10;35
>> Could not parse value, it is malformed. (10;35)
Found this: Box[A: 0;0]
Press any key to continue . . .

回答1:

UPDATE:

After checking the source code of OrmLite on GitHub, it appeared that:

  • JsConfig<Position>.TreatValueAsRefType would never be checked when serializing data into the database.
  • For deserialization OrmLite would always call TypeSerializer.DeserializeFromString, this is why your scenario worked in that direction only.

To fix the issue, I have submitted a patch to the master repository. In the mean time you are most welcome to either recompile OrmLite using this patch, or simply use the recompiled version (based on 4.0.3) I have made available for you here in lieu of one of the corresponding file from NuGet.

I hope this fix will be incorporated in the next official release, and also in the 3.* branch.

ORIGINAL ANSWER:

If you have control over the Position struct (which seems not to be the case), have you tried overriding ToString()? OrmLite should call it if I remember correctly:

struct Position {
    public override ToString(object o, CultureInfo culture) {
        /* Your serialization */
    }

    public override ToString() { // Will be used by OrmLite to serialize
        position.ToString(null, CultureInfo.InvariantCulture);
    }
}

It may not solve SerializeFn<> not being called but could be good enough for your objective, at least until the bug is fixed.



回答2:

If possible I would prefer to implement a proper solution.

For now I am patching the problem by defining my own class MyPosition that mimics the struct by defining implicit operators.
(Disregard the nulls, they are due to how the ToString overloads for Position were implemented.)

JsConfig<MyPosition>.SerializeFn = position => position.ToString(null, CultureInfo.InvariantCulture);
JsConfig<MyPosition>.DeSerializeFn = position => Position.Parse(position, CultureInfo.InvariantCulture);

public class MyPosition
{
    public Latitude Latitude { get; set; }
    public Longitude Longitude { get; set; }

    public override string ToString()
    {
        return ToString(null, CultureInfo.CurrentCulture);
    }

    public string ToString(CultureInfo culture)
    {
        return ToString(null, culture);
    }

    public string ToString(string format, CultureInfo culture)
    {
        var pos = new Position(Latitude, Longitude);
        return pos.ToString(null, culture);
    }

    public static implicit operator MyPosition(Position toConvert)
    {
        return new MyPosition
        {
            Latitude = toConvert.Latitude,
            Longitude = toConvert.Longitude
        };
    }
    public static implicit operator Position(MyPosition toConvert)
    {
        return new Position(toConvert.Latitude, toConvert.Longitude);
    }
}