From 55f992d235ac27856d25b29afc0563317e6a86c4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 12 Jan 2025 11:10:43 +0100 Subject: [PATCH 1/2] Change ExecuteUpdate to accept Action instead of Func parameter Part of #32018 --- .../Internal/PrecompiledQueryCodeGenerator.cs | 6 ++--- .../Properties/TypeForwards.cs | 2 +- .../EntityFrameworkQueryableExtensions.cs | 24 ++++++++++++++----- ...yableMethodTranslatingExpressionVisitor.cs | 6 ++--- src/EFCore/Query/SetPropertyCalls`.cs | 12 +++++----- ...opertyCalls.cs => UpdateSettersBuilder.cs} | 6 ++--- .../TestUtilities/BulkUpdatesAsserter.cs | 2 +- .../BulkUpdates/BulkUpdatesTestBase.cs | 2 +- .../NonSharedModelBulkUpdatesTestBase.cs | 2 +- .../NorthwindBulkUpdatesTestBase.cs | 2 +- .../TestUtilities/BulkUpdatesAsserter.cs | 2 +- 11 files changed, 39 insertions(+), 27 deletions(-) rename src/EFCore/Query/{SetPropertyCalls.cs => UpdateSettersBuilder.cs} (92%) diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index af985334ee7..7a9977e8a3d 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -1163,7 +1163,7 @@ MethodCallExpression RewriteToSync(MethodInfo? syncMethod) // builder; returns the resulting NewArrayExpression representing all the setters. private static NewArrayExpression ProcessExecuteUpdate(MethodCallExpression executeUpdateCall) { - var setPropertyCalls = Activator.CreateInstance(); + var setPropertyCalls = Activator.CreateInstance(); var settersLambda = (LambdaExpression)executeUpdateCall.Arguments[1]; var settersParameter = settersLambda.Parameters.Single(); var expression = settersLambda.Body; @@ -1175,7 +1175,7 @@ private static NewArrayExpression ProcessExecuteUpdate(MethodCallExpression exec Method: { IsGenericMethod: true, - Name: nameof(SetPropertyCalls.SetProperty), + Name: nameof(UpdateSettersBuilder.SetProperty), DeclaringType.IsGenericType: true, }, Arguments: @@ -1184,7 +1184,7 @@ private static NewArrayExpression ProcessExecuteUpdate(MethodCallExpression exec Expression valueSelector ] } methodCallExpression - && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>)) + && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(UpdateSettersBuilder<>)) { if (valueSelector is UnaryExpression { diff --git a/src/EFCore.Relational/Properties/TypeForwards.cs b/src/EFCore.Relational/Properties/TypeForwards.cs index 8121602abd9..a677d2acbe6 100644 --- a/src/EFCore.Relational/Properties/TypeForwards.cs +++ b/src/EFCore.Relational/Properties/TypeForwards.cs @@ -6,4 +6,4 @@ [assembly: TypeForwardedTo(typeof(AttributeCodeFragment))] [assembly: TypeForwardedTo(typeof(MethodCallCodeFragment))] [assembly: TypeForwardedTo(typeof(NestedClosureCodeFragment))] -[assembly: TypeForwardedTo(typeof(SetPropertyCalls<>))] +[assembly: TypeForwardedTo(typeof(UpdateSettersBuilder<>))] diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 007272c8480..2cc97adc22b 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -3337,12 +3337,18 @@ internal static readonly MethodInfo ExecuteDeleteMethodInfo /// The total number of rows updated in the database. public static int ExecuteUpdate( this IQueryable source, - Func, SetPropertyCalls> setPropertyCalls) - => source.Provider.Execute( + Action> setPropertyCalls) + { + var setterBuilder = new UpdateSettersBuilder(); + setPropertyCalls(setterBuilder); + var setters = setterBuilder.BuildSettersExpression(); + + return source.Provider.Execute( Expression.Call( ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, - setPropertyCalls(new SetPropertyCalls()).BuildSettersExpression())); + setters)); + } /// /// Asynchronously updates database rows for the entity instances which match the LINQ query from the database. @@ -3366,16 +3372,22 @@ public static int ExecuteUpdate( [DynamicDependency("ExecuteUpdate``1(System.Linq.IQueryable{``1},System.Collections.Generic.IReadOnlyList{ITuple})", typeof(EntityFrameworkQueryableExtensions))] public static Task ExecuteUpdateAsync( this IQueryable source, - Func, SetPropertyCalls> setPropertyCalls, + Action> setPropertyCalls, CancellationToken cancellationToken = default) - => source.Provider is IAsyncQueryProvider provider + { + var setterBuilder = new UpdateSettersBuilder(); + setPropertyCalls(setterBuilder); + var setters = setterBuilder.BuildSettersExpression(); + + return source.Provider is IAsyncQueryProvider provider ? provider.ExecuteAsync>( Expression.Call( ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, - setPropertyCalls(new SetPropertyCalls()).BuildSettersExpression()), + setters), cancellationToken) : throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync); + } private static int ExecuteUpdate(this IQueryable source, [NotParameterized] IReadOnlyList setters) => throw new UnreachableException("Can't call this overload directly"); diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index 9f0019f2b97..fd94bc8c766 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -1078,7 +1078,7 @@ protected abstract ShapedQueryExpression TranslateSelect( /// /// Translates /// + /// cref="EntityFrameworkQueryableExtensions.ExecuteUpdate{TSource}(IQueryable{TSource}, Action{UpdateSettersBuilder{TSource}})" /> /// method /// over the given source. /// @@ -1086,7 +1086,7 @@ protected abstract ShapedQueryExpression TranslateSelect( /// /// The setters for this /// + /// cref="EntityFrameworkQueryableExtensions.ExecuteUpdate{TSource}(IQueryable{TSource}, Action{UpdateSettersBuilder{TSource}})" /> /// call. /// /// The non query after translation. @@ -1097,7 +1097,7 @@ protected abstract ShapedQueryExpression TranslateSelect( /// /// Represents a single setter in an - /// + /// /// call, i.e. a pair of property and value selectors. /// public sealed record ExecuteUpdateSetter(LambdaExpression PropertySelector, Expression ValueExpression); diff --git a/src/EFCore/Query/SetPropertyCalls`.cs b/src/EFCore/Query/SetPropertyCalls`.cs index b6ebf2b0f9d..67d4e8c01a0 100644 --- a/src/EFCore/Query/SetPropertyCalls`.cs +++ b/src/EFCore/Query/SetPropertyCalls`.cs @@ -4,7 +4,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// -public sealed class SetPropertyCalls : SetPropertyCalls +public sealed class UpdateSettersBuilder : UpdateSettersBuilder { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ public sealed class SetPropertyCalls : SetPropertyCalls /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public SetPropertyCalls() + public UpdateSettersBuilder() { } @@ -25,10 +25,10 @@ public SetPropertyCalls() /// A value expression. /// /// The same instance so that multiple calls to - /// + /// /// can be chained. /// - public SetPropertyCalls SetProperty( + public UpdateSettersBuilder SetProperty( Expression> propertyExpression, Expression> valueExpression) { @@ -44,9 +44,9 @@ public SetPropertyCalls SetProperty( /// A value expression. /// /// The same instance so that multiple calls to - /// can be chained. + /// can be chained. /// - public SetPropertyCalls SetProperty( + public UpdateSettersBuilder SetProperty( Expression> propertyExpression, TProperty valueExpression) { diff --git a/src/EFCore/Query/SetPropertyCalls.cs b/src/EFCore/Query/UpdateSettersBuilder.cs similarity index 92% rename from src/EFCore/Query/SetPropertyCalls.cs rename to src/EFCore/Query/UpdateSettersBuilder.cs index f2779d4147e..9e25147f8ce 100644 --- a/src/EFCore/Query/SetPropertyCalls.cs +++ b/src/EFCore/Query/UpdateSettersBuilder.cs @@ -19,7 +19,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// See Implementation of database providers and extensions /// and How EF Core queries work for more information and examples. /// -public class SetPropertyCalls +public class UpdateSettersBuilder { private readonly List _setters = new(); @@ -42,7 +42,7 @@ public virtual NewArrayExpression BuildSettersExpression() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public virtual SetPropertyCalls SetProperty(LambdaExpression propertyExpression, LambdaExpression valueExpression) + public virtual UpdateSettersBuilder SetProperty(LambdaExpression propertyExpression, LambdaExpression valueExpression) { _setters.Add( Expression.New( @@ -60,7 +60,7 @@ public virtual SetPropertyCalls SetProperty(LambdaExpression propertyExpression, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public virtual SetPropertyCalls SetProperty(LambdaExpression propertyExpression, Expression valueExpression) + public virtual UpdateSettersBuilder SetProperty(LambdaExpression propertyExpression, Expression valueExpression) { if (valueExpression.Type.IsValueType) { diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs index 978e36553d1..4c8ee94c8bf 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs @@ -34,7 +34,7 @@ public Task AssertUpdate( bool async, Func> query, Expression> entitySelector, - Func, SetPropertyCalls> setPropertyCalls, + Action> setPropertyCalls, int rowsAffectedCount, Action, IReadOnlyList> asserter) where TResult : class diff --git a/test/EFCore.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs b/test/EFCore.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs index 4115b8b2428..656768cf04f 100644 --- a/test/EFCore.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs @@ -33,7 +33,7 @@ public Task AssertUpdate( bool async, Func> query, Expression> entitySelector, - Func, SetPropertyCalls> setPropertyCalls, + Action> setPropertyCalls, int rowsAffectedCount, Action, IReadOnlyList> asserter = null) where TResult : class diff --git a/test/EFCore.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs b/test/EFCore.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs index f4f6705db7f..9929cc3dbf0 100644 --- a/test/EFCore.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs @@ -321,7 +321,7 @@ public Task AssertUpdate( bool async, Func contextCreator, Func> query, - Func, SetPropertyCalls> setPropertyCalls, + Action> setPropertyCalls, int rowsAffectedCount) where TResult : class where TContext : DbContext diff --git a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs index 12c4cb7aa9a..df6b56b1742 100644 --- a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -43,7 +43,7 @@ public virtual Task Update_without_property_to_set_throws(bool async) async, ss => ss.Set().Where(od => od.OrderID < 10250), e => e, - s => s, + _ => { }, rowsAffectedCount: 0); [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs b/test/EFCore.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs index 12b1610a8cd..7b94ca4ed3d 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs @@ -35,7 +35,7 @@ public Task AssertUpdate( bool async, Func> query, Expression> entitySelector, - Func, SetPropertyCalls> setPropertyCalls, + Action> setPropertyCalls, int rowsAffectedCount, Action, IReadOnlyList> asserter) where TResult : class From 691c08205d2bc6267cd8d6d2792e4496a87e063b Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 12 Jan 2025 14:28:45 +0100 Subject: [PATCH 2/2] Augment CSharpToLinqTranslator to support (basic) contextual type awareness --- .../Query/Internal/CSharpToLinqTranslator.cs | 63 +++++++++++++++++-- .../Internal/PrecompiledQueryCodeGenerator.cs | 25 ++++---- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs index 7701c0d13ff..6b79d8553e1 100644 --- a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs +++ b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs @@ -123,6 +123,37 @@ public virtual Expression Translate(SyntaxNode node, SemanticModel semanticModel public override Expression? Visit(SyntaxNode? node) => base.Visit(node); + /// + /// This method gets called when the expression context provides an expected CLR type. For example, in Foo(x), x gets visited + /// with an expected type based on Foo's parameter; this may determine how x gets translated, require a LINQ Convert node, or + /// similar. In contrast, in var y = x, there is no context providing an expected type, and the type of x simply + /// bubbles out. + /// + [return: NotNullIfNotNull("node")] + private Expression? Visit(SyntaxNode? node, Type? expectedType) + { + if (expectedType is null) + { + return Visit(node); + } + + var result = node switch + { + ArgumentSyntax s => VisitArgument(s, expectedType), + + // For lambdas, we generate a different node based on the expected type (e.g. an Action rather than a Func, even if + // the lambda body does return a T2). + SimpleLambdaExpressionSyntax s => VisitLambdaExpression(s, expectedType), + ParenthesizedLambdaExpressionSyntax s => VisitLambdaExpression(s, expectedType), + + _ => Visit(node), + }; + + // TODO: Insert necessary Convert nodes etc. when the expected and actual types differ + + return result; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -180,13 +211,16 @@ public override Expression VisitAnonymousObjectCreationExpression(AnonymousObjec /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Expression VisitArgument(ArgumentSyntax argument) + => VisitArgument(argument, expectedType: null); + + private Expression VisitArgument(ArgumentSyntax argument, Type? expectedType) { if (!argument.RefKindKeyword.IsKind(SyntaxKind.None)) { throw new InvalidOperationException($"Argument with ref/out: {argument}"); } - return Visit(argument.Expression); + return Visit(argument.Expression, expectedType); } /// @@ -682,7 +716,7 @@ parameter.DefaultValue is null && parameter.ParameterType.IsValueType // Positional argument if (argument.NameColon is null) { - destArguments[paramIndex] = Visit(argument); + destArguments[paramIndex] = Visit(argument, parameter.ParameterType); continue; } @@ -1009,7 +1043,7 @@ public override Expression VisitTypeOfExpression(TypeOfExpressionSyntax typeOf) public override Expression DefaultVisit(SyntaxNode node) => throw new NotSupportedException($"Unsupported syntax node of type '{node.GetType()}': {node}"); - private Expression VisitLambdaExpression(AnonymousFunctionExpressionSyntax lambda) + private Expression VisitLambdaExpression(AnonymousFunctionExpressionSyntax lambda, Type? expectedType = null) { if (lambda.ExpressionBody is null) { @@ -1055,7 +1089,28 @@ private Expression VisitLambdaExpression(AnonymousFunctionExpressionSyntax lambd try { var body = Visit(lambda.ExpressionBody); - return Lambda(body, translatedParameters); + + return expectedType switch + { + // If there's no contextual expected type, we allow the lambda's type to be inferred from its parameters and the body's + // return type. + null => Lambda(body, translatedParameters), + + // This is for the case where the expected type is Action, but the lambda body does return something, which needs to get + // ignored; for example, the ExecuteUpdateAsync setter parameter is Action, but the function is + // invoked with ExecuteUpdateAsync(s => s.SetProperty(...)), and SetProperty() returns UpdateSettersBuilder for further + // chaining. In this case, the body's return type is an UpdateSettersBuilder, meaning that the type of the constructed + // lambda here would be Func, and not Action as + // ExecuteUpdateAsync's signature requires. + // Identify this case, and explicitly type the returned lambda as an Action when necessary. + _ when expectedType.IsGenericType && expectedType.IsAssignableTo(typeof(MulticastDelegate)) + => Lambda(expectedType, body, translatedParameters), + + _ when expectedType.IsGenericType && expectedType.GetGenericTypeDefinition() == typeof(Expression<>) + => Lambda(expectedType.GetGenericArguments()[0], body, translatedParameters), + + _ => throw new UnreachableException() + }; } finally { diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index 7a9977e8a3d..9b60e2a1d3c 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -743,7 +743,7 @@ void ProcessCapturedVariables() ExpressionTreeFuncletizer.PathNode? evaluatableRootPaths; - // ExecuteUpdate requires really special handling: the function accepts a Func argument, but + // ExecuteUpdate requires really special handling: the function accepts a Func argument, but // we need to run funcletization on the setter lambdas added via that Func<>. if (operatorMethodCall.Method is { @@ -753,7 +753,7 @@ or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync), } && operatorMethodCall.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)) { - // First, statically convert the Func to a NewArrayExpression which represents all the + // First, statically convert the Action to a NewArrayExpression which represents all the // setters; since that's an expression, we can run the funcletizer on it. var settersExpression = ProcessExecuteUpdate(operatorMethodCall); evaluatableRootPaths = _funcletizer.CalculatePathsToEvaluatableRoots(settersExpression); @@ -767,8 +767,11 @@ or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync), // If there were captured variables, generate code to evaluate and build the same NewArrayExpression at runtime, // and then fall through to the normal logic, generating variable extractors against that NewArrayExpression // (local var) instead of against the method argument. - code.AppendLine( - $"var setters = {parameterName}(new SetPropertyCalls<{sourceElementTypeName}>()).BuildSettersExpression();"); + code.AppendLine($""" + var setterBuilder = new UpdateSettersBuilder<{sourceElementTypeName}>(); + {parameterName}(setterBuilder); + var setters = setterBuilder.BuildSettersExpression(); + """); parameterName = "setters"; parameterType = typeof(NewArrayExpression); } @@ -1114,7 +1117,7 @@ or nameof(EntityFrameworkQueryableExtensions.ToListAsync) => RewriteToSync( typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))), - // ExecuteUpdate is special; it accepts a non-expression-tree argument (Func), + // ExecuteUpdate is special; it accepts a non-expression-tree argument (Action), // evaluates it immediately, and injects a different MethodCall node into the expression tree with the resulting setter // expressions. // When statically analyzing ExecuteUpdate, we have to manually perform the same thing. @@ -1159,11 +1162,11 @@ MethodCallExpression RewriteToSync(MethodInfo? syncMethod) } } - // Accepts an expression tree representing a series of SetProperty() calls, parses them and passes them through the SetPropertyCalls - // builder; returns the resulting NewArrayExpression representing all the setters. + // Accepts an expression tree representing a series of SetProperty() calls, parses them and passes them through the + // UpdateSettersBuilder; returns the resulting NewArrayExpression representing all the setters. private static NewArrayExpression ProcessExecuteUpdate(MethodCallExpression executeUpdateCall) { - var setPropertyCalls = Activator.CreateInstance(); + var settersBuilder = new UpdateSettersBuilder(); var settersLambda = (LambdaExpression)executeUpdateCall.Arguments[1]; var settersParameter = settersLambda.Parameters.Single(); var expression = settersLambda.Body; @@ -1192,11 +1195,11 @@ Expression valueSelector Operand: LambdaExpression unwrappedValueSelector }) { - setPropertyCalls.SetProperty(propertySelector, unwrappedValueSelector); + settersBuilder.SetProperty(propertySelector, unwrappedValueSelector); } else { - setPropertyCalls.SetProperty(propertySelector, valueSelector); + settersBuilder.SetProperty(propertySelector, valueSelector); } expression = methodCallExpression.Object; @@ -1206,7 +1209,7 @@ Expression valueSelector throw new InvalidOperationException(RelationalStrings.InvalidArgumentToExecuteUpdate); } - return setPropertyCalls.BuildSettersExpression(); + return settersBuilder.BuildSettersExpression(); } /// diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index fd94bc8c766..fbb484d8715 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -181,7 +181,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var valueSelector = @new.Arguments[1]; // When the value selector is a bare value type (no lambda), a cast-to-object Convert node needs to be added - // for proper typing (see SetPropertyCalls); remove it here. + // for proper typing (see UpdateSettersBuilder); remove it here. if (valueSelector is UnaryExpression { NodeType: ExpressionType.Convert, Operand: var unwrappedValueSelector } && valueSelector.Type == typeof(object)) {