Casting/serialization issue after switching to Linq v3

In my codebase I have a custom type defined together with the custom serializer for that type. After switching to linq v3 several types of queries are exploding with the following error:

Unhandled exception. System.ArgumentException: Invalid toType: System.Guid. (Parameter ‘toType’)
at MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions.AstExpression.Convert(AstExpression input, Type toType, AstExpression onError, AstExpression onNull)

Is there a way to configure the driver so the query rewrite won’t be necessary? Below full repro (driver 2.17.1):

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Security.Authentication;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using MongoDB.Driver.Linq;

BsonSerializer.RegisterSerializer(typeof(Guid), new GuidSerializer(BsonType.String));
BsonSerializer.RegisterSerializer(typeof(InvoiceId), new MyGuidSerializer());
BsonTypeMapper.RegisterCustomTypeMapper(typeof(InvoiceId), new MyGuidBsonTypeMapper());


var settings = MongoClientSettings.FromUrl(new MongoUrl("mongodb://localhost:27017/test"));
settings.SslSettings = new SslSettings {EnabledSslProtocols = SslProtocols.Tls12};
settings.LinqProvider = LinqProvider.V3;
var mongoClient = new MongoClient(settings);
var mongoDatabase = mongoClient.GetDatabase("test");
mongoDatabase.DropCollection("test");
var collection = mongoDatabase.GetCollection<Document>("test");


var guid = Guid.NewGuid();
var invoiceId = new InvoiceId(guid);
var guidNullable = (Guid?) guid;
var invoiceIdNullable = (InvoiceId?) invoiceId;
var document = new Document
{
    InvoiceId = invoiceId,
    InvoiceIdNullable = invoiceId,
    Guid = guid,
    GuidNullable = guid
};
collection.InsertOne(document);

Expression<Func<Document, bool>>[] f =
{
    c => c.Guid == guid,
    c => c.GuidNullable == guid,
    c => c.Guid == invoiceId,
    c => c.GuidNullable == invoiceId,
    c => c.InvoiceId == invoiceId,
    c => c.InvoiceIdNullable == invoiceId,
    
    c => c.Guid == guidNullable,
    c => c.GuidNullable == guidNullable,
    c => c.Guid == invoiceIdNullable,
    c => c.GuidNullable == invoiceIdNullable,
    c => c.InvoiceId == invoiceIdNullable,
    c => c.InvoiceIdNullable == invoiceIdNullable,
    
    c => c.InvoiceId == guidNullable, // explodes in V3
    c => c.InvoiceIdNullable == guidNullable, // explodes in V3
    c => c.InvoiceId == guid, // explodes in V3
    c => c.InvoiceIdNullable == guid, // explodes in V3
};

foreach (var expression in f)
{
    Console.Out.WriteLine(expression.ToString());
        
    var results = collection.AsQueryable().Where(expression).ToCursor().ToList();
    var result = results.FirstOrDefault() ?? throw new Exception("Not found!");
    if (result.InvoiceId != invoiceId)
    {
        throw new Exception("Mismatch!");
    }

    Console.Out.WriteLine("All good");
}

public class Document
{
    public ObjectId Id { get; set; }
    public InvoiceId InvoiceId { get; set; }
    public InvoiceId? InvoiceIdNullable { get; set; }
    public Guid Guid { get; set; }
    public Guid? GuidNullable { get; set; }
}

public readonly record struct InvoiceId(Guid Value)
{
    public static implicit operator Guid(InvoiceId s) => s.Value;
}

public class MyGuidSerializer : SerializerBase<InvoiceId>
{
    public override InvoiceId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        if (context.Reader.CurrentBsonType == BsonType.Null)
        {
            context.Reader.ReadNull();
            return default;
        }

        if (Guid.TryParse(context.Reader.ReadString(), out var guid))
        {
            return new InvoiceId(guid);
        }

        return new InvoiceId(default);
    }

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, InvoiceId value)
    {
        context.Writer.WriteString(value.Value.ToString());
    }
}
    
public class MyGuidBsonTypeMapper : ICustomBsonTypeMapper
{
    public bool TryMapToBsonValue(object value, out BsonValue bsonValue)
    {
        bsonValue = (BsonString)((InvoiceId)value).Value.ToString();
        return true;
    }
}
1 Like

Hi, @Marek_Olszewski,

Thank you for reporting this issue. We have confirmed that LINQ2 passes your tests, but LINQ3 fails for InvoiceId. We really appreciate the time you invested to create a self-contained repro. I have created CSHARP-4332 to track this issue. Please follow that ticket for updates. You can also comment on CSHARP-4332 if you have further questions.

Sincerely,
James

1 Like