[CSHARP-1447] Serialization fails when implementation of IDictionary member does not have a public parameterless constructor Created: 16/Oct/15  Updated: 25/Jan/18  Resolved: 02/Feb/16

Status: Closed
Project: C# Driver
Component/s: BSON, Serialization
Affects Version/s: 2.0, 2.0.1
Fix Version/s: 2.3

Type: Bug Priority: Minor - P4
Reporter: James Hadwen Assignee: Robert Stam
Resolution: Done Votes: 0
Labels: regression
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: Text File CSHARP-1447-Unified Diff Since Head.patch     Text File DictionaryTests.fs     Text File MongoDictionarySerializationTest.cs     Text File stacktrace.txt    
Issue Links:
Related
related to CSHARP-2154 Support IReadOnlyDictionary/ReadOnlyD... Closed

 Description   

When attempting to serialize a property of IDictionary<T,U>, if the implementing class does not contain a public parameterless constructor then serialization will fail with exception:

System.ArgumentException: GenericArguments[0], 'Microsoft.FSharp.Core.ExtraTopLevelOperators+CreateDictionary@45[System.String,System.DateTime]', on 'MongoDB.Bson.Serialization.Serializers.DictionaryInterfaceImplementerSerializer`3[TDictionary,TKey,TValue]' violates the constraint of type 'TDictionary'. ---> System.TypeLoadException: GenericArguments[0], 'Microsoft.FSharp.Core.ExtraTopLevelOperators+CreateDictionary@45[System.String,System.DateTime]', on 'MongoDB.Bson.Serialization.Serializers.DictionaryInterfaceImplementerSerializer`3[TDictionary,TKey,TValue]' violates the constraint of type parameter 'TDictionary'.

Example above (stack trace attached) from setting property as return of dict operator in F# Core https://msdn.microsoft.com/en-us/library/ee353774.aspx the implementation of which is the result of an object expression and therefore does not contain a public constructor.

