From fd4bc81c0213f75884bb77a86b814fff5bb4ba7e Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 18 Aug 2022 18:09:34 -0700 Subject: [PATCH] ExecuteUpdate: Implement support for DEFAULT Resolves #28660 --- ...lationalSqlTranslatingExpressionVisitor.cs | 6 +++++ .../Properties/SqliteStrings.Designer.cs | 8 +++++- .../Properties/SqliteStrings.resx | 5 +++- .../SqliteSqlTranslatingExpressionVisitor.cs | 19 +++++++++++++ src/EFCore/EF.cs | 17 ++++++++++++ .../Infrastructure/MethodInfoExtensions.cs | 27 ++++++++++++++----- src/EFCore/Properties/CoreStrings.Designer.cs | 6 +++++ src/EFCore/Properties/CoreStrings.resx | 3 +++ .../Query/EvaluatableExpressionFilter.cs | 3 ++- .../NorthwindBulkUpdatesTestBase.cs | 11 ++++++++ .../NorthwindBulkUpdatesSqlServerTest.cs | 11 ++++++++ .../NorthwindBulkUpdatesSqliteTest.cs | 7 +++++ 12 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 9e79413f4f0..a7e46feef10 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -735,6 +735,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } + // EF.Default + if (methodCallExpression.Method.IsEFDefaultMethod()) + { + return new SqlFragmentExpression("DEFAULT"); + } + var method = methodCallExpression.Method; var arguments = methodCallExpression.Arguments; EnumerableExpression? enumerableExpression = null; diff --git a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs index 14e2b35fd63..82e837b30f6 100644 --- a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs +++ b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs @@ -37,6 +37,12 @@ public static string AggregateOperationNotSupported(object? aggregateOperator, o public static string ApplyNotSupported => GetString("ApplyNotSupported"); + /// + /// Translating this operation requires the 'DEFAULT', which is not supported on SQLite. + /// + public static string DefaultNotSupported + => GetString("DefaultNotSupported"); + /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different SRIDs. /// @@ -74,7 +80,7 @@ public static string SequencesNotSupported => GetString("SequencesNotSupported"); /// - /// SQLite does not support stored procedures. See http://go.microsoft.com/fwlink/?LinkId=723262 for more information and examples. + /// SQLite does not support stored procedures, but one has been configured on entity type '{entityType}'. See http://go.microsoft.com/fwlink/?LinkId=723262 for more information and examples. /// public static string StoredProceduresNotSupported(object? entityType) => string.Format( diff --git a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx index 89d12f3924a..f18928093f5 100644 --- a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx +++ b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx @@ -123,6 +123,9 @@ Translating this query requires the SQL APPLY operation, which is not supported on SQLite. + + Translating this operation requires the 'DEFAULT', which is not supported on SQLite. + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different SRIDs. @@ -201,4 +204,4 @@ SQLite does not support stored procedures, but one has been configured on entity type '{entityType}'. See http://go.microsoft.com/fwlink/?LinkId=723262 for more information and examples. - + \ No newline at end of file diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs index 665b25159d5..4977e4875f7 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Sqlite.Internal; namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; @@ -211,6 +212,24 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) return visitedExpression; } + /// + /// 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. + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + // EF.Default + if (methodCallExpression.Method.IsEFDefaultMethod()) + { + AddTranslationErrorDetails(SqliteStrings.DefaultNotSupported); + return QueryCompilationContext.NotTranslatedExpression; + } + + return base.VisitMethodCall(methodCallExpression); + } + private static Type? GetProviderType(SqlExpression? expression) => expression == null ? null diff --git a/src/EFCore/EF.cs b/src/EFCore/EF.cs index 57b650f9251..6129242cd82 100644 --- a/src/EFCore/EF.cs +++ b/src/EFCore/EF.cs @@ -17,6 +17,9 @@ public static partial class EF internal static readonly MethodInfo PropertyMethod = typeof(EF).GetTypeInfo().GetDeclaredMethod(nameof(Property))!; + internal static readonly MethodInfo DefaultMethod + = typeof(EF).GetTypeInfo().GetDeclaredMethod(nameof(Default))!; + /// /// This flag is set to when code is being run from a design-time tool, such /// as "dotnet ef" or one of the Package Manager Console PowerShell commands "Add-Migration", "Update-Database", etc. @@ -55,6 +58,20 @@ public static TProperty Property( [NotParameterized] string propertyName) => throw new InvalidOperationException(CoreStrings.PropertyMethodInvoked); + /// + /// Used set a property to its default value within or + /// . + /// + /// + /// Depending on how the property is configured, this may be , or another value defined via + /// or similar. + /// + /// The type of the property being set. + /// The default value of the property. + public static T Default() + // TODO: Update exception message + => throw new InvalidOperationException(CoreStrings.DefaultMethodInvoked); + /// /// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. /// Calling these methods in other contexts (e.g. LINQ to Objects) will throw a . diff --git a/src/EFCore/Infrastructure/MethodInfoExtensions.cs b/src/EFCore/Infrastructure/MethodInfoExtensions.cs index b19bd5da776..644342577a9 100644 --- a/src/EFCore/Infrastructure/MethodInfoExtensions.cs +++ b/src/EFCore/Infrastructure/MethodInfoExtensions.cs @@ -25,11 +25,24 @@ public static class MethodInfoExtensions /// /// The method. /// if the method is ; otherwise. - public static bool IsEFPropertyMethod(this MethodInfo? methodInfo) - => Equals(methodInfo, EF.PropertyMethod) - // fallback to string comparison because MethodInfo.Equals is not - // always true in .NET Native even if methods are the same - || methodInfo?.IsGenericMethod == true - && methodInfo.Name == nameof(EF.Property) - && methodInfo.DeclaringType?.FullName == EFTypeName; + public static bool IsEFPropertyMethod(this MethodInfo methodInfo) + => methodInfo.IsGenericMethod + && (Equals(methodInfo.GetGenericMethodDefinition(), EF.PropertyMethod) + // fallback to string comparison because MethodInfo.Equals is not + // always true in .NET Native even if methods are the same + || (methodInfo.Name == nameof(EF.Property) + && methodInfo.DeclaringType?.FullName == EFTypeName)); + + /// + /// Returns if the given method is . + /// + /// The method. + /// if the method is ; otherwise. + public static bool IsEFDefaultMethod(this MethodInfo methodInfo) + => methodInfo.IsGenericMethod + && (Equals(methodInfo.GetGenericMethodDefinition(), EF.DefaultMethod) + // fallback to string comparison because MethodInfo.Equals is not + // always true in .NET Native even if methods are the same + || (methodInfo.Name == nameof(EF.DefaultMethod) + && methodInfo.DeclaringType?.FullName == EFTypeName)); } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 27f767ea761..de9d3009197 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -598,6 +598,12 @@ public static string DebugViewQueryStringError(object? message) GetString("DebugViewQueryStringError", nameof(message)), message); + /// + /// The EF.Default<T> property may only be used within Entity Framework ExecuteUpdate method. + /// + public static string DefaultMethodInvoked + => GetString("DefaultMethodInvoked"); + /// /// The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 0ed95bdb7cf..32a8f8bbfab 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -336,6 +336,9 @@ Error creating query string: {message}. + + The EF.Default<T> property may only be used within Entity Framework ExecuteUpdate method. + The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. diff --git a/src/EFCore/Query/EvaluatableExpressionFilter.cs b/src/EFCore/Query/EvaluatableExpressionFilter.cs index d3a2d0d9bf8..1dbb572dfc3 100644 --- a/src/EFCore/Query/EvaluatableExpressionFilter.cs +++ b/src/EFCore/Query/EvaluatableExpressionFilter.cs @@ -81,7 +81,8 @@ public virtual bool IsEvaluatableExpression(Expression expression, IModel model) || Equals(method, RandomNextNoArgs) || Equals(method, RandomNextOneArg) || Equals(method, RandomNextTwoArgs) - || method.DeclaringType == typeof(DbFunctionsExtensions)) + || method.DeclaringType == typeof(DbFunctionsExtensions) + || method.IsEFDefaultMethod()) { return false; } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs index 6e23350e842..38aced574f3 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -380,6 +380,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_default(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => EF.Default()), + rowsAffectedCount: 8, + (b, a) => Assert.All(a, c => Assert.Null(c.ContactName))); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Update_Where_parameter_set_constant(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs index 478adaca8a4..42ae2dac218 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs @@ -568,6 +568,17 @@ FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'F%'"); } + public override async Task Update_Where_set_default(bool async) + { + await base.Update_Where_set_default(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[ContactName] = DEFAULT +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + public override async Task Update_Where_parameter_set_constant(bool async) { await base.Update_Where_parameter_set_constant(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs index 0a1941d0a1d..785289a6102 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore.Sqlite.Internal; namespace Microsoft.EntityFrameworkCore.BulkUpdates; @@ -549,6 +550,12 @@ public override async Task Update_Where_set_constant(bool async) WHERE ""c"".""CustomerID"" LIKE 'F%'"); } + public override Task Update_Where_set_default(bool async) + => AssertTranslationFailed( + RelationalStrings.UnableToTranslateSetProperty( + "c => c.ContactName", "c => EF.Default()", SqliteStrings.DefaultNotSupported), + () => base.Update_Where_set_default(async)); + public override async Task Update_Where_parameter_set_constant(bool async) { await base.Update_Where_parameter_set_constant(async);