[CSHARP-3175] Regression in v2.10.2 - No matching creator found Created: 04/Aug/20  Updated: 27/Oct/23  Resolved: 30/Sep/20

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

Type: Bug Priority: Major - P3
Reporter: John Knoop Assignee: Dmitry Lukyanov (Inactive)
Resolution: Works as Designed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Duplicate
duplicates CSHARP-3108 Deserialization throws No matching cr... Closed
Related
related to CSHARP-2889 BsonClassMap.LookupClassMap supports ... Closed
related to CSHARP-3845 Deserialize Select of anonymous types... Closed

 Description   

This is my Warehouse class:

public class Warehouse
{
	public string Id { get; private set; }
	public string Name { get; private set; }
	public LocationAddress? Address { get; private set; }
	public WarehouseShelfSetting? ShelfSettings { get; private set; }
	public WarehouseTrolleySettings? TrolleySettings { get; private set; }
 
	public Warehouse(string name)
	{
		this.Name = name;
		this.Id = ObjectId.GenerateNewId().ToString();
	}
 
	public Warehouse(string name, LocationAddress? address)
	{
		this.Name = name;
		this.Address = address;
		this.Id = ObjectId.GenerateNewId().ToString();
	}
 
	public Warehouse(string id, string name, LocationAddress? address)
	{
		this.Name = name;
		this.Address = address;
		this.Id = id;
	}
 
	public class WarehouseTrolleySettings
	{
		public LabellingStrategy SlotLabelling { get; set; }
		public int NumberOfSlots { get; set; }
	}
 
	public class WarehouseShelfSetting
	{
		public LabellingStrategy ShelfLabelling { get; set; }
		public LabellingStrategy SlotLabelling { get; set; }
	}
 
	public enum LabellingStrategy
	{
		Alphabetic,
		Numeric
	}
}

This is the document in the database:

{
    "_id" : ObjectId("5c534452d3224cc69bdcb6ac"),
    "Name" : "Centrallagret",
    "Address" : {
        "StreetAddress" : "Storgatan 1",
        "StreetAddress2" : null,
        "PostalCode" : "123 45",
        "City" : "Stockholm",
        "CountryCode" : "se"
    }
}

This code has been the same for very long time, but today I upgraded the MongoDB.Driver package, and now I get this exception:

