[CSHARP-3731] Expression doesn't execute on composed class properly Created: 09/Jul/21  Updated: 28/Oct/23  Resolved: 15/Nov/22

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

Type: Bug Priority: Unknown
Reporter: John Fedorchak Assignee: Robert Stam
Resolution: Fixed Votes: 0
Labels: serialization
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

The problem we are experiencing is that we have C# classes such as the following (PascalCase):

public class InstanceData {
    public string? InstanceName { get; set; }
}
 
public class Model {
    public int Version { get; set; }
    public InstanceData? Data { get; set; }
}

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    \\ ...
 
    ConventionRegistry.Register("MyConvention",
        new ConventionPack { new CamelCaseElementNameConvention() },
        t => \* Some Condition *\);
 
    \\ ...
}

and data in MongoDb as such (camelCase):

_id: ObjectId("...")
version: 1
data: Object
    instanceName: "TestInstance123"

The problem is, when Model gets mapped, it creates a member map for Version just fine, but for Data, an expression is created. This expression, when further evaluated within the Freeze() method, creates a trie where all children of the root node are lowercase. It then tries to look up "InstanceName" from a trie which has children [ "i" ], and throws an exception since the capitalization differs. It looks like it's creating a NEW ClassMap/Trie for Model (where both ElementName and MemberName are pascal case ), but using the cached map for InstanceData. The cached one has all MemberName's in pascal case, but all ElementName's in camel case. It looks like we have a total of 3 Trie's when really we need 4? Or perhaps only 2?

Example Exception:

An error occurred while deserializing the Data property of class MyNamespace.Model: 
Element 'InstanceName' does not match any field or property of class MyNamespace.Data.

This appears to be a bug with the cached serializer (or classMap?) for the InstanceData class.

Example code:

public class Program
{
    public static void Main(string[] args)
    {
        ConventionRegistry.Register("MyConvention", new ConventionPack { new CamelCaseElementNameConvention() }, t => true);
 
        var client = new MongoClient(/* Connection String */);
        var database = client.GetDatabase(/* Database */);
 
        var documents = database.GetCollection<Model>(/* Collection */).AsQueryable().AsQueryable()
            // .Select(m => m) // This one works just fine.
            // .Select(m => new Model { Version = m.Version, Data = m.Data }) // This one works just fine as well.
            .Select(m => new Model { Version = m.Version, Data = m.Data != null ? new InstanceData { InstanceName = m.Data.InstanceName } : default }) // This one does not work. Can't find "InstanceName" in class "Data".
            .Take(1000)
            .ToArray();
    }
}

A few debug values/expressions from above query:

MongoQueryableImpl.GetEnumerator:
{aggregate([]).Select(m => new Model() {Version = m.Version, Data = IIF((m.Data != null), new InstanceData() {InstanceName = m.Data.InstanceName}, null)}).Take(1000)}
 
PipelineBinderBase.BindMethodCall:
{[MyDb.MyCollection].Select(new Model() {Version = {document}{version}, Data = IIF(({document}{Data} != null), new InstanceData() {InstanceName = {document}{Data}{instanceName}}, null)}).Take(1000)}
 
MongoQueryProviderImpl.Prepare:
{[MyDb.MyCollection].Select(new Model() {Version = {document}{version}, Data = IIF(({document}{Data} != null), new InstanceData() {InstanceName = {document}{Data}{instanceName}}, null)}).Take(1000)}
 
MongoQueryProviderImpl.Execute:
{() => Convert(value(MongoDB.Driver.Linq.MongoQueryProviderImpl`1[MyNamespace.Model]).ExecuteModel(aggregate([{ "$project" : { "Version" : "$version", "Data" : { "$cond" : [{ "$ne" : ["$Data", null] }, { "InstanceName" : "$Data.instanceName" }, null] }, "_id" : 0 } }, { "$limit" : 1000 }])), IAsyncCursor`1).ToEnumerable(value(System.Threading.CancellationToken))}
 
MongoQueryProviderImpl.ExecuteModel:
{aggregate([{ "$project" : { "Version" : "$version", "Data" : { "$cond" : [{ "$ne" : ["$Data", null] }, { "InstanceName" : "$Data.instanceName" }, null] }, "_id" : 0 } }, { "$limit" : 1000 }])}



 Comments   
Comment by James Kovacs [ 14/Nov/22 ]

This issue has been fixed in the new LINQ provider (known as LINQ3), which was introduced in the 2.14 release.

Configure your MongoClientSettings to use LinqProvider.V3 if you want to use this functionality.

To configure a client to use the LINQ3 provider use code like the following

var connectionString = "mongodb://localhost";
var clientSettings = MongoClientSettings.FromConnectionString(connectionString);
clientSettings.LinqProvider = LinqProvider.V3;
var client = new MongoClient(clientSettings);

Comment by James Kovacs [ 09/Jul/21 ]

Hi, jfedorchak@meso-scale.com,

Thank you for creating an issue from your PR and providing a detailed reproduction. We verified that the problematic IIF query throws an exception whereas the other two do not.

We are currently working on a new LINQ provider and we have confirmed that the problematic IIF query executes successfully using the new provider. This new LINQ provider will be available in an upcoming release on an opt-in basis. We will update this ticket when the new LINQ provider is available.

Sincerely,
James

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