Skip to content

Commit 39531c8

Browse files
authored
Fix ExecuteUpdate over scalar projections (#37791)
Fixes #37771
1 parent 209f088 commit 39531c8

4 files changed

Lines changed: 106 additions & 4 deletions

File tree

src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ private static readonly PropertyInfo QueryContextContextPropertyInfo
6161
private static readonly bool UseOldBehavior37247 =
6262
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37247", out var enabled) && enabled;
6363

64+
private static readonly bool UseOldBehavior37771 =
65+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37771", out var enabled) && enabled;
66+
6467
/// <summary>
6568
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
6669
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -1071,11 +1074,18 @@ private Expression ProcessExecuteUpdate(NavigationExpansionExpression source, Me
10711074
{
10721075
// Apply any pending selector before processing the ExecuteUpdate setters; this adds a Select() (if necessary) before
10731076
// ExecuteUpdate, to avoid the pending selector flowing into each setter lambda and making it more complicated.
1077+
// However, only do this when the pending selector produces entity/structural type references (i.e. the snapshot is not just
1078+
// a DefaultExpression). When the pending selector projects only scalar values (e.g. select new { p.Used, n.Qty }),
1079+
// applying it would lose the connection between the projected scalar and the original entity property, breaking
1080+
// ExecuteUpdate's property selector recognition (#37771).
10741081
var newStructure = SnapshotExpression(source.PendingSelector);
1075-
var queryable = Reduce(source);
1076-
var navigationTree = new NavigationTreeExpression(newStructure);
1077-
var parameterName = source.CurrentParameter.Name ?? GetParameterName("e");
1078-
source = new NavigationExpansionExpression(queryable, navigationTree, navigationTree, parameterName);
1082+
if (newStructure is not DefaultExpression || UseOldBehavior37771)
1083+
{
1084+
var queryable = Reduce(source);
1085+
var navigationTree = new NavigationTreeExpression(newStructure);
1086+
var parameterName = source.CurrentParameter.Name ?? GetParameterName("e");
1087+
source = new NavigationExpansionExpression(queryable, navigationTree, navigationTree, parameterName);
1088+
}
10791089
}
10801090

10811091
NewArrayExpression settersArray;

test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesRelationalTestBase.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,44 @@ FROM [Customers]
9999
}
100100
});
101101

102+
[ConditionalTheory, MemberData(nameof(IsAsyncData))] // #37771
103+
public virtual Task Update_with_select_mixed_entity_scalar_anonymous_projection(bool async)
104+
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
105+
() => Fixture.CreateContext(),
106+
(facade, transaction) => Fixture.UseTransaction(facade, transaction),
107+
async context =>
108+
{
109+
var queryable = context.Set<Customer>().Select(c => new { Entity = c, c.ContactName });
110+
111+
if (async)
112+
{
113+
await queryable.ExecuteUpdateAsync(s => s.SetProperty(c => c.Entity.ContactName, "Updated"));
114+
}
115+
else
116+
{
117+
queryable.ExecuteUpdate(s => s.SetProperty(c => c.Entity.ContactName, "Updated"));
118+
}
119+
});
120+
121+
[ConditionalTheory, MemberData(nameof(IsAsyncData))] // #37771
122+
public virtual Task Update_with_select_scalar_anonymous_projection(bool async)
123+
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
124+
() => Fixture.CreateContext(),
125+
(facade, transaction) => Fixture.UseTransaction(facade, transaction),
126+
async context =>
127+
{
128+
var queryable = context.Set<Customer>().Select(c => new { c.ContactName, c.City });
129+
130+
if (async)
131+
{
132+
await queryable.ExecuteUpdateAsync(s => s.SetProperty(c => c.ContactName, "Updated"));
133+
}
134+
else
135+
{
136+
queryable.ExecuteUpdate(s => s.SetProperty(c => c.ContactName, "Updated"));
137+
}
138+
});
139+
102140
protected static async Task AssertTranslationFailed(string details, Func<Task> query)
103141
=> Assert.Contains(
104142
CoreStrings.NonQueryTranslationFailedWithDetails("", details)[21..],

test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,6 +1651,34 @@ OFFSET @p ROWS
16511651
""");
16521652
}
16531653

1654+
public override async Task Update_with_select_mixed_entity_scalar_anonymous_projection(bool async)
1655+
{
1656+
await base.Update_with_select_mixed_entity_scalar_anonymous_projection(async);
1657+
1658+
AssertSql(
1659+
"""
1660+
@p='Updated' (Size = 30)
1661+
1662+
UPDATE [c]
1663+
SET [c].[ContactName] = @p
1664+
FROM [Customers] AS [c]
1665+
""");
1666+
}
1667+
1668+
public override async Task Update_with_select_scalar_anonymous_projection(bool async)
1669+
{
1670+
await base.Update_with_select_scalar_anonymous_projection(async);
1671+
1672+
AssertSql(
1673+
"""
1674+
@p='Updated' (Size = 30)
1675+
1676+
UPDATE [c]
1677+
SET [c].[ContactName] = @p
1678+
FROM [Customers] AS [c]
1679+
""");
1680+
}
1681+
16541682
private void AssertSql(params string[] expected)
16551683
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
16561684

test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,32 @@ ORDER BY "o"."OrderID"
15581558
""");
15591559
}
15601560

1561+
public override async Task Update_with_select_mixed_entity_scalar_anonymous_projection(bool async)
1562+
{
1563+
await base.Update_with_select_mixed_entity_scalar_anonymous_projection(async);
1564+
1565+
AssertSql(
1566+
"""
1567+
@p='Updated' (Size = 7)
1568+
1569+
UPDATE "Customers" AS "c"
1570+
SET "ContactName" = @p
1571+
""");
1572+
}
1573+
1574+
public override async Task Update_with_select_scalar_anonymous_projection(bool async)
1575+
{
1576+
await base.Update_with_select_scalar_anonymous_projection(async);
1577+
1578+
AssertSql(
1579+
"""
1580+
@p='Updated' (Size = 7)
1581+
1582+
UPDATE "Customers" AS "c"
1583+
SET "ContactName" = @p
1584+
""");
1585+
}
1586+
15611587
private void AssertSql(params string[] expected)
15621588
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
15631589

0 commit comments

Comments
 (0)