[CSHARP-4261] LinqProvider.V3 breaks custom BsonSerializerAttribute Created: 19/Jul/22  Updated: 27/Oct/23  Resolved: 18/Oct/22

Status: Closed
Project: C# Driver
Component/s: LINQ3, Serialization
Affects Version/s: 2.17.0
Fix Version/s: None

Type: Bug Priority: Unknown
Reporter: Damith G Assignee: Robert Stam
Resolution: Works as Designed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

i have the following custom BsonSerializerAttribute that is used to decorate string properties of entities. the serializer implementation simply stores the property value as ObjectId if it's a valid objectid or stores it as a string if not a valid objectid. 

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AsObjectIdAttribute : BsonSerializerAttribute
{
    public AsObjectIdAttribute() : base(typeof(ObjectIdSerializer)) { }
 
    private class ObjectIdSerializer : SerializerBase<string>
    {
        public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, string value)
        {
            if (value == null)
            {
                ctx.Writer.WriteNull(); return;
            }
 
            if (value.Length == 24 && ObjectId.TryParse(value, out var oID))
            {
                ctx.Writer.WriteObjectId(oID); return;
            }
 
            ctx.Writer.WriteString(value);
        }
 
        public override string Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args)
        {
            switch (ctx.Reader.CurrentBsonType)
            {
                case BsonType.String:
                    return ctx.Reader.ReadString();
 
                case BsonType.ObjectId:
                    return ctx.Reader.ReadObjectId().ToString();
 
                case BsonType.Null:
                    ctx.Reader.ReadNull();
                    return null;
 
                default:
                    throw new BsonSerializationException($"'{ctx.Reader.CurrentBsonType}' values are not valid on properties decorated with an [AsObjectId] attribute!");
            }
        }
    }
}

the following works with LinqProvider.V2 but does not work with LinqProvider.V3

public class Person
{
    [BsonId, AsObjectId]
    public string Id { get; set; }
 
    public string Name { get; set; }
}
 
var person = new Person
{
    Id = ObjectId.GenerateNewId().ToString(),
    Name = "john doe"
};
 
await collection.InsertOneAsync(person);
 
var result = await collection
    .AsQueryable()
    .Where(x => x.Id == person.Id)
    .ToListAsync();
 
var matchesFound = result.Count > 0;

i believe the underlying cause is that LINQ3 translate the filter as

"_id" : "62d6bfac3c2e5eef721c0a14"

while LINQ2 correctly translates the query to:

"_id" : ObjectId("62d6bfac3c2e5eef721c0a14")

i've also tried implelemting IBsonDocumentSerializer on the serializer but it doesn't seem to make a difference. LINQ2 does however work without implementing the interface.

any advise would be highly appreciated in order to migrate my existing code to LINQ3.

thanks!



 Comments   
Comment by Robert Stam [ 18/Oct/22 ]

LINQ3 assumes unless told otherwise that a string property is stored as a string in the database, and has special handling for string filters (for example sometimes using regular expressions).

If a string field is NOT stored as a string in the database you can let the LINQ3 translator know that by implementing the `IRepresentationConfigurable` interface in your custom serializer:

private class ObjectIdSerializer : SerializerBase<string>, IRepresentationConfigurable
{
    public BsonType Representation => BsonType.ObjectId; // lets the LINQ3 translators know that this string value is NOT stored as a string in the database
 
    public IBsonSerializer WithRepresentation(BsonType representation) => throw new NotImplementedException(); // throwing is fine
 
    // the rest of your existing code
}

 
See the following branch for a repro confirming this works:

https://github.com/rstam/mongo-csharp-driver/tree/csharp4261

Comment by James Kovacs [ 12/Aug/22 ]

Hi, djnitehawk83@gmail.com,

Thank you for your patience as we investigated. We have been able to reproduce the problem with the provided code. Some additional investigation is required to determine how best to fix the issue.

In the meantime you can work around the issue by implementing IRepresentationConfigurable<T> for your custom serializer to inform the serialization infrastructure that the database representation is different than the C# representation.

public BsonType Representation => BsonType.ObjectId;
public ObjectIdSerializer WithRepresentation(BsonType representation) => throw new NotImplementedException();
                                                                                                                              
IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) => WithRepresentation(representation);

Alternatively you can annotate your Id property with [BsonRepresentation(BsonType.ObjectId)]. If you look at the built-in ObjectIdSerializer, it already performs much the same work as your custom one does.

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

Please let us know if you have any additional questions.

Sincerely,
James

Comment by Dmitry Lukyanov (Inactive) [ 19/Jul/22 ]

Hey djnitehawk83@gmail.com, thanks for your report, we will look at it and come back to you.

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