[CSHARP-4075] Matching creator not found in unwind with missing field that has default value Created: 23/Feb/22  Updated: 27/Oct/23  Resolved: 24/Feb/22

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

Type: Bug Priority: Major - P3
Reporter: Joris Laperre Assignee: James Kovacs
Resolution: Works as Designed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: PNG File DefaultValue.png    

 Description   

Summary

Running MongoDB locally, using driver 2.14.1

Using LINQ (either V2 or V3), when doing an unwind and returning a field that is missing but that has a default value, an error "No matching creator found" is thrown. I also tried making the property for the missing field nullable ("int?"), same problem.

How to Reproduce

Paste the code below into a new console application and run it. The command "var m2 ..." will throw an error. I tried .NET 5.0 and .NET Code 3.1, both exhibit the problem.

Also tried the following things (with the same result):

  • remove the BsonDefaultValue attribute, use nullable type int? instead
  • remove the BsonDefaultValue attribute, create a parameter-less constructor that sets Runtime = 0

using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using MongoDB.Driver.Linq;namespace BugReport
{
    class Program
    {
        class Movie
        {
            [BsonElement("id"), BsonId]
            public ObjectId Id { get; set; }
            [BsonElement("title")]
            public string Title { get; set; }
            [BsonElement("directors")]
            public List<string> Directors { get; set; }
            [BsonElement("runtime")]            [BsonDefaultValue(0)]
            public int Runtime { get; set; }
        }
 
        static void Main(string[] args)
        {
            var connectionString = "mongodb://localhost";
            var clientSettings = MongoClientSettings.FromConnectionString(connectionString);
            clientSettings.LinqProvider = LinqProvider.V2;
            var mongoClient = new MongoClient(clientSettings);
            var db = mongoClient.GetDatabase("bug_report_jla");
 
            var moviesBson = db.GetCollection<BsonDocument>("movies");
            moviesBson.DeleteMany(new BsonDocument());
            moviesBson.InsertOne(new BsonDocument { { "title", "Hannibal" }, { "directors", new BsonArray { "Ridley Scott" } } });            
            Console.WriteLine(moviesBson.AsQueryable().ToList().Count);
 
            var moviesLinq = db.GetCollection<Movie>("movies");
 
            var m1 = moviesLinq.AsQueryable()
                .SelectMany(m => m.Directors, (m, d) => new { Director = d, Title = m.Title })
                .Where(m => m.Director == "Ridley Scott")
                .ToList();
 
            var m2 = moviesLinq.AsQueryable()
                .SelectMany(m => m.Directors, (m, d) => new { Director = d, Title = m.Title, Runtime = m.Runtime })
                .Where(m => m.Director == "Ridley Scott")
                .ToList();
            ;
            Console.ReadKey();
        }
    }
}

Additional Background

I stepped through the debugger code and it appears that <string, string, int> is not considered a suitable creator for <string, string>. I'm new to this code though, so I'm not sure if this makes any sense.



 Comments   
Comment by Joris Laperre [ 24/Feb/22 ]

Hi James,

Thanks for addressing this and taking the time to explain this in detail, much appreciated. Happy with both those workarounds too. Nothing further at this point, thanks again.

Comment by James Kovacs [ 24/Feb/22 ]

Hi, joris@sequel-consulting.com,

Thank you for filing this bug report. We have reproduced the issue and this appears to be expected behaviour. Let me explain in a bit more detail.

When you construct a LINQ query, that query is translated into MQL and sent to the server. The server executes the MQL and returns the results as BSON. The driver then deserializes that BSON into POCOs (or BsonDocuments). In your example, you are deserializing the BSON into anonymous objects of type:

new { Director, Title, Runtime }

The C# compiler actually synthesizes a class during compile time to represent the anonymous type. In this case the compiler called it:

{<>f__AnonymousType1<string, string, int>}

The driver will create a BsonClassMap on the fly to map the incoming BSON results into instances of the anonymous type. Note that we do not create intermediate instances of Movie nor do we use the BsonClassMap<Movie> during the deserialization process. That's why we don't have access to any default values set on the Movie class. The [BsonDefaultValue(0) attribute specifies a default value in the BsonClassMap<Movie>, it does not generate MQL for use by the server.

The root cause of the problem is that you have no way to explicitly configure the BsonClassMap for an anonymous type because C# provides no way of specifying the anonymous type outside of the constructor.

Option #1:
Give your result type an explicit name such as MovieView. Once you give the class a name, you can configure it with BsonDefaultValue and other BSON serialization attributes as necessary.

class MovieView
{
    public string Title { get; set; }
    public string Director { get; set; }
    [BsonDefaultValue(0)]
    public int Runtime { get; set; }
}

Option #2:
Write code that will translate into MQL to supply the default value.

var m2 = moviesLinq.AsQueryable()
    .SelectMany(m => m.Directors, (m, d) => new { Director = d, Title = m.Title, Runtime = m.Runtime ?? 0 })
    .Where(m => m.Director == "Ridley Scott")
    .ToList();

Here I have declared int? Runtime allowing me to specify a default value using Runtime = m.Runtime ?? 0. This translates into the following MQL (surrounding query not shown):

"Runtime" : { "$ifNull" : ["$runtime", 0] }

I have closed this ticket as "Works as Designed", but please let us know if you have any additional questions and we will be happy to answer them.

Sincerely,
James

Comment by Joris Laperre [ 23/Feb/22 ]

In BsonMemberMap.Reset(), I believe line 322 should read

_defaultValueSpecified = (_defaultValue != null);

instead of

_defaultValueSpecified = false;

Comment by Joris Laperre [ 23/Feb/22 ]

Stepped through it again, and I found that although DefaultValue in BsonMemberMap has a value, IsDefaultValueSpecified returns false, see attached screenshot. I think this is why the matching algorithm fails.

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