Attached is a unit test that works correctly in 1.x but does not work in 2.x (non F# but same principle).

Exception is resolvable by changing signature of class DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue> to remove the new() constraint, and then changing implementation of DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>.CreateInstance() to return Activator.CreateInstance<TDictionary>()



 Comments   
Comment by Stefano Ricciardi [ 27/Jul/16 ]

I also incurred in this problem trying to serialize a ReadOnlyDictionary<string, object>(), which was working previously with the 1.X driver.

Comment by Githook User [ 02/Feb/16 ]

Author:

{u'username': u'rstam', u'name': u'rstam', u'email': u'robert@robertstam.org'}

Message: CSHARP-1447: Fix copyrights and add new contributor to ReadMe.
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/c4a068aa8a5d2e949f1b00d839a10256073e60e8

Comment by Githook User [ 02/Feb/16 ]

Author:

{u'username': u'jhadwen', u'name': u'James Hadwen', u'email': u'james.hadwen@sociustec.com'}

Message: CSHARP-1447: Allow serialization of generic IDictionary implementations with non-public constructors when serializing to an interface definition
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/4d63c8ee0676a348274891cdf1090a5c118b313d

Comment by James Hadwen [ 19/Oct/15 ]

Thanks for looking at this.

The dictionary class in the test is somewhat contrived, it wasn't my intention that the class would be an example of a "known type", i.e. where a custom serializer could be applied at the repository layer; but instead an example of how behaviour that worked previously no longer does.

I've attached another test (DictionaryTests.fs), this time written in F# but using something far more standard, i.e. the call to the dict operator. The implementation of which (https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/fslib-extra-pervasives.fs dictImpl) would be, for all reasonable means, impossible to implement a custom serializer for for.

Just to summarise my thoughts on this issue

  • The default behavioural contract for mapped class members of declared type IDictionary<T,U> is
    • Serialized to BSON with no special treatment
      • See unit test snippet below
    • Deserialized to type Dictionary<T,U>
  • The new() constraint is a breaking change
    • DictionaryInterfaceImplementerSerializer is now making a run-time decision on whether a dictionary should be serialized instead of whether it can be serialized
    • The ability to serialize is now dependent on the failure to deserialize, regardless of whether the behaviour contract is implementation-type-bi-directional
      • I've no issues with a failure to deserialize if no public parameterless constructor, however I feel that this was already handled correctly and did not require a change of error
      • Even in the case when the property is declared as a concrete type with no-available-constructor, should this be the responsibility of the driver to enforce? (e.g. a case where the read and write class maps are different?)
    • Could be argued that new() is designed for use as compile-time constraint https://msdn.microsoft.com/en-us/library/d5x73970.aspx
    • "newing" the type parameter is just an alias for Activator.CreateInstance http://stackoverflow.com/questions/1649066/activator-createinstancet-vs-new

        [Test]
        public void TestClassMapContractIsMaintained()
        {
            var d = new Dictionary<object, object> { { "A", new C { P = "x" } } };
			//NOTE: id is implementation of SortedDictionary
            var id = CreateSortedDictionary(d);
            var sd = CreateSortedDictionary(d);
            var sl = CreateSortedList(d);
            var obj = new T { D = d, ID = id, SD = sd, SL = sl };
            var json = obj.ToJson();
            var rep = "{ 'A' : { '_t' : 'DictionaryGenericSerializers.C', 'P' : 'x' } }";
            var expected = "{ 'D' : #R, 'ID' : #R, 'SD' : #R, 'SL' : #R }".Replace("#R", rep).Replace("'", "\"");
            Assert.AreEqual(expected, json);
 
            var bson = obj.ToBson();
            var rehydrated = BsonSerializer.Deserialize<T>(bson);
            Assert.IsInstanceOf<Dictionary<object, object>>(rehydrated.D);
            //NOTE: regardless of serialized implementation class no special treatment occurs
            Assert.IsInstanceOf<Dictionary<object, object>>(rehydrated.ID);
            Assert.IsInstanceOf<SortedDictionary<object, object>>(rehydrated.SD);
            Assert.IsInstanceOf<SortedList<object, object>>(rehydrated.SL);
            Assert.IsTrue(bson.SequenceEqual(rehydrated.ToBson()));
        }

Comment by Robert Stam [ 18/Oct/15 ]

The gist of the problem here is that the custom ConstructorLessDictionary<TKey, TValue> class does not have a public parameterless constructor, so how should the driver create an instance of this class? Well, in this example, it turns out that intended way to create an instance of the ConstructorLessDictionary<TKey, TValue> class is to call the public static factory method called ConstructorReplacement.

One way to get the driver to use this public static factory method instead of the non-existent public parameterless constructor is to hook up a simple custom serializer for the custom ConstructorLessDictionary<TKey, TValue> class. It would look like this:

[BsonSerializer(typeof(ConstructorLessDictionarySerializer<,>))]
public class ConstructorLessDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    // as defined in your sample code
}
 
public class ConstructorLessDictionarySerializer<TKey, TValue> : DictionarySerializerBase<ConstructorLessDictionary<TKey, TValue>, TKey, TValue>
{
    protected override ConstructorLessDictionary<TKey, TValue> CreateInstance()
    {
        return ConstructorLessDictionary<TKey, TValue>.ConstructorReplacement();
    }
}

I don't know if this would address your particular need or not, but it does show one way to hook up a public static factory method as an alternative to public parameterless constructor.

Comment by Robert Stam [ 18/Oct/15 ]

The .NET driver's serialization design normally requires a public parameterless constructor (used to instantiate the object being deserialized) and settable public properties (used to populate the values of the object being deserialized). When using the BsonClassMapSerializer the BsonClassMap can be configured to you use alternate constructors, but they still must be public.

I don't think it would be safe in general to call a private parameterless constructor instead. It's normally private for a reason.

I'll continue to look more closely at your provided code samples.

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