Make ORMLite use proper serialization for structs
Asked Answered
B

2

3

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 . . .
Bookstand answered 27/11, 2013 at 15:30 Comment(10)
Have you tried RawSerializeFn and RawDeserializeFn?Trowbridge
Let me know if it works better!Trowbridge
@ErwinMayer no, it does not seem to work. Even with TreatValueAsRefType = true and setting both SerializeFn and RawSerializeFn, the property is still persisted with CurrentCulture (commas for decimals) and not with my InvariantCulture version (dots).Bookstand
Also, RawDeserializeFn does not exist. But that is not a problem, since the SerializeFn is hit without problems.Bookstand
OK, strange it seems there might be a bug then. Maybe you can check the source code to confirm. RawDeserializeFn does exist. Are you using the latest version?Trowbridge
I got it via Nuget just a few days ago, so yes it should be the latest. Maybe I ought to get the code from source control and step into the process... I'll consider this.Bookstand
I have ServiceStack.Text.3.9.70 and ServiceStack.OrmLite.Sqlite.Mono.3.9.70 in my nuget packages. I do have the .RawDeserializeFn members of JsConfig<T>.Trowbridge
I have 3.9.70 for both too, but ServiceStack.OrmLite.PostgreSQL instead of ServiceStack.OrmLite.Sqlite.Mono. Maybe that is it.Bookstand
Have you noticed that version 4.0.3 has just been released? github.com/ServiceStack/ServiceStack/wiki/Release-Notes Unfortunately it seems they have moved away from being free without restriction :(.Trowbridge
There you go, check my updated answer :).Trowbridge
T
3

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.

Trowbridge answered 3/12, 2013 at 7:7 Comment(7)
Unfortunately no, I have no control over it. Still, that would also be sort of a work-around and not the ideal solution; ideally, ToString would be culture-dependent - which is why most classes have overloads that receive a CultureInfo. (Why object has no overload that receives a CultureInfo is beyond me - that would be mighty useful!)Bookstand
OK. Can you try using a SQLite database to see if the behavior is also observed? Also have you tried using your own Position struct (a mock) to see if the behavior is consistent? Then you could also add it in your question so that we have a full minimalist code to try to reproduce. I think the only way to find out if there is a bug indeed might be to get the project from GitHub and start a debugging session (I can try that once I have the full minimalist code).Trowbridge
Ah, of course I should provide minimalist code... I will try that out, thanks!Bookstand
I have added some minimalist code to the end of the question. Uses SQLIte.Bookstand
Good news, after debugging with the cloned source code of ServiceStack.OrmLite and your minimalist example, I was able to find the problem and fix it. I am going to push my changes to the github repository and give you a dll that can be used until the fix makes it into the master. :)Trowbridge
My patch has just been merged into the master: github.com/ServiceStack/ServiceStack.OrmLite/pull/348Trowbridge
Oh WOW, you certainly went the extra mile! I'll quote: I am happy. :)Bookstand
B
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);
    }
}
Bookstand answered 2/12, 2013 at 19:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.