From 36e904773ddffb04775c1b438705ef97330a37a1 Mon Sep 17 00:00:00 2001 From: maumar Date: Tue, 11 Mar 2025 02:31:27 -0700 Subject: [PATCH] Fix to #35299 - EF Core InMemory database throws an argument exception on Nullable<>.ToString Problem was that when rewriting the (inmemory) query, sometimes we change the nullability of the expression fragment. (e.g. when accessing a property on an optional entity). We then have a logic to compensate for the change. E.g. when the modified fragment was a caller of a method, we add a null check and only call the method (as non-nullable argument) if the value is not null. This way we can retain the method signature as it was. In 9, we added some exemptions to this logic, e.g. `GetValueOrDefault`, or `Nullable<>.ToString`, which don't need the compensation. Problem was that we were matching the ToString method incorrectly, only looking at the method name, so we would also match non-nullable ToString(). Fix is to look at the declaring type of the ToString method as well to make sure it's nullable, otherwise apply the compensation. Fixes #35299 --- ...yExpressionTranslatingExpressionVisitor.cs | 6 +++-- .../Query/GearsOfWarQueryInMemoryTest.cs | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 61f3551e3f8..81803b46672 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -888,8 +888,10 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp if (methodCallExpression.Object != null && @object!.Type.IsNullableType() && methodCallExpression.Method.Name != nameof(Nullable.GetValueOrDefault) - && (!@object!.Type.IsNullableValueType() - || methodCallExpression.Method.Name != nameof(Nullable.ToString))) + && !(@object!.Type.IsNullableValueType() + && methodCallExpression.Method.Name == nameof(Nullable.ToString) + && methodCallExpression.Method.DeclaringType != null + && methodCallExpression.Method.DeclaringType.IsNullableType())) { var result = (Expression)methodCallExpression.Update( Expression.Convert(@object, methodCallExpression.Object.Type), diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs index 6ec5a0a431d..d528392a152 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs @@ -147,4 +147,28 @@ public override Task Join_include_conditional(bool async) // Right join not supported in InMemory public override Task Correlated_collections_on_RightJoin_with_predicate(bool async) => AssertTranslationFailed(() => base.Correlated_collections_on_RightJoin_with_predicate(async)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_ToString_on_non_nullable_property_of_an_optional_entity(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select(x => new + { + Id = x.Id, + SquadIdString = x.Gear.SquadId.ToString() + }), + ss => ss.Set().Select(x => new + { + Id = x.Id, + SquadIdString = x.Gear == null ? null : x.Gear.SquadId.ToString() + }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.SquadIdString, a.SquadIdString); + }); + } }