[CSHARP-4609] InvalidCastException with LinqProvider V3 when passing string as DateTime Created: 11/Apr/23  Updated: 28/Oct/23  Resolved: 20/Apr/23

Status: Closed
Project: C# Driver
Component/s: LINQ3
Affects Version/s: None
Fix Version/s: 2.19.2

Type: Bug Priority: Minor - P4
Reporter: James Kovacs Assignee: Robert Stam
Resolution: Fixed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Problem/Incident
is caused by CSHARP-4499 Support Convert calls to a base type ... Closed
Backwards Compatibility: Fully Compatible
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   

when adding filter like this
Builders<MeasurementDetails>.Filter.Lt(field, myValue);
where field is Expression<Func<MyObjectType, object>> field = x => x.CreationDate
and CreationDate property type is DateTime, but myValue is string (because we filter dynamically depending on user request)

LinqProvider V2 works fine, but V3 version throws exception

System.InvalidCastException : Unable to cast object of type 'System.String' to type 'System.DateTime'.
   at MongoDB.Bson.Serialization.Serializers.DowncastingSerializer`2.Serialize(BsonSerializationContext context, BsonSerializationArgs args, TBase value)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Serialize[TValue](IBsonSerializer`1 serializer, BsonSerializationContext context, TValue value)
   at MongoDB.Driver.OperatorFilterDefinition`2.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
   at MongoDB.Driver.OrFilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
   at MongoDB.Driver.AndFilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
   at MongoDB.Driver.AndFilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
   at MongoDB.Driver.AndFilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
   at MongoDB.Driver.MongoCollectionImpl`1.CreateFindOperation[TProjection](FilterDefinition`1 filter, FindOptions`2 options)
   at MongoDB.Driver.MongoCollectionImpl`1.FindAsync[TProjection](IClientSessionHandle session, FilterDefinition`1 filter, FindOptions`2 options, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.<>c__DisplayClass48_0`1.<FindAsync>b__0(IClientSessionHandle session)
   at MongoDB.Driver.MongoCollectionImpl`1.UsingImplicitSessionAsync[TResult](Func`2 funcAsync, CancellationToken cancellationToken)
   at MongoDB.Driver.IAsyncCursorSourceExtensions.ToListAsync[TDocument](IAsyncCursorSource`1 source, CancellationToken cancellationToken)

So it seems on this line in DowncastingSerializer it throws exception because it simply tries to cast string to DateTime. Not sure if that is intended behavior in new Linq version or a bug?

– created from https://www.mongodb.com/community/forums/t/invalidcastexception-with-linqprovider-v3-when-passing-string-as-datetime/221558/1



 Comments   
Comment by Githook User [ 18/May/23 ]

Author:

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

Message: CSHARP-4609: Replace DowncastingSerializer with more permissive ConvertIfPossibleSerializer when rendering values associated with a FieldDefnition.
Branch: v2.19.x
https://github.com/mongodb/mongo-csharp-driver/commit/366181297c76461db5985aa91d80c14fbc0f3135

Comment by Githook User [ 20/Apr/23 ]

Author:

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

Message: CSHARP-4609: Replace DowncastingSerializer with more permissive ConvertIfPossibleSerializer when rendering values associated with a FieldDefnition.
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/5ee1801080f8e6c4339791389d16023b96c90e6d

Comment by Robert Stam [ 13/Apr/23 ]

This was not an intentional breaking change in LINQ3.

LINQ3 assumes that you are comparing apples to apples. We were not expecting anyone to compare a `DateTime` to a `string`.

The best/cleanest work around would be to change your code to compare apples to apples (`DateTime` to `DateTime` in this case).

We are evaluating whether we should change LINQ3 to also support this somewhat unusual and surprising behavior from LINQ2.

 

Comment by James Kovacs [ 11/Apr/23 ]

We have a field expression of type object where the underlying type is DateTime or any value type.

In LinqProviderAdapterV2.cs, we have the following code in TranslateExpressionToField:

var underlyingSerializer = field.Serializer;
var fieldSerializer = underlyingSerializer as IBsonSerializer<TField>;
var valueSerializer = (IBsonSerializer<TField>)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField);

The underlying serializer is a DateTimeSerializer, which is not an IBsonSerializer<object>. Thus fieldSerializer is null and the resulting valueSerializer is FieldValueSerializerHelper.ConvertIfPossibleSerializer<object, DateTime>. This serializer attempts to convert from a string to a DateTime.

In LINQ3, we introduced the DowncastingSerializer, which resolves CSHARP-4499. This allowed derived instances to be treated as their base type. LinqProviderAdapterV3.cs has identical code as above (though how the field is created differs). Unfortunately DowncastingSerializer<object, DateTime> does in fact implement IBsonSerializer<object> and thus the third line simply returns the DowncastingSerializer<object, DateTime>. When called, it simply tries to cast the incoming object as a Datetime and a string is not a Datetime thus failing. If the second line had returned null, then the third line would have created a FieldValueSerializerHelper.ConvertIfPossibleSerializer<object, DateTime> just as with LINQ2 and an attempt is made to parse the DateTime from the string.

One solution is for ConvertExpressionToFilterFieldTranslator to detect a convert from a value type to object and simply return the existing field definition. Then the fieldSerializer is null as before and the correct FieldValueSerializerHelper.ConvertIfPossibleSerializer<object, DateTime> is created on the third line. Not the most elegant solution, but it replicates the code flow for value types prior to the introduction of DowncastingSerializer.

The following is a repro of the issue:

using System;
using System.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
 
var settings = new MongoClientSettings { LinqProvider = LinqProvider.V3 };
var client = new MongoClient(settings);
var db = client.GetDatabase("test");
var coll = db.GetCollection<MeasurementDetails>("measurements");
 
var myValue = "2023-04-11T15:21:00-0700";
Expression<Func<MeasurementDetails, object>> field = x => x.CreationDate;
var filter = Builders<MeasurementDetails>.Filter.Lt(field, myValue);
var query = coll.Find(filter);
Console.WriteLine(query);
 
record MeasurementDetails(ObjectId Id, DateTime CreationDate);

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