[CSHARP-1883] How to serialise all types containing non-default ctors and read only members Created: 06/Jan/17  Updated: 27/Oct/23  Resolved: 07/Jan/19

Status: Closed
Project: C# Driver
Component/s: Serialization
Affects Version/s: 2.4.1
Fix Version/s: None

Type: Task Priority: Major - P3
Reporter: David Cobbold Assignee: Robert Stam
Resolution: Works as Designed Votes: 0
Labels: question
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: Text File BaseClass.cs     Text File Implementation.cs     Text File Interface.cs     Text File SubClass.cs     Text File TestDocument.cs    

 Description   

I'm trying to configure the bson serialiser so that it's automatically able to serialise / deserialise types in our codebase, and also any system data types. The main issue is in determining how to automatically map constructors of all types so that any read only members will also be serialised, and passed into the correct constructor on deserialisation.

I've hit various issues and have attempted to find ways to resolve them, namely by producing a class map convention that maps constructors / properties, as well as a custom version of NamedParameterCreatorMapConvention that handles situations where the same property is defined in a sub class and its base class.

However I'm not able to get this working, so I've produced an example class structure (attached) that produces a few problems when attempting to serialise using this code:

var testDocument = new TestDocument
{
     Data = new SubClass(new Implementation(100), 200, 300.0)
};
 
collection.InsertOne(testDocument);

The initial problems are:

  • The BaseClass type contains properties that are also declared in the SubClass.
  • The type of one property in the SubClass is different from the type of the same property in the base class (due to the new keyword)

I attempted to work around this by creating a MapConstructorsConvention and modifying the NamedParameterCreatorMapConvention, ensuring that properties are only mapped in the class map of the type highest up the inheritance tree, that has a constructor referencing that property. This has mostly worked but it's far from a clean solution. However there's another problem caused by this:

  • The property DoubleValue on the BaseClass isn't referenced by any constructors in that class but is required by the SubClass implementation. This is where the fix i made above falls down because no types map this property.

Is there a correct / recommended approach to this? Being able to automatically call non-default constructors for all class hierarchies is quite fundamental for us.

On a related note, I see mention on here of integration with JSON.Net which is able to handle these types. What is the status of this?

Thanks



 Comments   
Comment by Robert Stam [ 07/Jan/19 ]

Sorry for the delay in getting back to this.

I've looked at it again and I don't see any way to support this using our automatic class mapping, which assumes that all properties are only defined once (and not redefined in subclasses, as the InterfaceValue property is in your example).

I would recommend that you not define classes like this. I think it's rather confusing to change the type of an existing inherited property in a subclass.

If you absolutely must have classes like this you will have to write custom serializers for these classes.

Comment by David Cobbold [ 12/Jan/17 ]

Ideally it would serialise the members of the type being serialised, and any inherited members. If a member has been overridden it would serialise the overridden member as its overridden type, and not the member of the base class with the same name. (I believe this is how the json.net serialiser works)

This would mean it's up the developer to ensure that the object can be deserialised correctly using the overridden data (e.g by providing a constructor that accepts the overridden property as a parameter).

In this example this means that when we serialise an instance of SubClass, the InterfaceValue member of SubClass is serialised instead of the InterfaceValue member of BaseClass. A constructor is provided that is able to instantiate the object using the overridden data so deserialisation should work.

If a sub class did depend on the data in a property of the base class for initialisation, and that property is overridden in a sub class to a completely different type that isn't assignable to the type of the corresponding constructor parameter argument, then this should fail deserialisation.

e.g

    public abstract class BaseClass2
    {
        protected BaseClass2(double value)
        {
            Value = value;
        }
 
        public double Value { get; }
    }
 
    public class SubClass2 : BaseClass2
    {
        public SubClass2(double value)
            : base(value)
        {
            Value = "Test";
        }
 
        public new string Value { get; set; }
    }

When serialising SubClass2, it would serialise Value as a string. However on deserialisation the constructor depends on a double Value so the serialised data isn't compatible. Therefore this would fail to deserialise.

However in the original example, because a constructor of SubClass can be called using serialised data of SubClass (as long as the overridden property in SubClass was serialised and not the property in BaseClass), then this should be able to deserialise into its constructor.

Comment by Robert Stam [ 11/Jan/17 ]

I'm not surprised that we might have undefined behavior if a subclass redefines an inherited property with a different type. What should even happen here? Are you expecting both properties to be serialized separately but with different element names?

We've done some work to integrate Json.NET with the .NET driver and had some success but we haven't released it. It took a lot of tricky code and we're not 100% sure we want to release it and therefore have to support it...

Comment by David Cobbold [ 09/Jan/17 ]

The title to this should read 'How to serialise all types containing non-default ctors and read only members'

Generated at Wed Feb 07 21:40:56 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.