louthy / language-ext

C# functional language extensions - a base class library for functional programming
MIT License
6.51k stars 420 forks source link

DataContract serialisation with v2 #196

Closed NickDarvey closed 7 years ago

NickDarvey commented 7 years ago

Hi Paul,

I was just reading about the amount of joy Json.NET serialisation is brining you here and I thought it was worth asking if you were considering increasing your overall happiness by also supporting DataContract serialisation.

I just grabbed 2.0.3 to see how it was serialising right now and it looks like the answer is not very well.

With a test class like

[DataContract]
    public class TestClass
    {
        [DataMember]
        public Map<string, int> TestProperty { get; private set; }

        public TestClass(Map<string, int> testProperty)
        {
            TestProperty = testProperty;
        }
    }

and an instantiation of

var test = new TestClass(Map(
      Tuple("yup", 1),
      Tuple("nup", 2)));

serializes like

<TestClass xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <TestProperty xmlns:a="http://schemas.datacontract.org/2004/07/LanguageExt">
    <a:value>
      <a:Rev>false</a:Rev>
      <a:Root>
        <Key i:type="b:string" xmlns="" xmlns:b="http://www.w3.org/2001/XMLSchema">yup</Key>
        <Value i:type="b:int" xmlns="" xmlns:b="http://www.w3.org/2001/XMLSchema">1</Value>
      </a:Root>
      <a:hashCode>0</a:hashCode>
    </a:value>
  </TestProperty>
</TestClass>

Only the first item in the map get serialised. It looks like all previous of versions of v2 exhibit this behaviour.

Use case I don't think many people would pick DataContract as their first choice, but it still is used pretty widely. (In our case, Service Fabric's implementation of actors uses a DataContract serialiser.) It does have some nice benefits like well-documented versioning strategies which other serialisers lack.

Right now (with v1.9 of Lang-Ext), we add private surrogates to all of our data types for serialisation (e.g. a plain ol' Dictionary<K,V> that's decorated with the [DataMember] attribute) and expose it via a Lang-Ext type (Map<K,V>). This works, but it's a lot of boilerplate when creating a new data type.

Nick

louthy commented 7 years ago

@NickDarvey Hi Nick, I'm open to advice and pull requests if you can think of a good way of making this work. I have to be honest that I haven't touched DataContract in a very long time, and even then it wasn't really in anger, so this isn't a specialist area for me.

The only requirements are:

NickDarvey commented 7 years ago

Okay, I had a brief look into this and struggled to bend to my contractual whim.

Would you be adverse to exposing Map's readonly value as a private settable like you do with MapItem's key and value?

And also, how the heck does JSON.Net figure get your Map structure out into an array? Does it automagically use the enumerator? (As an aside, might it be better if Map serialized as a dictionary rather than an array? Would that be semantically better?)

louthy commented 7 years ago

Would you be adverse to exposing Map's readonly value as a private settable like you do with MapItem's key and value?

Not a massive fan of the idea. Because obviously this will be for all types that we want to deserialise, and I like the power of readonly. What did you have in mind? Do you need additional constructors?

And also, how the heck does JSON.Net figure get your Map structure out into an array? Does it automagically use the enumerator?

Yes, it does exactly that. Which causes a few additional headaches. But it's the only way I could find to bend Json.NET to my whim.

As an aside, might it be better if Map serialized as a dictionary rather than an array? Would that be semantically better?

I don't think that's really important. The serialiser doesn't need to represent the inner workings of the type. It merely needs a consistent way to get the data in and out.

NickDarvey commented 7 years ago

Unfortunately Data Contract doesn't do constructors, but it does private settables. This looks like a headache, I think I might leave this one. (To anyone searching, we ended up including private surrogates in our DCs so that it serialized happily but we exposed it through lang-ext.)

RE: dictionary vs array, the only time it's important might be where you're integrating with an external API that expects a dictionary and you want to work in the happy land of Map. Though, it's no cost to consume it as an IReadOnlyDictionary at that point, so /shrug.

StefanBertels commented 7 years ago

Just a note: maybe having a custom ContractResolver might help with Json.NET (http://www.newtonsoft.com/json/help/html/ContractResolver.htm). I think this could result in a helper lib making e.g. NumTypes compatible with Json.NET (and maybe have Map as dictionary, too).

Solution would be without Json.NET attributes, but you would have to set this special JsonSerializer / JsonSerializerSettings in the application code. Perhaps generic NumType etc. need some common interface for detecting it (xy is NumType). Don't know if this is enough (and boxing/unboxing can be done using [Serializable]).

louthy commented 7 years ago

@StefanBertels They already do serialise:

https://github.com/louthy/language-ext/blob/master/LanguageExt.Tests/NewTypeTests.cs#L140