[CSHARP-4066] Only use regex filters against string properties that are serialized as strings Created: 19/Feb/22  Updated: 28/Oct/23  Resolved: 24/Mar/22

Status: Closed
Project: C# Driver
Component/s: LINQ3
Affects Version/s: 2.14.1
Fix Version/s: 2.15.0

Type: Bug Priority: Major - P3
Reporter: Felix König Assignee: Robert Stam
Resolution: Fixed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
related to CSHARP-3449 custom serializers sometimes not used... Closed
Epic Link: CSHARP-3615
Backwards Compatibility: Fully Compatible

 Description   

 

I have a struct that has an "Id" field mapped using a custom serializer. Unfortunately LINQ queries, in my example Find, does not seem to use the serializer, causing the query to not match any documents.

here is a full test case that reproduces the issue:

class ObjectIdAsStringSerializer : SerializerBase<string>
{
    public static readonly ObjectIdAsStringSerializer Instance = new ObjectIdAsStringSerializer();
 
    public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return context.Reader.ReadObjectId().ToString();
    }
 
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value)
    {
        context.Writer.WriteObjectId(ObjectId.Parse(value));
    }
}
 
struct Thing
{
    public string Id { get; set; }
}
 
[Test]
public async Task test_linq3()
{
    BsonClassMap.RegisterClassMap<Thing>(cm =>
    {
        cm.MapIdProperty(thing => thing.Id)
            .SetSerializer(ObjectIdAsStringSerializer.Instance);
    });
 
    MongoClientSettings settings = MongoClientSettings.FromConnectionString("mongodb://localhost:27017/?replicaSet=rs0");
    settings.LinqProvider = LinqProvider.V2; // V2 = test passes, V3 = test fails
    var client = new MongoClient(settings);
    string dbName = "testdb-" + Guid.NewGuid();
    IMongoDatabase database = client.GetDatabase(dbName);
    const string id = "590df61373b975210006fcdf";
    IMongoCollection<BsonDocument> coll = database.GetCollection<BsonDocument>("testCollection");
    await coll.InsertOneAsync(BsonDocument.Create(new Dictionary<string, object?>
    {
        ["_id"] = ObjectId.Parse(id)
    }));
    IMongoCollection<Thing> typedColl = database.GetCollection<Thing>("testCollection");
    long count = await typedColl.Find(t => t.Id == id).CountDocumentsAsync();
    await client.DropDatabaseAsync(dbName);
    Assert.That(count, Is.EqualTo(1));
}



 Comments   
Comment by Felix König [ 24/Mar/22 ]

Thank you very much, now everything seems to work as expected. All my tests passed 😁

Comment by Robert Stam [ 24/Mar/22 ]

Thank you for following up. You are correct that your original repro still exhibits the problem and having investigated it I understand why and what you could change to make it work.

The root of the problem is that in general we have no way of knowing what a custom serializer will do. So for the most part we assume that a `string` property will actually be serialized as a `string`. Your custom serializer doesn't do that, but we don't know that.

You easiest fix would probably be to just use the standard `StringSerializer` but customize it to convert `strings` to `ObjectIds` when serializing them. The easiest way to do that is to annotate the field like this:

[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }

If you want to use a custom serializer you can do that, but you need to help the driver out a bit by letting it know that your custom serializer does not in fact serialize a `string` as a `string`.

The way to do that is for your serializer to implement the `IRepresentationConfigurable` interface. The driver will detect that your custom serializer implements this interface and can then use it to interrogate whether your custom serializer serializes `strings` as `strings` or not.

To implement this interface implement the following property and method:

public class ObjectIdAsStringSerializer : SerializerBase<string>, IRepresentationConfigurable
{
    public BsonType Representation => BsonType.ObjectId;
 
    public IBsonSerializer WithRepresentation(BsonType representation) => throw new System.NotSupportedException();
 
    ... the rest of your custom serializer ...
}

Since your custom serializer does not support configuring the representation simply throw `NotSupportedException` if `WithRepresentation` is called.

The key here is that the `Representation` property returns `BsonType.ObjectId` (NOT `BsonType.String`) which informs the driver that this `string` is not actually serialized as a `string`.

 

 

Comment by Felix König [ 24/Mar/22 ]

Thank you for taking a look.

I see the issue is marked as Fixed for version 2.15.0. However, I can still reproduce the test failure using my original reproduction. Could you double-check whether the issue is actually fixed in version 2.15.0?

Comment by Githook User [ 02/Mar/22 ]

Author:

{'name': 'rstam', 'email': 'robert@robertstam.org', 'username': 'rstam'}

Message: CSHARP-4066: Only use regex filters against string properties that are serialized as strings.
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/00ac83e69624eaa2131724e70c09b41c5ac8ff26

Comment by James Kovacs [ 23/Feb/22 ]

Simplified repro:

using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
 
var settings = new MongoClientSettings { LinqProvider = LinqProvider.V3 };
var client = new MongoClient(settings);
var db = client.GetDatabase("csharp4066");
var coll = db.GetCollection<C>("coll");
 
var objectId = ObjectId.GenerateNewId();
var query = coll.Find(x => x.Id == objectId.ToString());
Console.WriteLine(query);
 
class C
{
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }
}

With LINQ3, the output is:

find({ "_id" : "62169458c115ae74973ba831" })

This is incorrect as the database type is BsonType.ObjectId, but the comparison is being performed against BsonType.String.

With LINQ2, we compare using the correct database type:

find({ "_id" : ObjectId("621694b00d3b3d226e35a976") })

Comment by James Kovacs [ 23/Feb/22 ]

Hi, de.felix.koenig@gmail.com,

Thank you for reporting this issue. We have confirmed that your repro demonstrates the problem in LINQ3.

LINQ3's ComparisonExpressionToFilterTranslator has short-circuit logic to render string comparisons (line 66) and we never reach line 79 where we render the MQL with the configured serializer. This results in rendering the comparison as "_id": "590df61373b975210006fcdf" rather than "_id": ObjectId("590df61373b975210006fcdf"). Since a string is not the same type as an ObjectId, no documents match.

Please follow this ticket for updates as we work to resolve this issue.

Sincerely,
James

Comment by Esha Bhargava [ 22/Feb/22 ]

de.felix.koenig@gmail.com Thank you for reporting this issue! We'll look into it and get back to you soon.

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