diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 77cb0b5d732..6164e159e28 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -58,6 +58,9 @@ private static readonly PropertyInfo QueryContextContextPropertyInfo private readonly Dictionary _parameters = new(); + private static readonly bool UseOldBehavior37247 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37247", out var enabled) && enabled; + /// /// 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 @@ -1064,6 +1067,17 @@ private Expression ProcessExecuteUpdate(NavigationExpansionExpression source, Me { source = (NavigationExpansionExpression)_pendingSelectorExpandingExpressionVisitor.Visit(source); + if (!UseOldBehavior37247) + { + // Apply any pending selector before processing the ExecuteUpdate setters; this adds a Select() (if necessary) before + // ExecuteUpdate, to avoid the pending selector flowing into each setter lambda and making it more complicated. + var newStructure = SnapshotExpression(source.PendingSelector); + var queryable = Reduce(source); + var navigationTree = new NavigationTreeExpression(newStructure); + var parameterName = source.CurrentParameter.Name ?? GetParameterName("e"); + source = new NavigationExpansionExpression(queryable, navigationTree, navigationTree, parameterName); + } + NewArrayExpression settersArray; switch (setters) { diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs index 2db7d066f6d..55b973e4059 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs @@ -68,7 +68,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("o => o.Outer.OrderDate", "o => o.Inner.ContactName"), + RelationalStrings.MultipleTablesInExecuteUpdate("o => o.e.OrderDate", "o => o.Customer.ContactName"), () => base.Update_multiple_tables_throws(async)); public override Task Update_unmapped_property_throws(bool async) diff --git a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs index 239ff1995b1..c91ccb1a089 100644 --- a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -951,4 +951,26 @@ public virtual Task Update_with_two_inner_joins(bool async) s => s.SetProperty(od => od.Quantity, 1), rowsAffectedCount: 228, (b, a) => Assert.All(a, od => Assert.Equal(1, od.Quantity))); + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] // #37247 + public virtual Task Update_with_PK_pushdown_and_join_and_multiple_setters(bool async) + => AssertUpdate( + async, + ss => ss + .Set() + // Not natively supported by UPDATE, so triggers PK subquery + .OrderBy(od => od.OrderID) + .Skip(1) + // Triggers JOIN with a pending selector for transparent identifier (Outer, Inner) + .Where(od => od.Order.CustomerID == "ALFKI"), + e => e, + s => s + .SetProperty(od => od.Quantity, 1) + .SetProperty(od => od.UnitPrice, 10), + rowsAffectedCount: 12, + (b, a) => Assert.All(a, od => + { + Assert.Equal(1, od.Quantity); + Assert.Equal(10, od.UnitPrice); + })); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs index e77f0187114..8152a4a77d6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs @@ -1623,6 +1623,34 @@ FROM [Order Details] AS [o] """); } + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public override async Task Update_with_PK_pushdown_and_join_and_multiple_setters(bool async) + { + await base.Update_with_PK_pushdown_and_join_and_multiple_setters(async); + + AssertExecuteUpdateSql( + """ +@p='1' +@p1='10' (DbType = Currency) + +UPDATE [o2] +SET [o2].[Quantity] = CAST(@p AS smallint), + [o2].[UnitPrice] = @p1 +FROM [Order Details] AS [o2] +INNER JOIN ( + SELECT [o1].[OrderID], [o1].[ProductID] + FROM ( + SELECT [o].[OrderID], [o].[ProductID] + FROM [Order Details] AS [o] + ORDER BY [o].[OrderID] + OFFSET @p ROWS + ) AS [o1] + INNER JOIN [Orders] AS [o0] ON [o1].[OrderID] = [o0].[OrderID] + WHERE [o0].[CustomerID] = N'ALFKI' +) AS [s] ON [o2].[OrderID] = [s].[OrderID] AND [o2].[ProductID] = [s].[ProductID] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs index a7118bf2a65..7de7a531f03 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs @@ -1527,6 +1527,34 @@ public override async Task Update_with_two_inner_joins(bool async) AssertExecuteUpdateSql( """ +"""); + } + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public override async Task Update_with_PK_pushdown_and_join_and_multiple_setters(bool async) + { + await base.Update_with_PK_pushdown_and_join_and_multiple_setters(async); + + AssertExecuteUpdateSql( + """ +@p='1' +@p1='10' + +UPDATE "Order Details" AS "o2" +SET "Quantity" = CAST(@p AS INTEGER), + "UnitPrice" = @p1 +FROM ( + SELECT "o1"."OrderID", "o1"."ProductID" + FROM ( + SELECT "o"."OrderID", "o"."ProductID" + FROM "Order Details" AS "o" + ORDER BY "o"."OrderID" + LIMIT -1 OFFSET @p + ) AS "o1" + INNER JOIN "Orders" AS "o0" ON "o1"."OrderID" = "o0"."OrderID" + WHERE "o0"."CustomerID" = 'ALFKI' +) AS "s" +WHERE "o2"."OrderID" = "s"."OrderID" AND "o2"."ProductID" = "s"."ProductID" """); }