In MongoDB C# Driver version 3.2.1, the LinqProvider V3 incorrectly generates MongoDB queries when performing projections on nested collections that contain ObjectId fields. This results in incorrect query results without throwing any exceptions. This behavior differs from the previous 2.30.0 version of the driver, which correctly preserves ObjectId representations in projections.
Repro code (it could be probably narrowed down even more):
BsonClassMap.RegisterClassMap<Document>();
BsonClassMap.RegisterClassMap<Chain>();
BsonClassMap.RegisterClassMap<Unit>();
var connectionString = "mongodb://localhost/db?directConnection=true";
var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString));
settings.ClusterConfigurator = builder =>
{
builder.Subscribe<CommandStartedEvent>(e =>
{
Console.WriteLine($"Command {e.CommandName} -> {e.Command.ToJson()}");
});
};
var client = new MongoClient(settings);
// bootstrap
var collection = client.GetDatabase("db").GetCollection<Chain>("test");
var id1 = BsonObjectId.Create(ObjectId.GenerateNewId());
var id2 = BsonObjectId.Create(ObjectId.GenerateNewId());
var id3 = BsonObjectId.Create(ObjectId.GenerateNewId());
var chain = new Chain()
{
Parts = new List<Unit>()
{
new()
{
Refs = new List<Document>()
{
new()
{
id = id1.ToString(),
},
new()
{
id = id2.ToString(),
},
new()
{
id = id3.ToString(),
},
}
}
}
};
await collection.InsertOneAsync(chain);
// end bootstrap
List<string> jobIds = [id2.ToString()];
var findFluent = collection
.Find(x => x.Parts.Any(a => a.Refs.Any(b => jobIds.Contains(b.id))))
.Project(chain => new { chain.Parts
.First(p => p.Refs.Any(j => jobIds.Contains(j.id)))
.Refs.First(j => jobIds.Contains(j.id)).id });
var result = await findFluent.ToListAsync();
// Driver 2.30.0 result.First().id is not null
// Driver 3.2.1 result.First().id is null
public class Document
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string id { get; set; }
}
public class Chain : Document
{
public ICollection<Unit> Parts { get; set; } = new List<Unit>();
}
public class Unit
{
public ICollection<Document> Refs { get; set; }
public Unit()
{
Refs = new List<Document>();
}
}
the resulting query when using driver 2.30.0 is:
find({ "Parts" : { "$elemMatch" : { "Refs" : { "$elemMatch" : { "_id" : { "$in" : [ObjectId("67dc3b79a35f548871c29233")] } } } } } }, { "_id" : { "$let" : { "vars" : { "this" : { "$arrayElemAt" : [{ "$filter" : { "input" : { "$let" : { "vars" : { "this" : { "$arrayElemAt" : [{ "$filter" : { "input" : "$Parts", "as" : "p", "cond" : { "$anyElementTrue" : { "$map" : { "input" : "$$p.Refs", "as" : "j", "in" : { "$in" : ["$$j._id", [ObjectId("67dc3b79a35f548871c29233")]] } } } } } }, 0] } }, "in" : "$$this.Refs" } }, "as" : "j", "cond" : { "$in" : ["$$j._id", [ObjectId("67dc3b79a35f548871c29233")]] } } }, 0] } }, "in" : "$$this._id" } } })
for the new driver 3.2.1 it’s:
find({ "Parts" : { "$elemMatch" : { "Refs" : { "$elemMatch" : { "_id" : { "$in" : [{ "$oid" : "67dc3bc04c5e85b06180264f" }] } } } } } }, { "_id" : { "$let" : { "vars" : { "this" : { "$arrayElemAt" : [{ "$filter" : { "input" : { "$let" : { "vars" : { "this" : { "$arrayElemAt" : [{ "$filter" : { "input" : "$Parts", "as" : "p", "cond" : { "$anyElementTrue" : { "$map" : { "input" : "$$p.Refs", "as" : "j", "in" : { "$in" : ["$$j._id", ["67dc3bc04c5e85b06180264f"]] } } } } } }, 0] } }, "in" : "$$this.Refs" } }, "as" : "j", "cond" : { "$in" : ["$$j._id", ["67dc3bc04c5e85b06180264f"]] } } }, 0] } }, "in" : "$$this._id" } } })
clearly, the ObjectId or $oid is missing in the inner selections of _id.
This is especially dangerous issue in our codebase because it alters the final results without any errors or exceptions.