diff --git a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs index 7fef6d89de0..7701c0d13ff 100644 --- a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs +++ b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs @@ -1245,10 +1245,10 @@ private sealed class FakeFieldInfo( public bool IsNonNullableReferenceType { get; } = isNonNullableReferenceType; public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); + => []; public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); + => []; public override bool IsDefined(Type attributeType, bool inherit) => false; @@ -1289,10 +1289,10 @@ public override RuntimeFieldHandle FieldHandle private sealed class FakeConstructorInfo(Type type, ParameterInfo[] parameters) : ConstructorInfo { public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); + => []; public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); + => []; public override bool IsDefined(Type attributeType, bool inherit) => false; diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index de2957a65d7..af985334ee7 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -734,18 +734,52 @@ void ProcessCapturedVariables() for (var i = 1; i < parameters.Length; i++) { - var parameter = parameters[i]; + var (parameterName, parameterType) = (parameters[i].Name!, parameters[i].ParameterType); - if (parameter.ParameterType == typeof(CancellationToken)) + if (parameterType == typeof(CancellationToken)) { continue; } - if (_funcletizer.CalculatePathsToEvaluatableRoots(operatorMethodCall, i) is not ExpressionTreeFuncletizer.PathNode - evaluatableRootPaths) + ExpressionTreeFuncletizer.PathNode? evaluatableRootPaths; + + // 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 + { + Name: nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate) + or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync), + IsGenericMethod: true + } + && operatorMethodCall.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)) { - // There are no captured variables in this lambda argument - skip the argument - continue; + // First, statically convert the Func 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); + + if (evaluatableRootPaths is null) + { + // There are no captured variables in this lambda argument - skip the argument + continue; + } + + // 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();"); + parameterName = "setters"; + parameterType = typeof(NewArrayExpression); + } + else + { + evaluatableRootPaths = _funcletizer.CalculatePathsToEvaluatableRoots(operatorMethodCall, i); + if (evaluatableRootPaths is null) + { + // There are no captured variables in this lambda argument - skip the argument + continue; + } } // We have a lambda argument with captured variables. Use the information returned by the funcletizer to generate code @@ -756,11 +790,11 @@ void ProcessCapturedVariables() declaredQueryContextVariable = true; } - if (!parameter.ParameterType.IsSubclassOf(typeof(Expression))) + if (!parameterType.IsSubclassOf(typeof(Expression))) { // Special case: this is a non-lambda argument (Skip/Take/FromSql). // Simply add the argument directly as a parameter - code.AppendLine($"""queryContext.AddParameter("{evaluatableRootPaths.ParameterName}", {parameter.Name});"""); + code.AppendLine($"""queryContext.AddParameter("{evaluatableRootPaths.ParameterName}", {parameterName});"""); continue; } @@ -769,7 +803,7 @@ void ProcessCapturedVariables() // Lambda argument. Recurse through evaluatable path trees. foreach (var child in evaluatableRootPaths.Children!) { - GenerateCapturedVariableExtractors(parameter.Name!, parameter.ParameterType, child); + GenerateCapturedVariableExtractors(parameterName, parameterType, child); void GenerateCapturedVariableExtractors( string currentIdentifier, @@ -786,12 +820,13 @@ void GenerateCapturedVariableExtractors( var variableName = capturedVariablesPathTree.ExpressionType.Name; variableName = char.ToLower(variableName[0]) + variableName[1..^"Expression".Length] + ++variableCounter; - code.AppendLine( - $"var {variableName} = ({capturedVariablesPathTree.ExpressionType.Name}){roslynPathSegment};"); if (capturedVariablesPathTree.Children?.Count > 0) { // This is an intermediate node which has captured variables in the children. Continue recursing down. + code.AppendLine( + $"var {variableName} = ({capturedVariablesPathTree.ExpressionType.Name}){roslynPathSegment};"); + foreach (var child in capturedVariablesPathTree.Children) { GenerateCapturedVariableExtractors(variableName, capturedVariablesPathTree.ExpressionType, child); @@ -816,7 +851,7 @@ void GenerateCapturedVariableExtractors( { code .Append('"').Append(capturedVariablesPathTree.ParameterName!).AppendLine("\",") - .AppendLine($"Expression.Lambda>(Expression.Convert({variableName}, typeof(object)))") + .AppendLine($"Expression.Lambda>(Expression.Convert({roslynPathSegment}, typeof(object)))") .AppendLine(".Compile(preferInterpretation: true)") .AppendLine(".Invoke());"); } @@ -1073,15 +1108,23 @@ or nameof(EntityFrameworkQueryableExtensions.ToListAsync) QueryableMethods.GetSumWithSelector( method.GetParameters()[1].ParameterType.GenericTypeArguments[0].GenericTypeArguments[1])), - // ExecuteDelete/Update behave just like other scalar-returning operators + // ExecuteDelete behaves just like other scalar-returning operators nameof(EntityFrameworkQueryableExtensions.ExecuteDeleteAsync) when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) => RewriteToSync( typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))), - nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync) when method.DeclaringType - == typeof(EntityFrameworkQueryableExtensions) - => RewriteToSync( - typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate))), + + // ExecuteUpdate is special; it accepts a non-expression-tree argument (Func), + // 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. + nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate) or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync) + when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) + => Expression.Call( + EntityFrameworkQueryableExtensions.ExecuteUpdateMethodInfo.MakeGenericMethod( + terminatingOperator.Arguments[0].Type.GetSequenceType()), + penultimateOperator, + ProcessExecuteUpdate(terminatingOperator)), // In the regular case (sync terminating operator which needs to stay in the query tree), simply compose the terminating // operator over the penultimate and return that. @@ -1116,6 +1159,56 @@ 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. + private static NewArrayExpression ProcessExecuteUpdate(MethodCallExpression executeUpdateCall) + { + var setPropertyCalls = Activator.CreateInstance(); + var settersLambda = (LambdaExpression)executeUpdateCall.Arguments[1]; + var settersParameter = settersLambda.Parameters.Single(); + var expression = settersLambda.Body; + + while (expression != settersParameter) + { + if (expression is MethodCallExpression + { + Method: + { + IsGenericMethod: true, + Name: nameof(SetPropertyCalls.SetProperty), + DeclaringType.IsGenericType: true, + }, + Arguments: + [ + UnaryExpression { NodeType: ExpressionType.Quote, Operand: LambdaExpression propertySelector }, + Expression valueSelector + ] + } methodCallExpression + && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>)) + { + if (valueSelector is UnaryExpression + { + NodeType: ExpressionType.Quote, + Operand: LambdaExpression unwrappedValueSelector + }) + { + setPropertyCalls.SetProperty(propertySelector, unwrappedValueSelector); + } + else + { + setPropertyCalls.SetProperty(propertySelector, valueSelector); + } + + expression = methodCallExpression.Object; + continue; + } + + throw new InvalidOperationException(RelationalStrings.InvalidArgumentToExecuteUpdate); + } + + return setPropertyCalls.BuildSettersExpression(); + } + /// /// Contains information on a failure to precompile a specific query in the user's source code. /// Includes information about the query, its location, and the exception that occured. diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs index 3a1bf9e8a78..51e2693b62d 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs @@ -16,25 +16,22 @@ public partial class RelationalQueryableMethodTranslatingExpressionVisitor typeof(RelationalSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; /// - protected override UpdateExpression? TranslateExecuteUpdate(ShapedQueryExpression source, LambdaExpression setPropertyCalls) + protected override UpdateExpression? TranslateExecuteUpdate(ShapedQueryExpression source, IReadOnlyList setters) { + if (setters.Count == 0) + { + throw new UnreachableException("Empty setters list"); + } + // Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for // ExecuteUpdate's lambdas. Note that we don't currently support updates across tables. source = source.UpdateShaperExpression(new IncludePruner().Visit(source.ShaperExpression)); - var setters = new List<(LambdaExpression PropertySelector, Expression ValueExpression)>(); - PopulateSetPropertyCalls(setPropertyCalls.Body, setters, setPropertyCalls.Parameters[0]); if (TranslationErrorDetails != null) { return null; } - if (setters.Count == 0) - { - AddTranslationErrorDetails(RelationalStrings.NoSetPropertyInvocation); - return null; - } - // Translate the setters: the left (property) selectors get translated to ColumnExpressions, the right (value) selectors to // arbitrary SqlExpressions. // Note that if the query isn't natively supported, we'll do a pushdown (see PushdownWithPkInnerJoinPredicate below); if that @@ -67,42 +64,9 @@ public partial class RelationalQueryableMethodTranslatingExpressionVisitor return PushdownWithPkInnerJoinPredicate(); - void PopulateSetPropertyCalls( - Expression expression, - List<(LambdaExpression, Expression)> list, - ParameterExpression parameter) - { - switch (expression) - { - case ParameterExpression p - when parameter == p: - break; - - case MethodCallExpression - { - Method: - { - IsGenericMethod: true, - Name: nameof(SetPropertyCalls.SetProperty), - DeclaringType.IsGenericType: true - } - } methodCallExpression - when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>): - list.Add(((LambdaExpression)methodCallExpression.Arguments[0], methodCallExpression.Arguments[1])); - - PopulateSetPropertyCalls(methodCallExpression.Object!, list, parameter); - - break; - - default: - AddTranslationErrorDetails(RelationalStrings.InvalidArgumentToExecuteUpdate); - break; - } - } - bool TranslateSetters( ShapedQueryExpression source, - List<(LambdaExpression PropertySelector, Expression ValueExpression)> setters, + IReadOnlyList setters, [NotNullWhen(true)] out List? translatedSetters, [NotNullWhen(true)] out TableExpressionBase? targetTable) { @@ -464,7 +428,7 @@ SqlParameterExpression parameter var inner = source; var outerParameter = Expression.Parameter(entityType.ClrType); var outerKeySelector = Expression.Lambda(outerParameter.CreateKeyValuesExpression(pk.Properties), outerParameter); - var firstPropertyLambdaExpression = setters[0].Item1; + var firstPropertyLambdaExpression = setters[0].PropertySelector; var entitySource = GetEntitySource(RelationalDependencies.Model, firstPropertyLambdaExpression.Body); var innerKeySelector = Expression.Lambda( entitySource.CreateKeyValuesExpression(pk.Properties), firstPropertyLambdaExpression.Parameters); @@ -481,6 +445,7 @@ SqlParameterExpression parameter var propertyReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Outer"); var valueReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Inner"); + var rewrittenSetters = new ExecuteUpdateSetter[setters.Count]; for (var i = 0; i < setters.Count; i++) { var (propertyExpression, valueExpression) = setters[i]; @@ -499,14 +464,14 @@ SqlParameterExpression parameter transparentIdentifierParameter) : valueExpression; - setters[i] = (propertyExpression, valueExpression); + rewrittenSetters[i] = new(propertyExpression, valueExpression); } tableExpression = (TableExpression)outerSelectExpression.Tables[0]; // Re-translate the property selectors to get column expressions pointing to the new outer select expression (the original one // has been pushed down into a subquery). - if (!TranslateSetters(outer, setters, out var translatedSetters, out _)) + if (!TranslateSetters(outer, rewrittenSetters, out var translatedSetters, out _)) { return null; } diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index a05a69773f8..d4924f562f8 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -1316,7 +1316,12 @@ protected virtual SqlExpression VisitSqlParameter( bool allowOptimizedExpansion, out bool nullable) { - var parameterValue = ParameterValues[sqlParameterExpression.Name]; + if (!ParameterValues.TryGetValue(sqlParameterExpression.Name, out var parameterValue)) + { + throw new UnreachableException( + $"Encountered SqlParameter with name '{sqlParameterExpression.Name}', but such a parameter does not exist."); + } + nullable = parameterValue == null; if (nullable) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 088c5193dcf..007272c8480 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -3337,9 +3337,12 @@ internal static readonly MethodInfo ExecuteDeleteMethodInfo /// The total number of rows updated in the database. public static int ExecuteUpdate( this IQueryable source, - Expression, SetPropertyCalls>> setPropertyCalls) + Func, SetPropertyCalls> setPropertyCalls) => source.Provider.Execute( - Expression.Call(ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyCalls)); + Expression.Call( + ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), + source.Expression, + setPropertyCalls(new SetPropertyCalls()).BuildSettersExpression())); /// /// Asynchronously updates database rows for the entity instances which match the LINQ query from the database. @@ -3360,18 +3363,35 @@ public static int ExecuteUpdate( /// A collection of set property statements specifying properties to update. /// A to observe while waiting for the task to complete. /// The total number of rows updated in the database. + [DynamicDependency("ExecuteUpdate``1(System.Linq.IQueryable{``1},System.Collections.Generic.IReadOnlyList{ITuple})", typeof(EntityFrameworkQueryableExtensions))] public static Task ExecuteUpdateAsync( this IQueryable source, - Expression, SetPropertyCalls>> setPropertyCalls, + Func, SetPropertyCalls> setPropertyCalls, CancellationToken cancellationToken = default) => source.Provider is IAsyncQueryProvider provider ? provider.ExecuteAsync>( Expression.Call( - ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyCalls), cancellationToken) + ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), + source.Expression, + setPropertyCalls(new SetPropertyCalls()).BuildSettersExpression()), + cancellationToken) : throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync); - internal static readonly MethodInfo ExecuteUpdateMethodInfo - = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteUpdate))!; + private static int ExecuteUpdate(this IQueryable source, [NotParameterized] IReadOnlyList setters) + => throw new UnreachableException("Can't call this overload directly"); + + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public static readonly MethodInfo ExecuteUpdateMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .Single( + m => m.Name == nameof(ExecuteUpdate) && m.GetParameters()[1].ParameterType == typeof(IReadOnlyList)); #endregion } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 1aa731b3728..3c2c0a631d4 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1155,7 +1155,7 @@ public static string ErrorMaterializingPropertyInvalidCast(object? entityType, o entityType, property, expectedType, actualType); /// - /// The methods '{methodName}' and '{asyncMethodName}' are not supported by the current database provider. Please contact the publisher of the database provider for more information. + /// The methods '{methodName}' and '{asyncMethodName}' are not supported by the current database provider. Please contact the publisher of the database provider for more information. /// public static string ExecuteQueriesNotSupported(object? methodName, object? asyncMethodName) => string.Format( @@ -2241,6 +2241,12 @@ public static string NoProviderConfiguredFailedToResolveService(object? service) GetString("NoProviderConfiguredFailedToResolveService", nameof(service)), service); + /// + /// An 'ExecuteUpdate' call must specify at least one 'SetProperty' invocation, to indicate the properties to be updated. + /// + public static string NoSetPropertyInvocation + => GetString("NoSetPropertyInvocation"); + /// /// The property '{1_entityType}.{0_property}' does not have a setter. Either make the property writable or use a different '{propertyAccessMode}'. /// @@ -2949,12 +2955,6 @@ public static string ServiceProviderConfigRemoved(object? key) public static string SetOperationWithDifferentIncludesInOperands => GetString("SetOperationWithDifferentIncludesInOperands"); - /// - /// The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method. - /// - public static string SetPropertyMethodInvoked - => GetString("SetPropertyMethodInvoked"); - /// /// The shared-type entity type '{entityType}' cannot have a base type. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 4849feeeac3..c010b57c389 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1301,6 +1301,9 @@ Unable to resolve service for type '{service}'. This is often because no database provider has been configured for this DbContext. A provider can be configured by overriding the 'DbContext.OnConfiguring' method or by using 'AddDbContext' on the application service provider. If 'AddDbContext' is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext. + + An 'ExecuteUpdate' call must specify at least one 'SetProperty' invocation, to indicate the properties to be updated. + The property '{1_entityType}.{0_property}' does not have a setter. Either make the property writable or use a different '{propertyAccessMode}'. @@ -1580,9 +1583,6 @@ Unable to translate set operation since both operands have different 'Include' operations. Consider having same 'Include' applied on both sides. - - The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method. - The shared-type entity type '{entityType}' cannot have a base type. diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index 664b9496532..93a923670a2 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -157,7 +157,41 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp case nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate) when genericMethod == EntityFrameworkQueryableExtensions.ExecuteUpdateMethodInfo: - return TranslateExecuteUpdate(shapedQueryExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()) + NewArrayExpression newArray; + switch (methodCallExpression.Arguments[1]) + { + case NewArrayExpression n: + newArray = n; + break; + + case ConstantExpression { Value: Array { Length: 0 } }: + throw new InvalidOperationException( + CoreStrings.NonQueryTranslationFailedWithDetails( + methodCallExpression.Print(), CoreStrings.NoSetPropertyInvocation)); + + default: + throw new UnreachableException("ExecuteUpdate with incorrect setters"); + } + + var setters = new ExecuteUpdateSetter[newArray.Expressions.Count]; + for (var i = 0; i < setters.Length; i++) + { + var @new = (NewExpression)newArray.Expressions[i]; + var propertySelector = (LambdaExpression)@new.Arguments[0]; + 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. + if (valueSelector is UnaryExpression { NodeType: ExpressionType.Convert, Operand: var unwrappedValueSelector } + && valueSelector.Type == typeof(object)) + { + valueSelector = unwrappedValueSelector; + } + + setters[i] = new(propertySelector, valueSelector); + } + + return TranslateExecuteUpdate(shapedQueryExpression, setters) ?? throw new InvalidOperationException( CoreStrings.NonQueryTranslationFailedWithDetails( methodCallExpression.Print(), TranslationErrorDetails)); @@ -1044,22 +1078,29 @@ protected abstract ShapedQueryExpression TranslateSelect( /// /// Translates /// + /// cref="EntityFrameworkQueryableExtensions.ExecuteUpdate{TSource}(IQueryable{TSource}, Func{SetPropertyCalls{TSource}, SetPropertyCalls{TSource}})" /> /// method /// over the given source. /// /// The shaped query on which the operator is applied. - /// - /// The lambda expression containing + /// + /// The setters for this /// - /// statements. + /// cref="EntityFrameworkQueryableExtensions.ExecuteUpdate{TSource}(IQueryable{TSource}, Func{SetPropertyCalls{TSource}, SetPropertyCalls{TSource}})" /> + /// call. /// /// The non query after translation. - protected virtual Expression? TranslateExecuteUpdate(ShapedQueryExpression source, LambdaExpression setPropertyCalls) + protected virtual Expression? TranslateExecuteUpdate(ShapedQueryExpression source, IReadOnlyList setters) => throw new InvalidOperationException( CoreStrings.ExecuteQueriesNotSupported( nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate), nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync))); + /// + /// Represents a single setter in an + /// + /// call, i.e. a pair of property and value selectors. + /// + public record ExecuteUpdateSetter(LambdaExpression PropertySelector, Expression ValueExpression); + #endregion ExecuteUpdate/ExecuteDelete } diff --git a/src/EFCore/Query/SetPropertyCalls.cs b/src/EFCore/Query/SetPropertyCalls.cs index d525aa8184f..3aa824148c3 100644 --- a/src/EFCore/Query/SetPropertyCalls.cs +++ b/src/EFCore/Query/SetPropertyCalls.cs @@ -1,28 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Microsoft.EntityFrameworkCore.Query; -/// -/// -/// Supports specifying property and value to be set in ExecuteUpdate method with chaining multiple calls for updating -/// multiple columns. -/// -/// -/// This type does not have any constructor or implementation since it is used inside LINQ query solely for the purpose of -/// creating expression tree. -/// -/// -/// -/// See Implementation of database providers and extensions -/// and How EF Core queries work for more information and examples. -/// -/// The type of source element on which ExecuteUpdate operation is being applied. -public sealed class SetPropertyCalls +/// +public sealed class SetPropertyCalls : SetPropertyCalls { - private SetPropertyCalls() + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public SetPropertyCalls() { } @@ -34,13 +27,16 @@ private SetPropertyCalls() /// A value expression. /// /// The same instance so that multiple calls to - /// + /// /// can be chained. /// public SetPropertyCalls SetProperty( - Func propertyExpression, - Func valueExpression) - => throw new InvalidOperationException(CoreStrings.SetPropertyMethodInvoked); + Expression> propertyExpression, + Expression> valueExpression) + { + SetProperty((LambdaExpression)propertyExpression, valueExpression); + return this; + } /// /// Specifies a property and corresponding value it should be updated to in ExecuteUpdate method. @@ -50,41 +46,87 @@ public SetPropertyCalls SetProperty( /// A value expression. /// /// The same instance so that multiple calls to - /// can be chained. + /// can be chained. /// public SetPropertyCalls SetProperty( - Func propertyExpression, + Expression> propertyExpression, TProperty valueExpression) - => throw new InvalidOperationException(CoreStrings.SetPropertyMethodInvoked); + { + // We pass in the value as a ConstantExpression; but it will get parameterized by the funcletizer as any method argument that isn't + // inside a lambda (just like parameters to Skip/Take). + SetProperty(propertyExpression, Expression.Constant(valueExpression, typeof(TProperty))); + return this; + } +} - #region Hidden System.Object members +/// +/// +/// Supports specifying property and value to be set in ExecuteUpdate method with chaining multiple calls for updating +/// multiple columns. +/// +/// +/// This type does not have any constructor or implementation since it is used inside LINQ query solely for the purpose of +/// creating expression tree. +/// +/// +/// +/// See Implementation of database providers and extensions +/// and How EF Core queries work for more information and examples. +/// +public class SetPropertyCalls +{ + private readonly List _setters = new(); + + private static ConstructorInfo? _setterTupleConstructor; /// - /// Returns a string that represents the current object. + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// A string that represents the current object. - [EditorBrowsable(EditorBrowsableState.Never)] - public override string? ToString() - => base.ToString(); + [EntityFrameworkInternal] + public NewArrayExpression BuildSettersExpression() + => Expression.NewArrayInit(typeof(ITuple), _setters); /// - /// Determines whether the specified object is equal to the current object. + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// The object to compare with the current object. - /// if the specified object is equal to the current object; otherwise, . - [EditorBrowsable(EditorBrowsableState.Never)] - // ReSharper disable once BaseObjectEqualsIsObjectEquals - public override bool Equals(object? obj) - => base.Equals(obj); + [EntityFrameworkInternal] + public SetPropertyCalls SetProperty(LambdaExpression propertyExpression, LambdaExpression valueExpression) + { + _setters.Add( + Expression.New( + _setterTupleConstructor ??= typeof(Tuple).GetConstructor([typeof(Delegate), typeof(object)])!, + propertyExpression, + valueExpression)); + + return this; + } /// - /// Serves as the default hash function. + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// A hash code for the current object. - [EditorBrowsable(EditorBrowsableState.Never)] - // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode - public override int GetHashCode() - => base.GetHashCode(); + [EntityFrameworkInternal] + public SetPropertyCalls SetProperty(LambdaExpression propertyExpression, Expression valueExpression) + { + if (valueExpression.Type.IsValueType) + { + valueExpression = Expression.Convert(valueExpression, typeof(object)); + } - #endregion + _setters.Add( + Expression.New( + _setterTupleConstructor ??= typeof(Tuple).GetConstructor([typeof(Delegate), typeof(object)])!, + propertyExpression, + valueExpression)); + + return this; + } } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs index e5188e9a4a7..9c4bd1cfad1 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs @@ -61,11 +61,6 @@ public override Task Update_without_property_to_set_throws(bool async) RelationalStrings.NoSetPropertyInvocation, () => base.Update_without_property_to_set_throws(async)); - public override Task Update_with_invalid_lambda_throws(bool async) - => AssertTranslationFailed( - RelationalStrings.InvalidArgumentToExecuteUpdate, - () => base.Update_with_invalid_lambda_throws(async)); - public override Task Update_with_invalid_lambda_in_set_property_throws(bool async) => AssertTranslationFailed( RelationalStrings.InvalidPropertyInSetProperty( @@ -74,7 +69,7 @@ public override Task Update_with_invalid_lambda_in_set_property_throws(bool asyn public override Task Update_multiple_tables_throws(bool async) => AssertTranslationFailed( - RelationalStrings.MultipleTablesInExecuteUpdate("c => c.Customer.ContactName", "c => c.e.OrderDate"), + RelationalStrings.MultipleTablesInExecuteUpdate("c => c.e.OrderDate", "c => c.Customer.ContactName"), () => base.Update_multiple_tables_throws(async)); public override Task Update_unmapped_property_throws(bool async) diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs index 0d54fee9c1e..3fbb93703b5 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs @@ -35,7 +35,7 @@ public override Task Delete_GroupBy_Where_Select_First_3(bool async) public override Task Update_base_and_derived_types(bool async) => AssertTranslationFailed( - RelationalStrings.MultipleTablesInExecuteUpdate("e => e.Name", "e => e.FoundOn"), + RelationalStrings.MultipleTablesInExecuteUpdate("e => e.FoundOn", "e => e.Name"), () => base.Update_base_and_derived_types(async)); // Keyless entities are mapped as TPH only diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs index f6a8c22ad69..72cf1ca20b5 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs @@ -49,7 +49,7 @@ public override Task Delete_where_using_hierarchy_derived(bool async) public override Task Update_base_and_derived_types(bool async) => AssertTranslationFailed( - RelationalStrings.MultipleTablesInExecuteUpdate("e => e.Name", "e => e.FoundOn"), + RelationalStrings.MultipleTablesInExecuteUpdate("e => e.FoundOn", "e => e.Name"), () => base.Update_base_and_derived_types(async)); // Keyless entities are mapped as TPH only diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs index 939018fc4de..59903587aa0 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/PrecompiledQueryRelationalTestBase.cs @@ -727,7 +727,7 @@ public virtual Task Terminating_ExecuteDeleteAsync() """); [ConditionalFact] - public virtual Task Terminating_ExecuteUpdate() + public virtual Task Terminating_ExecuteUpdate_with_lambda() => Test( """ await context.Database.BeginTransactionAsync(); @@ -739,7 +739,19 @@ public virtual Task Terminating_ExecuteUpdate() """); [ConditionalFact] - public virtual Task Terminating_ExecuteUpdateAsync() + public virtual Task Terminating_ExecuteUpdate_without_lambda() + => Test( + """ +await context.Database.BeginTransactionAsync(); + +var newValue = "NewValue"; +var rowsAffected = context.Blogs.Where(b => b.Id > 8).ExecuteUpdate(setters => setters.SetProperty(b => b.Name, newValue)); +Assert.Equal(1, rowsAffected); +Assert.Equal(1, await context.Blogs.CountAsync(b => b.Id == 9 && b.Name == "NewValue")); +"""); + + [ConditionalFact] + public virtual Task Terminating_ExecuteUpdateAsync_with_lambda() => Test( """ await context.Database.BeginTransactionAsync(); @@ -748,6 +760,18 @@ public virtual Task Terminating_ExecuteUpdateAsync() var rowsAffected = await context.Blogs.Where(b => b.Id > 8).ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Name, b => b.Name + suffix)); Assert.Equal(1, rowsAffected); Assert.Equal(1, await context.Blogs.CountAsync(b => b.Id == 9 && b.Name == "Blog2Suffix")); +"""); + + [ConditionalFact] + public virtual Task Terminating_ExecuteUpdateAsync_without_lambda() + => Test( + """ +await context.Database.BeginTransactionAsync(); + +var newValue = "NewValue"; +var rowsAffected = await context.Blogs.Where(b => b.Id > 8).ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Name, newValue)); +Assert.Equal(1, rowsAffected); +Assert.Equal(1, await context.Blogs.CountAsync(b => b.Id == 9 && b.Name == "NewValue")); """); #endregion Reducing terminating operators diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs index 98da8ff8bcf..978e36553d1 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, - Expression, SetPropertyCalls>> setPropertyCalls, + Func, SetPropertyCalls> 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 8b53b43414a..4115b8b2428 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, - Expression, SetPropertyCalls>> setPropertyCalls, + Func, SetPropertyCalls> 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 21099591a5f..f4f6705db7f 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, - Expression, SetPropertyCalls>> setPropertyCalls, + Func, SetPropertyCalls> 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 50750b0913b..12c4cb7aa9a 100644 --- a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -46,16 +46,6 @@ public virtual Task Update_without_property_to_set_throws(bool async) s => s, rowsAffectedCount: 0); - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Update_with_invalid_lambda_throws(bool async) - => AssertUpdate( - async, - ss => ss.Set().Where(od => od.OrderID < 10250), - e => e, - s => s.Maybe(e => e), - rowsAffectedCount: 0); - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Update_with_invalid_lambda_in_set_property_throws(bool async) @@ -400,6 +390,17 @@ public virtual Task Update_Where_set_constant(bool async) rowsAffectedCount: 8, (b, a) => Assert.All(a, c => Assert.Equal("Updated", c.ContactName))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_Where_set_constant_via_lambda(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, _ => "Updated"), + rowsAffectedCount: 8, + (b, a) => Assert.All(a, c => Assert.Equal("Updated", c.ContactName))); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Update_Where_parameter_set_constant(bool async) diff --git a/test/EFCore.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs b/test/EFCore.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs index c03cecd9125..12b1610a8cd 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, - Expression, SetPropertyCalls>> setPropertyCalls, + Func, SetPropertyCalls> setPropertyCalls, int rowsAffectedCount, Action, IReadOnlyList> asserter) where TResult : class diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs index da82e0c9754..092181773f7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs @@ -35,8 +35,10 @@ public override async Task Update_property_inside_complex_type(bool async) AssertExecuteUpdateSql( """ +@p='12345' + UPDATE [c] -SET [c].[ShippingAddress_ZipCode] = 12345 +SET [c].[ShippingAddress_ZipCode] = @p FROM [Customer] AS [c] WHERE [c].[ShippingAddress_ZipCode] = 7728 """); @@ -48,8 +50,10 @@ public override async Task Update_property_inside_nested_complex_type(bool async AssertExecuteUpdateSql( """ +@p='United States Modified' (Size = 4000) + UPDATE [c] -SET [c].[ShippingAddress_Country_FullName] = N'United States Modified' +SET [c].[ShippingAddress_Country_FullName] = @p FROM [Customer] AS [c] WHERE [c].[ShippingAddress_Country_Code] = N'US' """); @@ -61,10 +65,12 @@ public override async Task Update_multiple_properties_inside_multiple_complex_ty AssertExecuteUpdateSql( """ +@p='54321' + UPDATE [c] -SET [c].[BillingAddress_ZipCode] = 54321, +SET [c].[Name] = [c].[Name] + N'Modified', [c].[ShippingAddress_ZipCode] = [c].[BillingAddress_ZipCode], - [c].[Name] = [c].[Name] + N'Modified' + [c].[BillingAddress_ZipCode] = @p FROM [Customer] AS [c] WHERE [c].[ShippingAddress_ZipCode] = 7728 """); @@ -76,8 +82,10 @@ public override async Task Update_projected_complex_type(bool async) AssertExecuteUpdateSql( """ +@p='12345' + UPDATE [c] -SET [c].[ShippingAddress_ZipCode] = 12345 +SET [c].[ShippingAddress_ZipCode] = @p FROM [Customer] AS [c] """); } @@ -88,9 +96,11 @@ public override async Task Update_multiple_projected_complex_types_via_anonymous AssertExecuteUpdateSql( """ +@p='54321' + UPDATE [c] -SET [c].[BillingAddress_ZipCode] = 54321, - [c].[ShippingAddress_ZipCode] = [c].[BillingAddress_ZipCode] +SET [c].[ShippingAddress_ZipCode] = [c].[BillingAddress_ZipCode], + [c].[BillingAddress_ZipCode] = @p FROM [Customer] AS [c] """); } @@ -108,20 +118,20 @@ public override async Task Update_complex_type_to_parameter(bool async) AssertExecuteUpdateSql( """ -@complex_type_newAddress_AddressLine1='New AddressLine1' (Size = 4000) -@complex_type_newAddress_AddressLine2='New AddressLine2' (Size = 4000) -@complex_type_newAddress_Tags='["new_tag1","new_tag2"]' (Size = 4000) -@complex_type_newAddress_ZipCode='99999' (Nullable = true) -@complex_type_newAddress_Code='FR' (Size = 4000) -@complex_type_newAddress_FullName='France' (Size = 4000) +@complex_type_p_AddressLine1='New AddressLine1' (Size = 4000) +@complex_type_p_AddressLine2='New AddressLine2' (Size = 4000) +@complex_type_p_Tags='["new_tag1","new_tag2"]' (Size = 4000) +@complex_type_p_ZipCode='99999' (Nullable = true) +@complex_type_p_Code='FR' (Size = 4000) +@complex_type_p_FullName='France' (Size = 4000) UPDATE [c] -SET [c].[ShippingAddress_AddressLine1] = @complex_type_newAddress_AddressLine1, - [c].[ShippingAddress_AddressLine2] = @complex_type_newAddress_AddressLine2, - [c].[ShippingAddress_Tags] = @complex_type_newAddress_Tags, - [c].[ShippingAddress_ZipCode] = @complex_type_newAddress_ZipCode, - [c].[ShippingAddress_Country_Code] = @complex_type_newAddress_Code, - [c].[ShippingAddress_Country_FullName] = @complex_type_newAddress_FullName +SET [c].[ShippingAddress_AddressLine1] = @complex_type_p_AddressLine1, + [c].[ShippingAddress_AddressLine2] = @complex_type_p_AddressLine2, + [c].[ShippingAddress_Tags] = @complex_type_p_Tags, + [c].[ShippingAddress_ZipCode] = @complex_type_p_ZipCode, + [c].[ShippingAddress_Country_Code] = @complex_type_p_Code, + [c].[ShippingAddress_Country_FullName] = @complex_type_p_FullName FROM [Customer] AS [c] """); } @@ -132,12 +142,12 @@ public override async Task Update_nested_complex_type_to_parameter(bool async) AssertExecuteUpdateSql( """ -@complex_type_newCountry_Code='FR' (Size = 4000) -@complex_type_newCountry_FullName='France' (Size = 4000) +@complex_type_p_Code='FR' (Size = 4000) +@complex_type_p_FullName='France' (Size = 4000) UPDATE [c] -SET [c].[ShippingAddress_Country_Code] = @complex_type_newCountry_Code, - [c].[ShippingAddress_Country_FullName] = @complex_type_newCountry_FullName +SET [c].[ShippingAddress_Country_Code] = @complex_type_p_Code, + [c].[ShippingAddress_Country_FullName] = @complex_type_p_FullName FROM [Customer] AS [c] """); } @@ -165,13 +175,20 @@ public override async Task Update_complex_type_to_inline_without_lambda(bool asy AssertExecuteUpdateSql( """ +@complex_type_p_AddressLine1='New AddressLine1' (Size = 4000) +@complex_type_p_AddressLine2='New AddressLine2' (Size = 4000) +@complex_type_p_Tags='["new_tag1","new_tag2"]' (Size = 4000) +@complex_type_p_ZipCode='99999' (Nullable = true) +@complex_type_p_Code='FR' (Size = 4000) +@complex_type_p_FullName='France' (Size = 4000) + UPDATE [c] -SET [c].[ShippingAddress_AddressLine1] = N'New AddressLine1', - [c].[ShippingAddress_AddressLine2] = N'New AddressLine2', - [c].[ShippingAddress_Tags] = N'["new_tag1","new_tag2"]', - [c].[ShippingAddress_ZipCode] = 99999, - [c].[ShippingAddress_Country_Code] = N'FR', - [c].[ShippingAddress_Country_FullName] = N'France' +SET [c].[ShippingAddress_AddressLine1] = @complex_type_p_AddressLine1, + [c].[ShippingAddress_AddressLine2] = @complex_type_p_AddressLine2, + [c].[ShippingAddress_Tags] = @complex_type_p_Tags, + [c].[ShippingAddress_ZipCode] = @complex_type_p_ZipCode, + [c].[ShippingAddress_Country_Code] = @complex_type_p_Code, + [c].[ShippingAddress_Country_FullName] = @complex_type_p_FullName FROM [Customer] AS [c] """); } @@ -224,8 +241,10 @@ public override async Task Update_collection_inside_complex_type(bool async) AssertExecuteUpdateSql( """ +@p='["new_tag1","new_tag2"]' (Size = 4000) + UPDATE [c] -SET [c].[ShippingAddress_Tags] = N'["new_tag1","new_tag2"]' +SET [c].[ShippingAddress_Tags] = @p FROM [Customer] AS [c] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs index 8dcd3cb44c2..aee74180982 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs @@ -68,8 +68,10 @@ public override async Task Replace_ColumnExpression_in_column_setter(bool async) AssertSql( """ +@p='SomeValue' (Size = 4000) + UPDATE [o0] -SET [o0].[Value] = N'SomeValue' +SET [o0].[Value] = @p FROM [Owner] AS [o] INNER JOIN [OwnedCollection] AS [o0] ON [o].[Id] = [o0].[OwnerId] """); @@ -81,8 +83,10 @@ public override async Task Update_non_owned_property_on_entity_with_owned(bool a AssertSql( """ +@p='SomeValue' (Size = 4000) + UPDATE [o] -SET [o].[Title] = N'SomeValue' +SET [o].[Title] = @p FROM [Owner] AS [o] """); } @@ -105,8 +109,10 @@ public override async Task Update_non_owned_property_on_entity_with_owned_in_joi AssertSql( """ +@p='NewValue' (Size = 4000) + UPDATE [o] -SET [o].[Title] = N'NewValue' +SET [o].[Title] = @p FROM [Owner] AS [o] INNER JOIN [Owner] AS [o0] ON [o].[Id] = [o0].[Id] """); @@ -119,8 +125,8 @@ public override async Task Update_owned_and_non_owned_properties_with_table_shar AssertSql( """ UPDATE [o] -SET [o].[OwnedReference_Number] = CAST(LEN([o].[Title]) AS int), - [o].[Title] = COALESCE(CONVERT(varchar(11), [o].[OwnedReference_Number]), '') +SET [o].[Title] = COALESCE(CONVERT(varchar(11), [o].[OwnedReference_Number]), ''), + [o].[OwnedReference_Number] = CAST(LEN([o].[Title]) AS int) FROM [Owner] AS [o] """); } @@ -144,8 +150,8 @@ public override async Task Update_non_main_table_in_entity_with_entity_splitting AssertSql( """ UPDATE [b0] -SET [b0].[Rating] = CAST(LEN([b0].[Title]) AS int), - [b0].[Title] = CONVERT(varchar(11), [b0].[Rating]) +SET [b0].[Title] = CONVERT(varchar(11), [b0].[Rating]), + [b0].[Rating] = CAST(LEN([b0].[Title]) AS int) FROM [Blogs] AS [b] INNER JOIN [BlogsPart1] AS [b0] ON [b].[Id] = [b0].[Id] """); @@ -209,8 +215,10 @@ public override async Task Update_with_view_mapping(bool async) AssertSql( """ +@p='Updated' (Size = 4000) + UPDATE [b] -SET [b].[Data] = N'Updated' +SET [b].[Data] = @p FROM [Blogs] AS [b] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs index b4458f05782..35aba244116 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs @@ -611,10 +611,12 @@ public override async Task Update_Where_set_constant_TagWith(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + -- MyUpdate UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -624,6 +626,21 @@ public override async Task Update_Where_set_constant(bool async) { await base.Update_Where_set_constant(async); + AssertExecuteUpdateSql( + """ +@p='Updated' (Size = 30) + +UPDATE [c] +SET [c].[ContactName] = @p +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%' +"""); + } + + public override async Task Update_Where_set_constant_via_lambda(bool async) + { + await base.Update_Where_set_constant_via_lambda(async); + AssertExecuteUpdateSql( """ UPDATE [c] @@ -639,10 +656,11 @@ public override async Task Update_Where_parameter_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) @customer='ALFKI' (Size = 5) (DbType = StringFixedLength) UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] = @customer """, @@ -662,8 +680,10 @@ FROM [Customers] AS [c] """, // """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE 0 = 1 """); @@ -675,10 +695,10 @@ public override async Task Update_Where_set_parameter(bool async) AssertExecuteUpdateSql( """ -@value='Abc' (Size = 30) +@p='Abc' (Size = 30) UPDATE [c] -SET [c].[ContactName] = @value +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -705,8 +725,10 @@ public override async Task Update_Where_set_parameter_from_inline_list(bool asyn AssertExecuteUpdateSql( """ +@p='Abc' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Abc' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -718,10 +740,10 @@ public override async Task Update_Where_set_parameter_from_multilevel_property_a AssertExecuteUpdateSql( """ -@container_Containee_Property='Abc' (Size = 30) +@p='Abc' (Size = 30) UPDATE [c] -SET [c].[ContactName] = @container_Containee_Property +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -733,10 +755,11 @@ public override async Task Update_Where_Skip_set_constant(bool async) AssertExecuteUpdateSql( """ +@p0='Updated' (Size = 30) @p='4' UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p0 FROM [Customers] AS [c0] INNER JOIN ( SELECT [c].[CustomerID] @@ -755,9 +778,10 @@ public override async Task Update_Where_Take_set_constant(bool async) AssertExecuteUpdateSql( """ @p='4' +@p0='Updated' (Size = 30) UPDATE TOP(@p) [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p0 FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -769,11 +793,12 @@ public override async Task Update_Where_Skip_Take_set_constant(bool async) AssertExecuteUpdateSql( """ +@p1='Updated' (Size = 30) @p='2' @p0='4' UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p1 FROM [Customers] AS [c0] INNER JOIN ( SELECT [c].[CustomerID] @@ -791,8 +816,10 @@ public override async Task Update_Where_OrderBy_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p FROM [Customers] AS [c0] INNER JOIN ( SELECT [c].[CustomerID] @@ -808,10 +835,11 @@ public override async Task Update_Where_OrderBy_Skip_set_constant(bool async) AssertExecuteUpdateSql( """ +@p0='Updated' (Size = 30) @p='4' UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p0 FROM [Customers] AS [c0] INNER JOIN ( SELECT [c].[CustomerID] @@ -829,10 +857,11 @@ public override async Task Update_Where_OrderBy_Take_set_constant(bool async) AssertExecuteUpdateSql( """ +@p0='Updated' (Size = 30) @p='4' UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p0 FROM [Customers] AS [c0] INNER JOIN ( SELECT TOP(@p) [c].[CustomerID] @@ -849,11 +878,12 @@ public override async Task Update_Where_OrderBy_Skip_Take_set_constant(bool asyn AssertExecuteUpdateSql( """ +@p1='Updated' (Size = 30) @p='2' @p0='4' UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p1 FROM [Customers] AS [c0] INNER JOIN ( SELECT [c].[CustomerID] @@ -871,11 +901,12 @@ public override async Task Update_Where_OrderBy_Skip_Take_Skip_Take_set_constant AssertExecuteUpdateSql( """ +@p3='Updated' (Size = 30) @p='2' @p0='6' UPDATE [c1] -SET [c1].[ContactName] = N'Updated' +SET [c1].[ContactName] = @p3 FROM [Customers] AS [c1] INNER JOIN ( SELECT [c0].[CustomerID] @@ -898,8 +929,10 @@ public override async Task Update_Where_GroupBy_aggregate_set_constant(bool asyn AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] = ( SELECT TOP(1) [o].[CustomerID] @@ -915,8 +948,10 @@ public override async Task Update_Where_GroupBy_First_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] = ( SELECT TOP(1) ( @@ -942,8 +977,10 @@ public override async Task Update_Where_GroupBy_First_set_constant_3(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] IN ( SELECT ( @@ -964,8 +1001,10 @@ public override async Task Update_Where_Distinct_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c0] -SET [c0].[ContactName] = N'Updated' +SET [c0].[ContactName] = @p FROM [Customers] AS [c0] INNER JOIN ( SELECT DISTINCT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] @@ -995,8 +1034,10 @@ public override async Task Update_Where_using_navigation_2_set_constant(bool asy AssertExecuteUpdateSql( """ +@p='1' + UPDATE [o] -SET [o].[Quantity] = CAST(1 AS smallint) +SET [o].[Quantity] = CAST(@p AS smallint) FROM [Order Details] AS [o] INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] @@ -1065,8 +1106,10 @@ public override async Task Update_Where_set_constant_using_ef_property(bool asyn AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -1092,13 +1135,6 @@ public override async Task Update_without_property_to_set_throws(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_with_invalid_lambda_throws(bool async) - { - await base.Update_with_invalid_lambda_throws(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_Where_multiple_set(bool async) { await base.Update_Where_multiple_set(async); @@ -1106,10 +1142,11 @@ public override async Task Update_Where_multiple_set(bool async) AssertExecuteUpdateSql( """ @value='Abc' (Size = 30) +@p='Seattle' (Size = 15) UPDATE [c] -SET [c].[City] = N'Seattle', - [c].[ContactName] = @value +SET [c].[ContactName] = @value, + [c].[City] = @p FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%' """); @@ -1142,8 +1179,10 @@ public override async Task Update_Union_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c1] -SET [c1].[ContactName] = N'Updated' +SET [c1].[ContactName] = @p FROM [Customers] AS [c1] INNER JOIN ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] @@ -1163,8 +1202,10 @@ public override async Task Update_Concat_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c1] -SET [c1].[ContactName] = N'Updated' +SET [c1].[ContactName] = @p FROM [Customers] AS [c1] INNER JOIN ( SELECT [c].[CustomerID] @@ -1184,8 +1225,10 @@ public override async Task Update_Except_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c1] -SET [c1].[ContactName] = N'Updated' +SET [c1].[ContactName] = @p FROM [Customers] AS [c1] INNER JOIN ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] @@ -1205,8 +1248,10 @@ public override async Task Update_Intersect_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c1] -SET [c1].[ContactName] = N'Updated' +SET [c1].[ContactName] = @p FROM [Customers] AS [c1] INNER JOIN ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] @@ -1226,8 +1271,10 @@ public override async Task Update_with_join_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] INNER JOIN ( SELECT [o].[CustomerID] @@ -1244,8 +1291,10 @@ public override async Task Update_with_left_join_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] LEFT JOIN ( SELECT [o].[CustomerID] @@ -1262,8 +1311,10 @@ public override async Task Update_with_cross_join_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] CROSS JOIN ( SELECT 1 AS empty @@ -1280,8 +1331,10 @@ public override async Task Update_with_cross_apply_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] CROSS APPLY ( SELECT 1 AS empty @@ -1298,8 +1351,10 @@ public override async Task Update_with_outer_apply_set_constant(bool async) AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] OUTER APPLY ( SELECT 1 AS empty @@ -1316,8 +1371,10 @@ public override async Task Update_with_cross_join_left_join_set_constant(bool as AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] CROSS JOIN ( SELECT 1 AS empty @@ -1339,8 +1396,10 @@ public override async Task Update_with_cross_join_cross_apply_set_constant(bool AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] CROSS JOIN ( SELECT 1 AS empty @@ -1362,8 +1421,10 @@ public override async Task Update_with_cross_join_outer_apply_set_constant(bool AssertExecuteUpdateSql( """ +@p='Updated' (Size = 30) + UPDATE [c] -SET [c].[ContactName] = N'Updated' +SET [c].[ContactName] = @p FROM [Customers] AS [c] CROSS JOIN ( SELECT 1 AS empty @@ -1468,8 +1529,10 @@ public override async Task Update_with_two_inner_joins(bool async) AssertExecuteUpdateSql( """ +@p='1' + UPDATE [o] -SET [o].[Quantity] = CAST(1 AS smallint) +SET [o].[Quantity] = CAST(@p AS smallint) FROM [Order Details] AS [o] INNER JOIN [Products] AS [p] ON [o].[ProductID] = [p].[ProductID] INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs index 1de45df484e..9e5d4e77d50 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -134,8 +134,10 @@ public override async Task Update_base_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='SomeOtherKiwi' (Size = 4000) + UPDATE [k] -SET [k].[Name] = N'SomeOtherKiwi' +SET [k].[Name] = @p FROM [Kiwi] AS [k] WHERE [k].[CountryId] = 1 """); @@ -147,8 +149,10 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='0' (Size = 1) + UPDATE [k] -SET [k].[FoundOn] = CAST(0 AS tinyint) +SET [k].[FoundOn] = @p FROM [Kiwi] AS [k] WHERE [k].[CountryId] = 1 """); @@ -160,8 +164,10 @@ public override async Task Update_where_using_hierarchy(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -182,9 +188,12 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ +@p='Kiwi' (Size = 4000) +@p0='0' (Size = 1) + UPDATE [k] -SET [k].[FoundOn] = CAST(0 AS tinyint), - [k].[Name] = N'Kiwi' +SET [k].[Name] = @p, + [k].[FoundOn] = @p0 FROM [Kiwi] AS [k] WHERE [k].[CountryId] = 1 """); @@ -196,8 +205,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs index e711d658681..ab3fc6e598c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs @@ -134,8 +134,10 @@ public override async Task Update_base_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='SomeOtherKiwi' (Size = 4000) + UPDATE [k] -SET [k].[Name] = N'SomeOtherKiwi' +SET [k].[Name] = @p FROM [Kiwi] AS [k] """); } @@ -146,8 +148,10 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='0' (Size = 1) + UPDATE [k] -SET [k].[FoundOn] = CAST(0 AS tinyint) +SET [k].[FoundOn] = @p FROM [Kiwi] AS [k] """); } @@ -158,8 +162,10 @@ public override async Task Update_where_using_hierarchy(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -180,9 +186,12 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ +@p='Kiwi' (Size = 4000) +@p0='0' (Size = 1) + UPDATE [k] -SET [k].[FoundOn] = CAST(0 AS tinyint), - [k].[Name] = N'Kiwi' +SET [k].[Name] = @p, + [k].[FoundOn] = @p0 FROM [Kiwi] AS [k] """); } @@ -193,8 +202,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -212,8 +223,10 @@ public override async Task Update_with_interface_in_property_expression(bool asy AssertExecuteUpdateSql( """ +@p='0' + UPDATE [c] -SET [c].[SugarGrams] = 0 +SET [c].[SugarGrams] = @p FROM [Coke] AS [c] """); } @@ -224,8 +237,10 @@ public override async Task Update_with_interface_in_EF_Property_in_property_expr AssertExecuteUpdateSql( """ +@p='0' + UPDATE [c] -SET [c].[SugarGrams] = 0 +SET [c].[SugarGrams] = @p FROM [Coke] AS [c] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSqlServerTest.cs index 433f5daed28..f1e39c5630f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -138,8 +138,10 @@ public override async Task Update_base_type(bool async) AssertExecuteUpdateSql( """ +@p='Animal' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'Animal' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[CountryId] = 1 AND [a].[Name] = N'Great spotted kiwi' """); @@ -151,8 +153,10 @@ public override async Task Update_base_type_with_OfType(bool async) AssertExecuteUpdateSql( """ +@p='NewBird' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'NewBird' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[CountryId] = 1 AND [a].[Discriminator] = N'Kiwi' """); @@ -171,8 +175,10 @@ public override async Task Update_base_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='SomeOtherKiwi' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'SomeOtherKiwi' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 """); @@ -184,8 +190,10 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='0' (Size = 1) + UPDATE [a] -SET [a].[FoundOn] = CAST(0 AS tinyint) +SET [a].[FoundOn] = @p FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 """); @@ -197,9 +205,12 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ +@p='Kiwi' (Size = 4000) +@p0='0' (Size = 1) + UPDATE [a] -SET [a].[FoundOn] = CAST(0 AS tinyint), - [a].[Name] = N'Kiwi' +SET [a].[Name] = @p, + [a].[FoundOn] = @p0 FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 """); @@ -211,8 +222,10 @@ public override async Task Update_where_using_hierarchy(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -227,8 +240,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSqlServerTest.cs index 6439493a5e2..b16a4916650 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesSqlServerTest.cs @@ -136,8 +136,10 @@ public override async Task Update_base_type(bool async) AssertExecuteUpdateSql( """ +@p='Animal' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'Animal' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[Name] = N'Great spotted kiwi' """); @@ -149,8 +151,10 @@ public override async Task Update_base_type_with_OfType(bool async) AssertExecuteUpdateSql( """ +@p='NewBird' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'NewBird' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' """); @@ -169,8 +173,10 @@ public override async Task Update_base_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='SomeOtherKiwi' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'SomeOtherKiwi' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' """); @@ -182,8 +188,10 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='0' (Size = 1) + UPDATE [a] -SET [a].[FoundOn] = CAST(0 AS tinyint) +SET [a].[FoundOn] = @p FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' """); @@ -195,8 +203,10 @@ public override async Task Update_where_using_hierarchy(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -211,9 +221,12 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ +@p='Kiwi' (Size = 4000) +@p0='0' (Size = 1) + UPDATE [a] -SET [a].[FoundOn] = CAST(0 AS tinyint), - [a].[Name] = N'Kiwi' +SET [a].[Name] = @p, + [a].[FoundOn] = @p0 FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Kiwi' """); @@ -225,8 +238,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -248,8 +263,10 @@ public override async Task Update_with_interface_in_property_expression(bool asy AssertExecuteUpdateSql( """ +@p='0' + UPDATE [d] -SET [d].[SugarGrams] = 0 +SET [d].[SugarGrams] = @p FROM [Drinks] AS [d] WHERE [d].[Discriminator] = 1 """); @@ -261,8 +278,10 @@ public override async Task Update_with_interface_in_EF_Property_in_property_expr AssertExecuteUpdateSql( """ +@p='0' + UPDATE [d] -SET [d].[SugarGrams] = 0 +SET [d].[SugarGrams] = @p FROM [Drinks] AS [d] WHERE [d].[Discriminator] = 1 """); diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs index eab0f89e764..c65435df320 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -100,8 +100,10 @@ public override async Task Update_base_type(bool async) AssertExecuteUpdateSql( """ +@p='Animal' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'Animal' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[CountryId] = 1 AND [a].[Name] = N'Great spotted kiwi' """); @@ -113,8 +115,10 @@ public override async Task Update_base_type_with_OfType(bool async) AssertExecuteUpdateSql( """ +@p='NewBird' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'NewBird' +SET [a].[Name] = @p FROM [Animals] AS [a] LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] WHERE [a].[CountryId] = 1 AND [k].[Id] IS NOT NULL @@ -134,8 +138,10 @@ public override async Task Update_base_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='SomeOtherKiwi' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'SomeOtherKiwi' +SET [a].[Name] = @p FROM [Animals] AS [a] INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] @@ -149,8 +155,10 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='0' (Size = 1) + UPDATE [k] -SET [k].[FoundOn] = CAST(0 AS tinyint) +SET [k].[FoundOn] = @p FROM [Animals] AS [a] INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] @@ -171,8 +179,10 @@ public override async Task Update_where_using_hierarchy(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -187,8 +197,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs index 42168ca6a4f..1234e6a5238 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs @@ -84,8 +84,10 @@ public override async Task Update_base_type(bool async) AssertExecuteUpdateSql( """ +@p='Animal' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'Animal' +SET [a].[Name] = @p FROM [Animals] AS [a] WHERE [a].[Name] = N'Great spotted kiwi' """); @@ -97,8 +99,10 @@ public override async Task Update_base_type_with_OfType(bool async) AssertExecuteUpdateSql( """ +@p='NewBird' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'NewBird' +SET [a].[Name] = @p FROM [Animals] AS [a] LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] WHERE [k].[Id] IS NOT NULL @@ -118,8 +122,10 @@ public override async Task Update_base_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='SomeOtherKiwi' (Size = 4000) + UPDATE [a] -SET [a].[Name] = N'SomeOtherKiwi' +SET [a].[Name] = @p FROM [Animals] AS [a] INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] @@ -132,8 +138,10 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ +@p='0' (Size = 1) + UPDATE [k] -SET [k].[FoundOn] = CAST(0 AS tinyint) +SET [k].[FoundOn] = @p FROM [Animals] AS [a] INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] @@ -153,8 +161,10 @@ public override async Task Update_where_using_hierarchy(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -169,8 +179,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) AssertExecuteUpdateSql( """ +@p='Monovia' (Size = 4000) + UPDATE [c] -SET [c].[Name] = N'Monovia' +SET [c].[Name] = @p FROM [Countries] AS [c] WHERE ( SELECT COUNT(*) @@ -193,8 +205,10 @@ public override async Task Update_with_interface_in_property_expression(bool asy AssertExecuteUpdateSql( """ +@p='0' + UPDATE [c] -SET [c].[SugarGrams] = 0 +SET [c].[SugarGrams] = @p FROM [Drinks] AS [d] INNER JOIN [Coke] AS [c] ON [d].[Id] = [c].[Id] """); @@ -206,8 +220,10 @@ public override async Task Update_with_interface_in_EF_Property_in_property_expr AssertExecuteUpdateSql( """ +@p='0' + UPDATE [c] -SET [c].[SugarGrams] = 0 +SET [c].[SugarGrams] = @p FROM [Drinks] AS [d] INNER JOIN [Coke] AS [c] ON [d].[Id] = [c].[Id] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledQuerySqlServerTest.cs index eb165b381de..6d8d29018b2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrecompiledQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public class PrecompiledQuerySqlServerTest( IClassFixture { protected override bool AlwaysPrintGeneratedSources - => true; + => false; #region Expression types @@ -1372,9 +1372,9 @@ FROM [Blogs] AS [b] """); } - public override async Task Terminating_ExecuteUpdate() + public override async Task Terminating_ExecuteUpdate_with_lambda() { - await base.Terminating_ExecuteUpdate(); + await base.Terminating_ExecuteUpdate_with_lambda(); AssertSql( """ @@ -1393,9 +1393,30 @@ FROM [Blogs] AS [b] """); } - public override async Task Terminating_ExecuteUpdateAsync() + public override async Task Terminating_ExecuteUpdate_without_lambda() { - await base.Terminating_ExecuteUpdateAsync(); + await base.Terminating_ExecuteUpdate_without_lambda(); + + AssertSql( + """ +@newValue='NewValue' (Size = 4000) + +UPDATE [b] +SET [b].[Name] = @newValue +FROM [Blogs] AS [b] +WHERE [b].[Id] > 8 +""", + // + """ +SELECT COUNT(*) +FROM [Blogs] AS [b] +WHERE [b].[Id] = 9 AND [b].[Name] = N'NewValue' +"""); + } + + public override async Task Terminating_ExecuteUpdateAsync_with_lambda() + { + await base.Terminating_ExecuteUpdateAsync_with_lambda(); AssertSql( """ @@ -1414,6 +1435,27 @@ FROM [Blogs] AS [b] """); } + public override async Task Terminating_ExecuteUpdateAsync_without_lambda() + { + await base.Terminating_ExecuteUpdateAsync_without_lambda(); + + AssertSql( + """ +@newValue='NewValue' (Size = 4000) + +UPDATE [b] +SET [b].[Name] = @newValue +FROM [Blogs] AS [b] +WHERE [b].[Id] > 8 +""", + // + """ +SELECT COUNT(*) +FROM [Blogs] AS [b] +WHERE [b].[Id] = 9 AND [b].[Name] = N'NewValue' +"""); + } + #endregion Reducing terminating operators #region SQL expression quotability diff --git a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs index 0a1684fda67..3f21d64b816 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs @@ -189,8 +189,10 @@ public override async Task ExecuteUpdate_works_for_table_sharing(bool async) AssertSql( """ +@p='1' + UPDATE [v] -SET [v].[SeatingCapacity] = 1 +SET [v].[SeatingCapacity] = @p FROM [Vehicles] AS [v] """, // diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs index 25e9edd40c3..f2797d35ebb 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs @@ -610,6 +610,18 @@ public override async Task Update_Where_set_constant(bool async) """); } + public override async Task Update_Where_set_constant_via_lambda(bool async) + { + await base.Update_Where_set_constant_via_lambda(async); + + AssertExecuteUpdateSql( + """ +UPDATE "Customers" AS "c" +SET "ContactName" = 'Updated' +WHERE "c"."CustomerID" LIKE 'F%' +"""); + } + public override async Task Update_Where_parameter_set_constant(bool async) { await base.Update_Where_parameter_set_constant(async); @@ -1065,13 +1077,6 @@ public override async Task Update_without_property_to_set_throws(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_with_invalid_lambda_throws(bool async) - { - await base.Update_with_invalid_lambda_throws(async); - - AssertExecuteUpdateSql(); - } - public override async Task Update_Where_multiple_set(bool async) { await base.Update_Where_multiple_set(async);