[CSHARP-4517] System.InvalidCastException when using custom operator in filter Created: 04/Feb/23  Updated: 28/Oct/23  Resolved: 09/Mar/23

Status: Closed
Project: C# Driver
Component/s: LINQ3
Affects Version/s: 2.19.0
Fix Version/s: 2.19.1

Type: Bug Priority: Unknown
Reporter: x y Assignee: Robert Stam
Resolution: Fixed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Documentation Changes: Not Needed
Documentation Changes Summary:

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?


 Description   

Summary

I created a custom Id type that wraps an integer. I implemented the equality operators for this type. The problem is, when I want to filter in mongoDB with linq and the expression includes this custom operator, the expression translator throws an error.

MongoDB driver version 2.19.0, .NET version 7.0

How to Reproduce

Code:

using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
 
int idToFind = 5;
 
var filter = Builders<MyDocument>.Filter.Where(x => x.Id == idToFind);
var renderedFilter = filter.Render(BsonSerializer.LookupSerializer<MyDocument>(), BsonSerializer.SerializerRegistry);
Console.WriteLine(renderedFilter);
 
public class MyDocument
{
	public MyId Id { get; set; }
	public string Name { get; set; }
}
 
[BsonSerializer(typeof(MyIdSerializer))]
public class MyId
{
 
	public MyId(int id)
	{
		Id = id;
	}
 
	public int Id { get; }
 
	public static bool operator ==(int id, MyId other) => id == other.Id;
	public static bool operator ==(MyId id, int other) => id.Id == other;
	public static bool operator !=(int id, MyId other) => !(id == other);
	public static bool operator !=(MyId id, int other) => !(id == other);
}
 
public class MyIdSerializer : SerializerBase<MyId>
{
	public override MyId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
	{
		return new MyId(context.Reader.ReadInt32());
	}
 
	public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, MyId value)
	{
		context.Writer.WriteInt32(value.Id);
	}
}
 

Exception:

 

Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'MyId'.
   at MongoDB.Bson.Serialization.Serializers.SerializerBase`1.MongoDB.Bson.Serialization.IBsonSerializer.Serialize(BsonSerializationContext context, BsonSerializationArgs args, Object value)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Serialize(IBsonSerializer serializer, BsonSerializationContext context, Object value)
   at MongoDB.Driver.Linq.Linq3Implementation.Misc.SerializationHelper.SerializeValue(IBsonSerializer serializer, Object value)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ExpressionTranslators.ComparisonExpressionToFilterTranslator.Translate(TranslationContext context, BinaryExpression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ExpressionToFilterTranslator.TranslateUsingQueryOperators(TranslationContext context, Expression expression)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ExpressionToFilterTranslator.Translate(TranslationContext context, Expression expression, Boolean exprOk)
   at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ExpressionToFilterTranslator.TranslateLambda(TranslationContext context, LambdaExpression lambdaExpression, IBsonSerializer parameterSerializer, Boolean asRoot)
   at MongoDB.Driver.Linq.Linq3Implementation.LinqProviderAdapterV3.TranslateExpressionToFilter[TDocument](Expression`1 expression, IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry)
   at MongoDB.Driver.ExpressionFilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
   at MongoDB.Driver.FilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry)
   at Program.<Main>$(String[] args) in C:\src\MongoTest\MongoTest\Program.cs:line 9 

Additional Background

The issue is probably in ComparisonExpressionToFilterTranslator. It tries to use the serializer of the leftExpression for serializing the right operand:

 

https://github.com/mongodb/mongo-csharp-driver/blob/5d1353dc000613c46184636c8a30c86c46970905/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ExpressionTranslators/ComparisonExpressionToFilterTranslator.cs#L78-L80

 

var field = ExpressionToFilterFieldTranslator.Translate(context, leftExpression);
var serializedComparand = SerializationHelper.SerializeValue(field.Serializer, comparand);
return AstFilter.Compare(field, comparisonOperator, serializedComparand); 



 Comments   
Comment by Githook User [ 24/Mar/23 ]

Author:

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

Message: CSHARP-4517: Values being compared must be compatible.
Branch: v2.19.x
https://github.com/mongodb/mongo-csharp-driver/commit/da9d055cc1eee14a129e062ba9914c2816113c9c

Comment by Githook User [ 09/Mar/23 ]

Author:

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

Message: CSHARP-4517: Values being compared must be compatible.
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/202be2527f1059ab5f8cb292d61264c65e725861

Comment by Robert Stam [ 09/Mar/23 ]

Turns out that values being compared don't have to be the same type, but they do have to be compatible with each other, where compatible is defined as:

1. Both values are of the same type

2. One of the values is null

3. One of the values is of a type that could be assigned to the other (without using implicit conversions)

Comment by Robert Stam [ 06/Feb/23 ]

The LINQ3 translator assumes that values being compared must be of the same type, but we didn't verify that assumption. I've added code to enforce that assumption.

We can't translate custom comparison operators. There is no way (in general) to know what the server side equivalent of a user written custom comparison operator might be.

If this example worked at all in LINQ2 it would have been by coincidence, not by design.

The code change I've made now throws an exception that includes "values being compared must be of the same type" as part of the exception message.

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