Skip to content

Commit f072ffd

Browse files
committed
Support ExecuteUpdate on entity with owned types
Referencing unowned properties only for now. Fixes #29618
1 parent 124d334 commit f072ffd

File tree

4 files changed

+69
-32
lines changed

4 files changed

+69
-32
lines changed

src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,7 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio
103103
return null;
104104
}
105105

106-
if (!(expression is NewExpression
107-
|| expression is MemberInitExpression
108-
|| expression is EntityShaperExpression
109-
|| expression is IncludeExpression))
106+
if (expression is not NewExpression and not MemberInitExpression and not EntityShaperExpression and not IncludeExpression)
110107
{
111108
if (_indexBasedBinding)
112109
{

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMe
1818
private readonly ISqlExpressionFactory _sqlExpressionFactory;
1919
private readonly bool _subquery;
2020

21+
private UpdatePropertySelectorUnwrappingExpressionVisitor? _updatePropertySelectorUnwrappingExpressionVisitor;
22+
2123
/// <summary>
2224
/// Creates a new instance of the <see cref="QueryableMethodTranslatingExpressionVisitor" /> class.
2325
/// </summary>
@@ -1196,7 +1198,10 @@ static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
11961198
{
11971199
var left = RemapLambdaBody(source, propertyExpression);
11981200

1199-
if (!TryProcessPropertyAccess(RelationalDependencies.Model, ref left, out var ese))
1201+
_updatePropertySelectorUnwrappingExpressionVisitor ??= new UpdatePropertySelectorUnwrappingExpressionVisitor();
1202+
left = _updatePropertySelectorUnwrappingExpressionVisitor.Visit(left);
1203+
1204+
if (!IsValidPropertyAccess(RelationalDependencies.Model, left, out var ese))
12001205
{
12011206
AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertyExpression.Print()));
12021207
return null;
@@ -1399,46 +1404,29 @@ when methodCallExpression.Method.IsGenericMethod
13991404
}
14001405
}
14011406

1402-
static bool TryProcessPropertyAccess(
1407+
static bool IsValidPropertyAccess(
14031408
IModel model,
1404-
ref Expression expression,
1409+
Expression expression,
14051410
[NotNullWhen(true)] out EntityShaperExpression? entityShaperExpression)
14061411
{
1407-
// Unwrap any object/base-type Convert nodes around the property access expression
1408-
expression = expression.UnwrapTypeConversion(out _);
1409-
1410-
// Identify property access (direct, EF.Property...), while also unwrapping object/base-type Convert nodes on the expression
1411-
// being accessed.
1412-
if (expression is MemberExpression memberExpression
1413-
&& memberExpression.Expression.UnwrapTypeConversion(out _) is EntityShaperExpression ese)
1412+
if (expression is MemberExpression { Expression: EntityShaperExpression ese })
14141413
{
1415-
expression = memberExpression.Update(ese);
1416-
14171414
entityShaperExpression = ese;
14181415
return true;
14191416
}
14201417

14211418
if (expression is MethodCallExpression mce)
14221419
{
14231420
if (mce.TryGetEFPropertyArguments(out var source, out _)
1424-
&& source.UnwrapTypeConversion(out _) is EntityShaperExpression ese1)
1421+
&& source is EntityShaperExpression ese1)
14251422
{
1426-
if (source != ese1)
1427-
{
1428-
var rewrittenArguments = mce.Arguments.ToArray();
1429-
rewrittenArguments[0] = ese1;
1430-
expression = mce.Update(mce.Object, rewrittenArguments);
1431-
}
1432-
14331423
entityShaperExpression = ese1;
14341424
return true;
14351425
}
14361426

14371427
if (mce.TryGetIndexerArguments(model, out var source2, out _)
1438-
&& source2.UnwrapTypeConversion(out _) is EntityShaperExpression ese2)
1428+
&& source2 is EntityShaperExpression ese2)
14391429
{
1440-
expression = mce.Update(ese2, mce.Arguments);
1441-
14421430
entityShaperExpression = ese2;
14431431
return true;
14441432
}
@@ -1468,6 +1456,29 @@ static Expression GetEntitySource(IModel model, Expression propertyAccessExpress
14681456
}
14691457
}
14701458

1459+
// For property setter selectors in ExecuteUpdate, this unwraps casts to interface/base class (#29618), as well as IncludeExpressions
1460+
// (which occur when the target entity has owned entities, #28727).
1461+
private class UpdatePropertySelectorUnwrappingExpressionVisitor : ExpressionVisitor
1462+
{
1463+
[return: NotNullIfNotNull(nameof(node))]
1464+
public override Expression? Visit(Expression? node)
1465+
{
1466+
if (node is null)
1467+
{
1468+
return node;
1469+
}
1470+
1471+
node = node.UnwrapTypeConversion(out _);
1472+
1473+
if (node is IncludeExpression includeExpression)
1474+
{
1475+
node = Visit(includeExpression.EntityExpression);
1476+
}
1477+
1478+
return base.Visit(node);
1479+
}
1480+
}
1481+
14711482
/// <summary>
14721483
/// Checks weather the current select expression can be used as-is for execute a delete operation,
14731484
/// or whether it must be pushed down into a subquery.
@@ -1662,11 +1673,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
16621673
}
16631674

16641675
protected override Expression VisitExtension(Expression extensionExpression)
1665-
=> extensionExpression is EntityShaperExpression
1666-
|| extensionExpression is ShapedQueryExpression
1667-
|| extensionExpression is GroupByShaperExpression
1668-
? extensionExpression
1669-
: base.VisitExtension(extensionExpression);
1676+
=> extensionExpression is EntityShaperExpression or ShapedQueryExpression or GroupByShaperExpression
1677+
? extensionExpression
1678+
: base.VisitExtension(extensionExpression);
16701679

16711680
private Expression? TryExpand(Expression? source, MemberIdentity member)
16721681
{

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ public class OtherReference
9090
}
9191

9292
#nullable enable
93+
94+
[ConditionalTheory]
95+
[MemberData(nameof(IsAsyncData))]
96+
public virtual async Task Update_non_owned_property_on_entity_with_owned(bool async)
97+
{
98+
var contextFactory = await InitializeAsync<Context28671>(
99+
onModelCreating: mb =>
100+
{
101+
mb.Entity<Owner>().OwnsOne(o => o.OwnedReference);
102+
});
103+
104+
await AssertUpdate(
105+
async,
106+
contextFactory.CreateContext,
107+
ss => ss.Set<Owner>(),
108+
s => s.SetProperty(o => o.Title, "SomeValue"),
109+
rowsAffectedCount: 0);
110+
}
111+
93112
[ConditionalTheory]
94113
[MemberData(nameof(IsAsyncData))]
95114
public virtual async Task Delete_predicate_based_on_optional_navigation(bool async)

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ public override async Task Delete_aggregate_root_when_table_sharing_with_non_own
4141
AssertSql();
4242
}
4343

44+
public override async Task Update_non_owned_property_on_entity_with_owned(bool async)
45+
{
46+
await base.Update_non_owned_property_on_entity_with_owned(async);
47+
48+
AssertSql(
49+
"""
50+
UPDATE [o]
51+
SET [o].[Title] = N'SomeValue'
52+
FROM [Owner] AS [o]
53+
""");
54+
}
55+
4456
public override async Task Delete_predicate_based_on_optional_navigation(bool async)
4557
{
4658
await base.Delete_predicate_based_on_optional_navigation(async);

0 commit comments

Comments
 (0)