Skip to content

Commit 1ff8573

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 5077465 commit 1ff8573

File tree

4 files changed

+125
-37
lines changed

4 files changed

+125
-37
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
10291029
{
10301030
if (source.ShaperExpression is IncludeExpression includeExpression)
10311031
{
1032-
source = source.UpdateShaperExpression(PruneOwnedIncludes(includeExpression));
1032+
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
10331033
}
10341034

10351035
if (source.ShaperExpression is not EntityShaperExpression entityShaperExpression)
@@ -1128,20 +1128,6 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa
11281128
Expression.Quote(Expression.Lambda(predicateBody, entityParameter)));
11291129

11301130
return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource));
1131-
1132-
static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
1133-
{
1134-
if (includeExpression.Navigation is ISkipNavigation
1135-
|| includeExpression.Navigation is not INavigation navigation
1136-
|| !navigation.ForeignKey.IsOwnership)
1137-
{
1138-
return includeExpression;
1139-
}
1140-
1141-
return includeExpression.EntityExpression is IncludeExpression innerIncludeExpression
1142-
? PruneOwnedIncludes(innerIncludeExpression)
1143-
: includeExpression.EntityExpression;
1144-
}
11451131
}
11461132

11471133
/// <summary>
@@ -1163,6 +1149,13 @@ static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
11631149
ShapedQueryExpression source,
11641150
LambdaExpression setPropertyCalls)
11651151
{
1152+
// Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for
1153+
// ExecuteUpdate's lambdas. Note that we don't currently support updates across tables.
1154+
if (source.ShaperExpression is IncludeExpression includeExpression)
1155+
{
1156+
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
1157+
}
1158+
11661159
var propertyValueLambdaExpressions = new List<(LambdaExpression, Expression)>();
11671160
PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]);
11681161
if (TranslationErrorDetails != null)
@@ -1381,11 +1374,16 @@ void PopulateSetPropertyCalls(
13811374
when parameter == p:
13821375
break;
13831376

1384-
case MethodCallExpression methodCallExpression
1385-
when methodCallExpression.Method.IsGenericMethod
1386-
&& methodCallExpression.Method.Name == nameof(SetPropertyCalls<int>.SetProperty)
1387-
&& methodCallExpression.Method.DeclaringType!.IsGenericType
1388-
&& methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>):
1377+
case MethodCallExpression
1378+
{
1379+
Method:
1380+
{
1381+
IsGenericMethod: true,
1382+
Name: nameof(SetPropertyCalls<int>.SetProperty),
1383+
DeclaringType.IsGenericType: true
1384+
}
1385+
} methodCallExpression
1386+
when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>):
13891387

13901388
list.Add(((LambdaExpression)methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]));
13911389

@@ -1400,8 +1398,8 @@ when methodCallExpression.Method.IsGenericMethod
14001398
}
14011399

14021400
// 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).
1401+
// We also unwrap casts to interface/base class (#29618). Note that owned IncludeExpressions have already been pruned from the
1402+
// source before remapping the lambda (#28727).
14051403
static bool TryProcessPropertyAccess(
14061404
IModel model,
14071405
ref Expression expression,
@@ -1410,7 +1408,7 @@ static bool TryProcessPropertyAccess(
14101408
expression = expression.UnwrapTypeConversion(out _);
14111409

14121410
if (expression is MemberExpression { Expression : not null } memberExpression
1413-
&& Unwrap(memberExpression.Expression) is EntityShaperExpression ese)
1411+
&& memberExpression.Expression.UnwrapTypeConversion(out _) is EntityShaperExpression ese)
14141412
{
14151413
expression = memberExpression.Update(ese);
14161414
entityShaperExpression = ese;
@@ -1420,7 +1418,7 @@ static bool TryProcessPropertyAccess(
14201418
if (expression is MethodCallExpression mce)
14211419
{
14221420
if (mce.TryGetEFPropertyArguments(out var source, out _)
1423-
&& Unwrap(source) is EntityShaperExpression ese1)
1421+
&& source.UnwrapTypeConversion(out _) is EntityShaperExpression ese1)
14241422
{
14251423
if (source != ese1)
14261424
{
@@ -1434,7 +1432,7 @@ static bool TryProcessPropertyAccess(
14341432
}
14351433

14361434
if (mce.TryGetIndexerArguments(model, out var source2, out _)
1437-
&& Unwrap(source2) is EntityShaperExpression ese2)
1435+
&& source2.UnwrapTypeConversion(out _) is EntityShaperExpression ese2)
14381436
{
14391437
expression = mce.Update(ese2, mce.Arguments);
14401438
entityShaperExpression = ese2;
@@ -1444,18 +1442,6 @@ static bool TryProcessPropertyAccess(
14441442

14451443
entityShaperExpression = null;
14461444
return false;
1447-
1448-
static Expression Unwrap(Expression expression)
1449-
{
1450-
expression = expression.UnwrapTypeConversion(out _);
1451-
1452-
while (expression is IncludeExpression includeExpression)
1453-
{
1454-
expression = includeExpression.EntityExpression;
1455-
}
1456-
1457-
return expression;
1458-
}
14591445
}
14601446

14611447
// Old quirked implementation only
@@ -1660,6 +1646,18 @@ private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression,
16601646
private Expression ExpandSharedTypeEntities(SelectExpression selectExpression, Expression lambdaBody)
16611647
=> _sharedTypeEntityExpandingExpressionVisitor.Expand(selectExpression, lambdaBody);
16621648

1649+
private static Expression PruneIncludes(IncludeExpression includeExpression)
1650+
{
1651+
if (includeExpression.Navigation is ISkipNavigation or not INavigation)
1652+
{
1653+
return includeExpression;
1654+
}
1655+
1656+
return includeExpression.EntityExpression is IncludeExpression innerIncludeExpression
1657+
? PruneIncludes(innerIncludeExpression)
1658+
: includeExpression.EntityExpression;
1659+
}
1660+
16631661
private sealed class SharedTypeEntityExpandingExpressionVisitor : ExpressionVisitor
16641662
{
16651663
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;

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

Lines changed: 52 additions & 0 deletions
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

Lines changed: 24 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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)