Uploaded image for project: 'C# Driver'
  1. C# Driver
  2. CSHARP-5162

Projection Expressions Fail to Deserialize Data Correctly

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Unknown Unknown
    • 2.28.0
    • Affects Version/s: 2.20.0
    • Component/s: Builders, Serialization
    • None
    • Fully Compatible
    • Dotnet Drivers
    • Not Needed
    • Hide

      1. What would you like to communicate to the user about this feature?
      2. Would you like the user to see examples of the syntax and/or executable code and its output?
      3. Which versions of the driver/connector does this apply to?

      Show
      1. What would you like to communicate to the user about this feature? 2. Would you like the user to see examples of the syntax and/or executable code and its output? 3. Which versions of the driver/connector does this apply to?

      Starting in driver version 2.20.0 with LINQ2, Builders<T>.Projection.Expression fails to deserialize data correctly in certain cases.

      var projectionTypedCamel = Builders<CamelDocument>.Projection.Expression(x => new CamelDocument { Id = x.Id, Name = x.Name });
      var queryTypedCamel = camelColl.Aggregate().Project(projectionTypedCamel).Limit(1);
      var resultTypedCamel = queryTypedCamel.FirstOrDefault();
      

      The resulting object contains only default values even though the server returned the correct data.

      { "_id" : ObjectId("000000000000000000000000"), "name" : null, "activeSince" : ISODate("0001-01-01T00:00:00Z"), "isActive" : false }
      

      If we switch to pascal casing rather than camel casing, the Name is returned, but not the Id:

      { "_id" : ObjectId("000000000000000000000000"), "Name" : "Test638544783272212590", "ActiveSince" : ISODate("0001-01-01T00:00:00Z"), "IsActive" : false }
      

      If we project into an anonymous object, both the Id and Name are returned correctly:

      { "_id" : ObjectId("66746157f75c0df9cadda90e"), "Name" : "Test638544783270965320" }
      

      Full repro:

      using System;
      using MongoDB.Bson;
      using MongoDB.Bson.Serialization.Conventions;
      using MongoDB.Driver;
      using MongoDB.Driver.Linq;
      
      ConventionRegistry.Register("camelCaseConvention", new ConventionPack { new CamelCaseElementNameConvention() }, t => t == typeof(CamelDocument));
      ConventionRegistry.Register("extraElementsConvention", new ConventionPack { new IgnoreExtraElementsConvention(true) }, _ => true);
      
      var mongoSettings = MongoClientSettings.FromUrl(new MongoUrl("mongodb://localhost:27017"));
      mongoSettings.LinqProvider = LinqProvider.V2;
      var client = new MongoClient(mongoSettings);
      var db = client.GetDatabase("test");
      var pascalColl = db.GetCollection<PascalDocument>("pascal");
      var camelColl = db.GetCollection<CamelDocument>("camel");
      
      pascalColl.DeleteMany("{}");
      camelColl.DeleteMany("{}");
      
      var camel = new CamelDocument
      {
          Id = ObjectId.GenerateNewId(),
          Name = $"Test{DateTime.Now.Ticks}",
          ActiveSince = DateTime.UtcNow,
          IsActive = true
      };
      camelColl.InsertOne(camel);
      var pascal = new PascalDocument
      {
          Id = ObjectId.GenerateNewId(),
          Name = $"Test{DateTime.Now.Ticks}",
          ActiveSince = DateTime.UtcNow,
          IsActive = true
      };
      pascalColl.InsertOne(pascal);
      
      var projectionTypedCamel = Builders<CamelDocument>.Projection.Expression(x => new CamelDocument { Id = x.Id, Name = x.Name });
      var queryTypedCamel = camelColl.Aggregate().Project(projectionTypedCamel).Limit(1);
      var resultTypedCamel = queryTypedCamel.FirstOrDefault();
      
      var projectionTypedPascal = Builders<PascalDocument>.Projection.Expression(x => new PascalDocument { Id = x.Id, Name = x.Name });
      var queryTypedPascal = pascalColl.Aggregate().Project(projectionTypedPascal).Limit(1);
      var resultTypedPascal = queryTypedPascal.FirstOrDefault();
      
      var projectionAnonymousCamel = Builders<CamelDocument>.Projection.Expression(x => new { x.Id, x.Name });
      var queryAnonymousCamel = camelColl.Aggregate().Project(projectionAnonymousCamel).Limit(1);
      var resultAnonymousCamel = queryAnonymousCamel.FirstOrDefault();
      
      var projectionAnonymousPascal = Builders<PascalDocument>.Projection.Expression(x => new { x.Id, x.Name });
      var queryAnonymousPascal = pascalColl.Aggregate().Project(projectionAnonymousPascal).Limit(1);
      var resultAnonymousPascal = queryAnonymousPascal.FirstOrDefault();
      
      Console.WriteLine("Generated MQL is identical across queries (modulo casing of field names)...");
      Console.WriteLine("Query with projection to typed (camel):      " + queryTypedCamel);
      Console.WriteLine("Query with projection to typed (pascal):     " + queryTypedPascal);
      Console.WriteLine("Query with projection to anonymous (camel):  " + queryAnonymousCamel);
      Console.WriteLine("Query with projection to anonymous (pascal): " + queryAnonymousPascal);
      Console.WriteLine("Deserialized results are incorrect for typed queries...");
      Console.WriteLine($"Result with projection (camel) to CamelDocument:   {resultTypedCamel.ToJson()}");
      Console.WriteLine($"Result with projection (pascal) to PascalDocument: {resultTypedPascal.ToJson()}");
      Console.WriteLine($"Result with projection (camel) to anonymous:       {resultAnonymousCamel.ToJson()}");
      Console.WriteLine($"Result with projection (pascal) to anonymous:      {resultAnonymousPascal.ToJson()}");
      
      record CamelDocument
      {
          public ObjectId Id { get; set; }
          public string Name { get; set; }
          public DateTime ActiveSince { get; set; }
          public bool IsActive { get; set; }
      }
      
      record PascalDocument
      {
          public ObjectId Id { get; set; }
          public string Name { get; set; }
          public DateTime ActiveSince { get; set; }
          public bool IsActive { get; set; }
      }
      

      Based on this post in the MongoDB Community Forums. The reporter also provided a self-contained repro.

            Assignee:
            robert@mongodb.com Robert Stam
            Reporter:
            james.kovacs@mongodb.com James Kovacs
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: