Skip to content

Commit 6dedc58

Browse files
committed
Fixes around IncludeExpression for ExecuteUpdate/Delete (dotnet#30571)
* Fully prune IncludeExpression before ExecuteUpdate, not just for the property lambda * Prune also non-owned Includes Fixes dotnet#30572 Fixes dotnet#30528 (cherry picked from commit 4c6f854)
1 parent f3d7019 commit 6dedc58

File tree

4 files changed

+132
-6
lines changed

4 files changed

+132
-6
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

+42-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMe
2020

2121
private static readonly bool QuirkEnabled28727
2222
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue28727", out var enabled) && enabled;
23+
private static readonly bool QuirkEnabled30528
24+
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30528", out var enabled) && enabled;
25+
private static readonly bool QuirkEnabled30572
26+
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30572", out var enabled) && enabled;
2327

2428
/// <summary>
2529
/// Creates a new instance of the <see cref="QueryableMethodTranslatingExpressionVisitor" /> class.
@@ -1029,7 +1033,14 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
10291033
{
10301034
if (source.ShaperExpression is IncludeExpression includeExpression)
10311035
{
1032-
source = source.UpdateShaperExpression(PruneOwnedIncludes(includeExpression));
1036+
if (QuirkEnabled30572)
1037+
{
1038+
source = source.UpdateShaperExpression(PruneOwnedIncludes(includeExpression));
1039+
}
1040+
else
1041+
{
1042+
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
1043+
}
10331044
}
10341045

10351046
if (source.ShaperExpression is not EntityShaperExpression entityShaperExpression)
@@ -1129,6 +1140,7 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa
11291140

11301141
return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource));
11311142

1143+
// Old quirked implementation for #30572
11321144
static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
11331145
{
11341146
if (includeExpression.Navigation is ISkipNavigation
@@ -1163,6 +1175,16 @@ static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
11631175
ShapedQueryExpression source,
11641176
LambdaExpression setPropertyCalls)
11651177
{
1178+
if (!QuirkEnabled30528)
1179+
{
1180+
// Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for
1181+
// ExecuteUpdate's lambdas. Note that we don't currently support updates across tables.
1182+
if (source.ShaperExpression is IncludeExpression includeExpression)
1183+
{
1184+
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
1185+
}
1186+
}
1187+
11661188
var propertyValueLambdaExpressions = new List<(LambdaExpression, Expression)>();
11671189
PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]);
11681190
if (TranslationErrorDetails != null)
@@ -1386,7 +1408,6 @@ when methodCallExpression.Method.IsGenericMethod
13861408
&& methodCallExpression.Method.Name == nameof(SetPropertyCalls<int>.SetProperty)
13871409
&& methodCallExpression.Method.DeclaringType!.IsGenericType
13881410
&& methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>):
1389-
13901411
list.Add(((LambdaExpression)methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]));
13911412

13921413
PopulateSetPropertyCalls(methodCallExpression.Object!, list, parameter);
@@ -1400,8 +1421,8 @@ when methodCallExpression.Method.IsGenericMethod
14001421
}
14011422

