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 af985334ee7..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;
@@ -1175,7 +1178,7 @@ private static NewArrayExpression ProcessExecuteUpdate(MethodCallExpression exec
Method:
{
IsGenericMethod: true,
- Name: nameof(SetPropertyCalls.SetProperty),
+ Name: nameof(UpdateSettersBuilder.SetProperty),
DeclaringType.IsGenericType: true,
},
Arguments:
@@ -1184,7 +1187,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
{
@@ -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.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..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))
{
@@ -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