System.FormatException: An error occurred while deserializing the Address property of class Zwiftly.Items.Warehouses.Warehouse: No matching creator found.
 ---> MongoDB.Bson.BsonSerializationException: No matching creator found.
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.ChooseBestCreator(Dictionary`2 values)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.CreateInstanceUsingCreator(Dictionary`2 values)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeClass(BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.Serializers.SerializerBase`1.MongoDB.Bson.Serialization.IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize(IBsonSerializer serializer, BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeMemberValue(BsonDeserializationContext context, BsonMemberMap memberMap)
   --- End of inner exception stack trace ---
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeMemberValue(BsonDeserializationContext context, BsonMemberMap memberMap)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeClass(BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize[TValue](IBsonSerializer`1 serializer, BsonDeserializationContext context)
   at MongoDB.Driver.Core.Operations.CursorBatchDeserializationHelper.DeserializeBatch[TDocument](RawBsonArray batch, IBsonSerializer`1 documentSerializer, MessageEncoderSettings messageEncoderSettings)
   at MongoDB.Driver.Core.Operations.FindCommandOperation`1.CreateCursorBatch(BsonDocument commandResult)
   at MongoDB.Driver.Core.Operations.FindCommandOperation`1.CreateCursor(IChannelSourceHandle channelSource, BsonDocument commandResult)
   at MongoDB.Driver.Core.Operations.FindCommandOperation`1.ExecuteAsync(RetryableReadContext context, CancellationToken cancellationToken)
   at MongoDB.Driver.Core.Operations.FindOperation`1.ExecuteAsync(RetryableReadContext context, CancellationToken cancellationToken)
   at MongoDB.Driver.Core.Operations.FindOperation`1.ExecuteAsync(IReadBinding binding, CancellationToken cancellationToken)
   at MongoDB.Driver.OperationExecutor.ExecuteReadOperationAsync[TResult](IReadBinding binding, IReadOperation`1 operation, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.ExecuteReadOperationAsync[TResult](IClientSessionHandle session, IReadOperation`1 operation, ReadPreference readPreference, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.UsingImplicitSessionAsync[TResult](Func`2 funcAsync, CancellationToken cancellationToken)
   at MongoDB.Driver.IAsyncCursorSourceExtensions.FirstOrDefaultAsync[TDocument](IAsyncCursorSource`1 source, CancellationToken cancellationToken)

I backed version by version and found that this regression was introduced in version 2.10.2.



 Comments   
Comment by Alessandro Piccione [ 06/Feb/22 ]

For an F# record like this:

type Fund = {     
  Id: string     
  CompanyId: string    
  LastChangeDate: DateTime 
}

where the *LastChangeDate* field was added recently, I use ""MapProperty"" to set a default value for non existing values, and avoid the error:
> No matching creator found.: BsonSerializationException

map.MapProperty(fun f -> f.LastChangeDate).SetDefaultValue(DateTime(2000, 01, 01)) |> ignore

Comment by Dmitry Lukyanov (Inactive) [ 30/Sep/20 ]

Hey knoopjohn@gmail.com ,

This program will fail, but if I remove the deletion of the optional property on the nested class, then it works.

the reason is related to the fact that we apply immutable convention only for classes that have a constructor with the same argument names as the class properties. In your example, the constructor of MyStandaloneEntity has 3 arguments, but the number of properties is 4.

But I can't do the same using the fluent configuration:

you can use a different overriding:

map.MapMember(x => x.MyOptionalProperty).SetDefaultValue(() => null);

 

Comment by John Knoop [ 16/Aug/20 ]

Sorry to bombard you with questions, but I feel I need to understand all the ins and outs around this.

If I decorate the property with the attribute you suggested, then it works as I'd like it:

[BsonDefaultValue(null)]
public string? MyOptionalProperty { get; private set; }

But I can't do the same using the fluent configuration:

var map = BsonClassMap.RegisterClassMap<SharedClass>();
map.AutoMap();
map.MapMember(x => x.MyOptionalProperty).SetDefaultValue(null);

Because then I get this exception:

System.ArgumentNullException : Value cannot be null. (Parameter 'defaultValueCreator')

Comment by John Knoop [ 16/Aug/20 ]

Also... how come this only applies to nested objects? Have a look at this example:

 

Model:

 public class MyStandaloneEntity
 {
 public MyStandaloneEntity(string name, MyNestedClass myObject, string? myOptionalProperty = null)
 
{ Id = ObjectId.GenerateNewId().ToString(); Name = name; MyOptionalProperty = myOptionalProperty; MyObject = myObject; }
 
public string Id
 
{ get; private set; }
 public string Name \{ get; private set; }
 
public MyNestedClass MyObject
 
{ get; private set; }
 public string? MyOptionalProperty \{ get; private set; }
 
}
 
public class MyNestedClass
 {
 public MyNestedClass(string name, string? myOptionalProperty = null)
 
{ Name = name; MyOptionalProperty = myOptionalProperty; }
 
public string Name
 
{ get; private set; }
 public string? MyOptionalProperty \{ get; private set; }
 
}

Program:

 var collection = _mongoClient.GetDatabase("TestDb").GetCollection<MyStandaloneEntity>("MyStandaloneEntities");
 var entity = new MyStandaloneEntity("Bengt", new MyNestedClass("Hej", "test"));
 
await collection.InsertOneAsync(entity);
 
var deletePropDefinition = Builders<MyStandaloneEntity>.Update.Unset(x => x.MyOptionalProperty);
 var deleteChildPropDefinition = Builders<MyStandaloneEntity>.Update.Unset(x => x.MyObject.MyOptionalProperty);
 await collection.UpdateOneAsync(x => x.Id == entity.Id, deletePropDefinition); 
 await collection.UpdateOneAsync(x => x.Id == entity.Id, deleteChildPropDefinition); // <-- **** This makes it fail ****
 
var readInstance = await collection.FindAsync(x => x.Id == entity.Id);

This program will fail, but if I remove the deletion of the optional property on the nested class, then it works.

Comment by John Knoop [ 14/Aug/20 ]

Do you know if BsonDefaultValue(null) can be achieved using a convention? I would like missing properties to default to null by default (unless then property on the type isn't nullable of course).

Comment by John Knoop [ 14/Aug/20 ]

Oh, I thought that since that parameter is nullable in the constructor, that it would still be allowed. Then I see how it's the same issue as the one you refered to. Thanks.

Comment by Robert Stam [ 14/Aug/20 ]

Your reproduction involves a document that is *missing* one of the fields of the Address document (no StreetAddress2).

 

{
    "_id" : ObjectId("5c534452d3224cc69bdcb6ac"),
    "Name" : "Centrallagret",
    "Address" : {
        "StreetAddress" : "test",
        "PostalCode" : "test",
        "City" : "test",
        "CountryCode" : "se"
    }
}

 

which means it is the same issue as CSHARP-3108.

You can tell the driver to use a default value of null when StreetAddress2 is missing using the [BsonDefaultValue(null)] annotation.

Comment by John Knoop [ 14/Aug/20 ]

I managed to create a blank project that reproduced the issue.

You can find it here: https://github.com/johnknoop/CSHARP-3175-reproduction

Comment by Robert Stam [ 14/Aug/20 ]

It should be reproducible by just removing any one of the fields of the Address document in the JSON string.

But then it's the same situation as CSHARP-3108.

Comment by John Knoop [ 14/Aug/20 ]

Yes, I'm using nullable reference types from C#8.

Let me see if I can get a fully reproducable example and send it.

Comment by Robert Stam [ 14/Aug/20 ]

I am unable to reproduce this.

Here's my full test program:

using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using System;
 
#nullable enable
 
namespace TestCSharp3175
{
    public class Warehouse
    {
        public string Id { get; private set; }
        public string Name { get; private set; }
        public LocationAddress? Address { get; private set; }
        public WarehouseShelfSetting? ShelfSettings { get; private set; }
        public WarehouseTrolleySettings? TrolleySettings { get; private set; }
 
        public Warehouse(string name)
        {
            this.Name = name;
            this.Id = ObjectId.GenerateNewId().ToString();
        }
 
        public Warehouse(string name, LocationAddress? address)
        {
            this.Name = name;
            this.Address = address;
            this.Id = ObjectId.GenerateNewId().ToString();
        }
 
        public Warehouse(string id, string name, LocationAddress? address)
        {
            this.Name = name;
            this.Address = address;
            this.Id = id;
        }
 
        public class WarehouseTrolleySettings
        {
            public LabellingStrategy SlotLabelling { get; set; }
            public int NumberOfSlots { get; set; }
        }
 
        public class WarehouseShelfSetting
        {
            public LabellingStrategy ShelfLabelling { get; set; }
            public LabellingStrategy SlotLabelling { get; set; }
        }
 
        public enum LabellingStrategy
        {
            Alphabetic,
            Numeric
        }
    }
 
    public class LocationAddress
    {
        public LocationAddress(LocationAddress address)
            : this(address.StreetAddress, address.StreetAddress2, address.PostalCode, address.City, address.CountryCode) { }
 
        // [JsonConstructor]
        public LocationAddress(string streetAddress, string? streetAddress2, string postalCode, string city, string countryCode)
        {
            this.StreetAddress = streetAddress;
            this.PostalCode = postalCode;
            this.City = city;
            StreetAddress2 = streetAddress2;
            CountryCode = countryCode;
        }
 
        public string StreetAddress { get; private set; }
        public string? StreetAddress2 { get; private set; }
        public string PostalCode { get; private set; }
        public string City { get; private set; }
 
        /// <summary>
        /// Two-letter
        /// </summary>
        public string CountryCode { get; private set; }
    }
 
    public static class Program
    {
        public static void Main(string[] args)
        {
            var json =
                "{" +
                "	\"_id\" : \"5c534452d3224cc69bdcb6ac\"" +
                "	\"Name\" : \"Centrallagret\"" +
                "	\"Address\" : {" +
                "		\"StreetAddress\" : \"Storgatan 1\"" +
                "		\"StreetAddress2\" : null" +
                "		\"PostalCode\" : \"123 45\"" +
                "		\"City\" : \"Stockholm\"" +
                "		\"CountryCode\" : \"se\"" +
                "	}" +
                "}";
 
            var document = BsonSerializer.Deserialize<Warehouse>(json);
            Console.WriteLine(document.ToJson(new JsonWriterSettings { Indent = true }));
        }
    }
}

No exception is thrown and the document round trips without error. The output of the test program is:

{
  "_id" : "5c534452d3224cc69bdcb6ac",
  "Name" : "Centrallagret",
  "Address" : {
    "StreetAddress" : "Storgatan 1",
    "StreetAddress2" : null,
    "PostalCode" : "123 45",
    "City" : "Stockholm",
    "CountryCode" : "se"
  },
  "ShelfSettings" : null,
  "TrolleySettings" : null
}

 Notes:

  • I commented out the [JsonConstructor] attribute but it should not be relevant to this test
  • I had to adjust the JSON string so that the _id is a string to match the declaration in the class

 

Comment by Robert Stam [ 14/Aug/20 ]

Can you confirm that you are using C# 8.0 and the new nullable reference types feature here:

public LocationAddress? Address { get; private set; }

I was assuming that `LocationAddress` was a struct and that `LocationAddress?` meant `Nullable<LocationAddress>`.

Comment by John Knoop [ 14/Aug/20 ]

The first constructor might look a little weird in isolation, but this type is also being inherited and the subtypes use the first constructor.

Comment by John Knoop [ 14/Aug/20 ]

Hi Robert. Certainly, here it is:

public class LocationAddress
{
	public LocationAddress(LocationAddress address) 
		: this(address.StreetAddress, address.StreetAddress2, address.PostalCode, address.City, address.CountryCode) {}
 
	[JsonConstructor]
	public LocationAddress(string streetAddress, string? streetAddress2, string postalCode, string city, string countryCode)
	{
		this.StreetAddress = streetAddress;
		this.PostalCode = postalCode;
		this.City = city;
		StreetAddress2 = streetAddress2;
		CountryCode = countryCode;
	}
 
	public string StreetAddress { get; private set; }
	public string? StreetAddress2 { get; private set; }
	public string PostalCode { get; private set; }
	public string City { get; private set; }
 
	/// <summary>
	/// Two-letter
	/// </summary>
	public string CountryCode { get; private set; }
}

 The reason for the [JsonConstructor] attribute is that I also serialize and deserialize this type in order to send it over a message queue.

Comment by Robert Stam [ 14/Aug/20 ]

Can you please provide the source code for the LocationAddress class and I will attempt to reproduce? Thanks.

Comment by John Knoop [ 13/Aug/20 ]

No I don't think it is. The persisted document in my case has all the data needed to call any of the constructors.

Comment by Jeffrey Yemin [ 13/Aug/20 ]

This looks like a duplicate of CSHARP-3108, which has been closed as Works as Designed.

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