14021423
// For property setter selectors in ExecuteUpdate, we support only simple member access, EF.Function, etc.
1403-
// We also unwrap casts to interface/base class (#29618), as well as IncludeExpressions (which occur when the target entity has
1404-
// owned entities, #28727).
1424+
// We also unwrap casts to interface/base class (#29618). Note that owned IncludeExpressions have already been pruned from the
1425+
// source before remapping the lambda (#28727).
14051426
static bool TryProcessPropertyAccess(
14061427
IModel model,
14071428
ref Expression expression,
@@ -1449,9 +1470,12 @@ static Expression Unwrap(Expression expression)
14491470
{
14501471
expression = expression.UnwrapTypeConversion(out _);
14511472

1452-
while (expression is IncludeExpression includeExpression)
1473+
if (QuirkEnabled30528)
14531474
{
1454-
expression = includeExpression.EntityExpression;
1475+
while (expression is IncludeExpression includeExpression)
1476+
{
1477+
expression = includeExpression.EntityExpression;
1478+
}
14551479
}
14561480

14571481
return expression;
@@ -1660,6 +1684,18 @@ private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression,
16601684
private Expression ExpandSharedTypeEntities(SelectExpression selectExpression, Expression lambdaBody)
16611685
=> _sharedTypeEntityExpandingExpressionVisitor.Expand(selectExpression, lambdaBody);
16621686

1687+
private static Expression PruneIncludes(IncludeExpression includeExpression)
1688+
{
1689+
if (includeExpression.Navigation is ISkipNavigation or not INavigation)
1690+
{
1691+
return includeExpression;
1692+
}
1693+
1694+
return includeExpression.EntityExpression is IncludeExpression innerIncludeExpression
1695+
? PruneIncludes(innerIncludeExpression)
1696+
: includeExpression.EntityExpression;
1697+
}
1698+
16631699
private sealed class SharedTypeEntityExpandingExpressionVisitor : ExpressionVisitor
16641700
{
16651701
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;

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

+52
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,58 @@ await AssertUpdate(
109109
rowsAffectedCount: 0);
110110
}
111111

112+
[ConditionalTheory]
113+
[MemberData(nameof(IsAsyncData))]
114+
public virtual async Task Update_non_owned_property_on_entity_with_owned2(bool async)
115+
{
116+
var contextFactory = await InitializeAsync<Context28671>(
117+
onModelCreating: mb =>
118+
{
119+
mb.Entity<Owner>().OwnsOne(o => o.OwnedReference);
120+
});
121+
122+
await AssertUpdate(
123+
async,
124+
contextFactory.CreateContext,
125+
ss => ss.Set<Owner>(),
126+
s => s.SetProperty(o => o.Title, o => o.Title + "_Suffix"),
127+
rowsAffectedCount: 0);
128+
}
129+
130+
[ConditionalTheory]
131+
[MemberData(nameof(IsAsyncData))]
132+
public virtual async Task Delete_entity_with_auto_include(bool async)
133+
{
134+
var contextFactory = await InitializeAsync<Context30572>();
135+
await AssertDelete(async, contextFactory.CreateContext, ss => ss.Set<Context30572_Principal>(), rowsAffectedCount: 0);
136+
}
137+
138+
protected class Context30572 : DbContext
139+
{
140+
public Context30572(DbContextOptions options)
141+
: base(options)
142+
{
143+
}
144+
145+
protected override void OnModelCreating(ModelBuilder modelBuilder)
146+
=> modelBuilder.Entity<Context30572_Principal>().Navigation(o => o.Dependent).AutoInclude();
147+
}
148+
149+
public class Context30572_Principal
150+
{
151+
public int Id { get; set; }
152+
public string? Title { get; set; }
153+
154+
public Context30572_Dependent? Dependent { get; set; }
155+
}
156+
157+
public class Context30572_Dependent
158+
{
159+
public int Id { get; set; }
160+
161+
public int Number { get; set; }
162+
}
163+
112164
[ConditionalTheory]
113165
[MemberData(nameof(IsAsyncData))]
114166
public virtual async Task Delete_predicate_based_on_optional_navigation(bool async)

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

+24
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,30 @@ FROM [Owner] AS [o]
5353
""");
5454
}
5555

56+
public override async Task Update_non_owned_property_on_entity_with_owned2(bool async)
57+
{
58+
await base.Update_non_owned_property_on_entity_with_owned2(async);
59+
60+
AssertSql(
61+
"""
62+
UPDATE [o]
63+
SET [o].[Title] = COALESCE([o].[Title], N'') + N'_Suffix'
64+
FROM [Owner] AS [o]
65+
""");
66+
}
67+
68+
public override async Task Delete_entity_with_auto_include(bool async)
69+
{
70+
await base.Delete_entity_with_auto_include(async);
71+
72+
AssertSql(
73+
"""
74+
DELETE FROM [c]
75+
FROM [Context30572_Principal] AS [c]
76+
LEFT JOIN [Context30572_Dependent] AS [c0] ON [c].[DependentId] = [c0].[Id]
77+
""");
78+
}
79+
5680
public override async Task Delete_predicate_based_on_optional_navigation(bool async)
5781
{
5882
await base.Delete_predicate_based_on_optional_navigation(async);

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

+14
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ public override async Task Update_non_owned_property_on_entity_with_owned(bool a
5050
""");
5151
}
5252

53+
public override async Task Update_non_owned_property_on_entity_with_owned2(bool async)
54+
{
55+
await base.Update_non_owned_property_on_entity_with_owned2(async);
56+
57+
AssertSql(
58+
"""
59+
UPDATE "Owner" AS "o"
60+
SET "Title" = COALESCE("o"."Title", '') || '_Suffix'
61+
""");
62+
}
63+
64+
public override Task Delete_entity_with_auto_include(bool async)
65+
=> Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => base.Delete_entity_with_auto_include(async));
66+
5367
public override async Task Delete_predicate_based_on_optional_navigation(bool async)
5468
{
5569
await base.Delete_predicate_based_on_optional_navigation(async);

0 commit comments

Comments
 (0)