@@ -734,18 +734,52 @@ void ProcessCapturedVariables()
734734
735735 for ( var i = 1 ; i < parameters . Length ; i ++ )
736736 {
737- var parameter = parameters [ i ] ;
737+ var ( parameterName , parameterType ) = ( parameters [ i ] . Name ! , parameters [ i ] . ParameterType ) ;
738738
739- if ( parameter . ParameterType == typeof ( CancellationToken ) )
739+ if ( parameterType == typeof ( CancellationToken ) )
740740 {
741741 continue ;
742742 }
743743
744- if ( _funcletizer . CalculatePathsToEvaluatableRoots ( operatorMethodCall , i ) is not ExpressionTreeFuncletizer . PathNode
745- evaluatableRootPaths )
744+ ExpressionTreeFuncletizer . PathNode ? evaluatableRootPaths ;
745+
746+ // ExecuteUpdate requires really special handling: the function accepts a Func<SetPropertyCalls...> argument, but
747+ // we need to run funcletization on the setter lambdas added via that Func<>.
748+ if ( operatorMethodCall . Method is
749+ {
750+ Name : nameof ( EntityFrameworkQueryableExtensions . ExecuteUpdate )
751+ or nameof ( EntityFrameworkQueryableExtensions . ExecuteUpdateAsync ) ,
752+ IsGenericMethod : true
753+ }
754+ && operatorMethodCall . Method . DeclaringType == typeof ( EntityFrameworkQueryableExtensions ) )
746755 {
747- // There are no captured variables in this lambda argument - skip the argument
748- continue ;
756+ // First, statically convert the Func<SetPropertyCalls...> to a NewArrayExpression which represents all the
757+ // setters; since that's an expression, we can run the funcletizer on it.
758+ var settersExpression = ProcessExecuteUpdate ( operatorMethodCall ) ;
759+ evaluatableRootPaths = _funcletizer . CalculatePathsToEvaluatableRoots ( settersExpression ) ;
760+
761+ if ( evaluatableRootPaths is null )
762+ {
763+ // There are no captured variables in this lambda argument - skip the argument
764+ continue ;
765+ }
766+
767+ // If there were captured variables, generate code to evaluate and build the same NewArrayExpression at runtime,
768+ // and then fall through to the normal logic, generating variable extractors against that NewArrayExpression
769+ // (local var) instead of against the method argument.
770+ code . AppendLine (
771+ $ "var setters = { parameterName } (new SetPropertyCalls<{ sourceElementTypeName } >()).BuildSettersExpression();") ;
772+ parameterName = "setters" ;
773+ parameterType = typeof ( NewArrayExpression ) ;
774+ }
775+ else
776+ {
777+ evaluatableRootPaths = _funcletizer . CalculatePathsToEvaluatableRoots ( operatorMethodCall , i ) ;
778+ if ( evaluatableRootPaths is null )
779+ {
780+ // There are no captured variables in this lambda argument - skip the argument
781+ continue ;
782+ }
749783 }
750784
751785 // We have a lambda argument with captured variables. Use the information returned by the funcletizer to generate code
@@ -756,11 +790,11 @@ void ProcessCapturedVariables()
756790 declaredQueryContextVariable = true ;
757791 }
758792
759- if ( ! parameter . ParameterType . IsSubclassOf ( typeof ( Expression ) ) )
793+ if ( ! parameterType . IsSubclassOf ( typeof ( Expression ) ) )
760794 {
761795 // Special case: this is a non-lambda argument (Skip/Take/FromSql).
762796 // Simply add the argument directly as a parameter
763- code . AppendLine ( $ """ queryContext.AddParameter("{ evaluatableRootPaths . ParameterName } ", { parameter . Name } );""" ) ;
797+ code . AppendLine ( $ """ queryContext.AddParameter("{ evaluatableRootPaths . ParameterName } ", { parameterName } );""" ) ;
764798 continue ;
765799 }
766800
@@ -769,7 +803,7 @@ void ProcessCapturedVariables()
769803 // Lambda argument. Recurse through evaluatable path trees.
770804 foreach ( var child in evaluatableRootPaths . Children ! )
771805 {
772- GenerateCapturedVariableExtractors ( parameter . Name ! , parameter . ParameterType , child ) ;
806+ GenerateCapturedVariableExtractors ( parameterName , parameterType , child ) ;
773807
774808 void GenerateCapturedVariableExtractors (
775809 string currentIdentifier ,
@@ -786,12 +820,13 @@ void GenerateCapturedVariableExtractors(
786820
787821 var variableName = capturedVariablesPathTree . ExpressionType . Name ;
788822 variableName = char . ToLower ( variableName [ 0 ] ) + variableName [ 1 ..^ "Expression" . Length ] + ++ variableCounter ;
789- code . AppendLine (
790- $ "var { variableName } = ({ capturedVariablesPathTree . ExpressionType . Name } ){ roslynPathSegment } ;") ;
791823
792824 if ( capturedVariablesPathTree . Children ? . Count > 0 )
793825 {
794826 // This is an intermediate node which has captured variables in the children. Continue recursing down.
827+ code . AppendLine (
828+ $ "var { variableName } = ({ capturedVariablesPathTree . ExpressionType . Name } ){ roslynPathSegment } ;") ;
829+
795830 foreach ( var child in capturedVariablesPathTree . Children )
796831 {
797832 GenerateCapturedVariableExtractors ( variableName , capturedVariablesPathTree . ExpressionType , child ) ;
@@ -816,7 +851,7 @@ void GenerateCapturedVariableExtractors(
816851 {
817852 code
818853 . Append ( '"' ) . Append ( capturedVariablesPathTree . ParameterName ! ) . AppendLine ( "\" ," )
819- . AppendLine ( $ "Expression.Lambda<Func<object?>>(Expression.Convert({ variableName } , typeof(object)))")
854+ . AppendLine ( $ "Expression.Lambda<Func<object?>>(Expression.Convert({ roslynPathSegment } , typeof(object)))")
820855 . AppendLine ( ".Compile(preferInterpretation: true)" )
821856 . AppendLine ( ".Invoke());" ) ;
822857 }
@@ -1073,15 +1108,23 @@ or nameof(EntityFrameworkQueryableExtensions.ToListAsync)
10731108 QueryableMethods . GetSumWithSelector (
10741109 method . GetParameters ( ) [ 1 ] . ParameterType . GenericTypeArguments [ 0 ] . GenericTypeArguments [ 1 ] ) ) ,
10751110
1076- // ExecuteDelete/Update behave just like other scalar-returning operators
1111+ // ExecuteDelete behaves just like other scalar-returning operators
10771112 nameof ( EntityFrameworkQueryableExtensions . ExecuteDeleteAsync ) when method . DeclaringType
10781113 == typeof ( EntityFrameworkQueryableExtensions )
10791114 => RewriteToSync (
10801115 typeof ( EntityFrameworkQueryableExtensions ) . GetMethod ( nameof ( EntityFrameworkQueryableExtensions . ExecuteDelete ) ) ) ,
1081- nameof ( EntityFrameworkQueryableExtensions . ExecuteUpdateAsync ) when method . DeclaringType
1082- == typeof ( EntityFrameworkQueryableExtensions )
1083- => RewriteToSync (
1084- typeof ( EntityFrameworkQueryableExtensions ) . GetMethod ( nameof ( EntityFrameworkQueryableExtensions . ExecuteUpdate ) ) ) ,
1116+
1117+ // ExecuteUpdate is special; it accepts a non-expression-tree argument (Func<SetPropertyCalls, SetPropertyCalls>),
1118+ // evaluates it immediately, and injects a different MethodCall node into the expression tree with the resulting setter
1119+ // expressions.
1120+ // When statically analyzing ExecuteUpdate, we have to manually perform the same thing.
1121+ nameof ( EntityFrameworkQueryableExtensions . ExecuteUpdate ) or nameof ( EntityFrameworkQueryableExtensions . ExecuteUpdateAsync )
1122+ when method . DeclaringType == typeof ( EntityFrameworkQueryableExtensions )
1123+ => Expression . Call (
1124+ EntityFrameworkQueryableExtensions . ExecuteUpdateMethodInfo . MakeGenericMethod (
1125+ terminatingOperator . Arguments [ 0 ] . Type . GetSequenceType ( ) ) ,
1126+ penultimateOperator ,
1127+ ProcessExecuteUpdate ( terminatingOperator ) ) ,
10851128
10861129 // In the regular case (sync terminating operator which needs to stay in the query tree), simply compose the terminating
10871130 // operator over the penultimate and return that.
@@ -1116,6 +1159,56 @@ MethodCallExpression RewriteToSync(MethodInfo? syncMethod)
11161159 }
11171160 }
11181161
1162+ // Accepts an expression tree representing a series of SetProperty() calls, parses them and passes them through the SetPropertyCalls
1163+ // builder; returns the resulting NewArrayExpression representing all the setters.
1164+ private static NewArrayExpression ProcessExecuteUpdate ( MethodCallExpression executeUpdateCall )
1165+ {
1166+ var setPropertyCalls = Activator . CreateInstance < SetPropertyCalls > ( ) ;
1167+ var settersLambda = ( LambdaExpression ) executeUpdateCall . Arguments [ 1 ] ;
1168+ var settersParameter = settersLambda . Parameters . Single ( ) ;
1169+ var expression = settersLambda . Body ;
1170+
1171+ while ( expression != settersParameter )
1172+ {
1173+ if ( expression is MethodCallExpression
1174+ {
1175+ Method :
1176+ {
1177+ IsGenericMethod : true ,
1178+ Name : nameof ( SetPropertyCalls < int > . SetProperty ) ,
1179+ DeclaringType . IsGenericType : true ,
1180+ } ,
1181+ Arguments :
1182+ [
1183+ UnaryExpression { NodeType : ExpressionType . Quote , Operand : LambdaExpression propertySelector } ,
1184+ Expression valueSelector
1185+ ]
1186+ } methodCallExpression
1187+ && methodCallExpression . Method . DeclaringType . GetGenericTypeDefinition ( ) == typeof ( SetPropertyCalls < > ) )
1188+ {
1189+ if ( valueSelector is UnaryExpression
1190+ {
1191+ NodeType : ExpressionType . Quote ,
1192+ Operand : LambdaExpression unwrappedValueSelector
1193+ } )
1194+ {
1195+ setPropertyCalls . SetProperty ( propertySelector , unwrappedValueSelector ) ;
1196+ }
1197+ else
1198+ {
1199+ setPropertyCalls . SetProperty ( propertySelector , valueSelector ) ;
1200+ }
1201+
1202+ expression = methodCallExpression . Object ;
1203+ continue ;
1204+ }
1205+
1206+ throw new InvalidOperationException ( RelationalStrings . InvalidArgumentToExecuteUpdate ) ;
1207+ }
1208+
1209+ return setPropertyCalls . BuildSettersExpression ( ) ;
1210+ }
1211+
11191212 /// <summary>
11201213 /// Contains information on a failure to precompile a specific query in the user's source code.
11211214 /// Includes information about the query, its location, and the exception that occured.
0 commit comments