Skip to content

Commit 4c6f854

Browse files
authored
Fixes around IncludeExpression for ExecuteUpdate/Delete (#30571)
* Fully prune IncludeExpression before ExecuteUpdate, not just for the property lambda * Prune also non-owned Includes Fixes #30572 Fixes #30528
1 parent ae1bc0d commit 4c6f854

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
@@ -1044,7 +1044,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
10441044
{
10451045
if (source.ShaperExpression is IncludeExpression includeExpression)
10461046
{
1047-
source = source.UpdateShaperExpression(PruneOwnedIncludes(includeExpression));
1047+
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
10481048
}
10491049

10501050
if (source.ShaperExpression is not EntityShaperExpression entityShaperExpression)
@@ -1143,20 +1143,6 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa
11431143
Expression.Quote(Expression.Lambda(predicateBody, entityParameter)));
11441144

11451145
return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource));
1146-
1147-
static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
1148-
{
1149-
if (includeExpression.Navigation is ISkipNavigation
1150-
|| includeExpression.Navigation is not INavigation navigation
1151-
|| !navigation.ForeignKey.IsOwnership)
1152-
{
1153-
return includeExpression;
1154-
}
1155-
1156-
return includeExpression.EntityExpression is IncludeExpression innerIncludeExpression
1157-
? PruneOwnedIncludes(innerIncludeExpression)
1158-
: includeExpression.EntityExpression;
1159-
}
11601146
}
11611147

11621148
/// <summary>
@@ -1178,6 +1164,13 @@ static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
11781164
ShapedQueryExpression source,
11791165
LambdaExpression setPropertyCalls)
11801166
{
1167+
// Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for
1168+
// ExecuteUpdate's lambdas. Note that we don't currently support updates across tables.
1169+
if (source.ShaperExpression is IncludeExpression includeExpression)
1170+
{
1171+
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
1172+
}
1173+
11811174
var propertyValueLambdaExpressions = new List<(LambdaExpression, Expression)>();
11821175
PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]);
11831176
if (TranslationErrorDetails != null)
@@ -1382,11 +1375,16 @@ void PopulateSetPropertyCalls(
13821375
when parameter == p:
13831376
break;
13841377

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

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

@@ -1401,8 +1399,8 @@ when methodCallExpression.Method.IsGenericMethod
14011399
}
14021400

14031401
// For property setter selectors in ExecuteUpdate, we support only simple member access, EF.Function, etc.
1404-
// We also unwrap casts to interface/base class (#29618), as well as IncludeExpressions (which occur when the target entity has
1405-
// owned entities, #28727).
1402+
// We also unwrap casts to interface/base class (#29618). Note that owned IncludeExpressions have already been pruned from the
1403+
// source before remapping the lambda (#28727).
14061404
static bool TryProcessPropertyAccess(
14071405
IModel model,
14081406
ref Expression expression,
@@ -1411,7 +1409,7 @@ static bool TryProcessPropertyAccess(
14111409
expression = expression.UnwrapTypeConversion(out _);
14121410

14131411
if (expression is MemberExpression { Expression : not null } memberExpression
1414-
&& Unwrap(memberExpression.Expression) is EntityShaperExpression ese)
1412+
&& memberExpression.Expression.UnwrapTypeConversion(out _) is EntityShaperExpression ese)
14151413
{
14161414
expression = memberExpression.Update(ese);
14171415
entityShaperExpression = ese;
@@ -1421,7 +1419,7 @@ static bool TryProcessPropertyAccess(
14211419
if (expression is MethodCallExpression mce)
14221420
{
14231421
if (mce.TryGetEFPropertyArguments(out var source, out _)
1424-
&& Unwrap(source) is EntityShaperExpression ese1)
1422+
&& source.UnwrapTypeConversion(out _) is EntityShaperExpression ese1)
14251423
{
14261424
if (source != ese1)
14271425
{
@@ -1435,7 +1433,7 @@ static bool TryProcessPropertyAccess(
14351433
}
14361434

14371435
if (mce.TryGetIndexerArguments(model, out var source2, out _)
1438-
&& Unwrap(source2) is EntityShaperExpression ese2)
1436+
&& source2.UnwrapTypeConversion(out _) is EntityShaperExpression ese2)
14391437
{
14401438
expression = mce.Update(ese2, mce.Arguments);
14411439
entityShaperExpression = ese2;
@@ -1445,18 +1443,6 @@ static bool TryProcessPropertyAccess(
14451443

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

14621448
static Expression GetEntitySource(IModel model, Expression propertyAccessExpression)
@@ -1628,6 +1614,18 @@ private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression,
16281614
private Expression ExpandSharedTypeEntities(SelectExpression selectExpression, Expression lambdaBody)
16291615
=> _sharedTypeEntityExpandingExpressionVisitor.Expand(selectExpression, lambdaBody);
16301616

1617+
private static Expression PruneIncludes(IncludeExpression includeExpression)
1618+
{
1619+
if (includeExpression.Navigation is ISkipNavigation or not INavigation)
1620+
{
1621+
return includeExpression;
1622+
}
1623+
1624+
return includeExpression.EntityExpression is IncludeExpression innerIncludeExpression
1625+
? PruneIncludes(innerIncludeExpression)
1626+
: includeExpression.EntityExpression;
1627+
}
1628+
16311629
private sealed class SharedTypeEntityExpandingExpressionVisitor : ExpressionVisitor
16321630
{
16331631
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)