-
Type: Bug
-
Resolution: Fixed
-
Priority: Unknown
-
Affects Version/s: None
-
Component/s: LINQ3
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 }])}