Skip to content

Commit fbc493a

Browse files
committed
Support interfaces and entities with owned types in ExecuteUpdate setter property lambda (dotnet#29672)
Fixes dotnet#29618 Fixes dotnet#28727 (cherry picked from commit d4be66b)
1 parent ce1392a commit fbc493a

12 files changed

+299
-9
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

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

21+
private static readonly bool QuirkEnabled28727
22+
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue28727", out var enabled) && enabled;
23+
2124
/// <summary>
2225
/// Creates a new instance of the <see cref="QueryableMethodTranslatingExpressionVisitor" /> class.
2326
/// </summary>
@@ -1178,11 +1181,25 @@ static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
11781181
foreach (var (propertyExpression, _) in propertyValueLambdaExpressions)
11791182
{
11801183
var left = RemapLambdaBody(source, propertyExpression);
1181-
left = left.UnwrapTypeConversion(out _);
1182-
if (!IsValidPropertyAccess(RelationalDependencies.Model, left, out var ese))
1184+
1185+
EntityShaperExpression? ese;
1186+
1187+
if (QuirkEnabled28727)
11831188
{
1184-
AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertyExpression.Print()));
1185-
return null;
1189+
left = left.UnwrapTypeConversion(out _);
1190+
if (!IsValidPropertyAccess(RelationalDependencies.Model, left, out ese))
1191+
{
1192+
AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertyExpression.Print()));
1193+
return null;
1194+
}
1195+
}
1196+
else
1197+
{
1198+
if (!TryProcessPropertyAccess(RelationalDependencies.Model, ref left, out ese))
1199+
{
1200+
AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertyExpression.Print()));
1201+
return null;
1202+
}
11861203
}
11871204

11881205
if (entityShaperExpression is null)
@@ -1382,6 +1399,66 @@ when methodCallExpression.Method.IsGenericMethod
13821399
}
13831400
}
13841401

1402+
// 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).
1405+
static bool TryProcessPropertyAccess(
1406+
IModel model,
1407+
ref Expression expression,
1408+
[NotNullWhen(true)] out EntityShaperExpression? entityShaperExpression)
1409+
{
1410+
expression = expression.UnwrapTypeConversion(out _);
1411+
1412+
if (expression is MemberExpression { Expression : not null } memberExpression
1413+
&& Unwrap(memberExpression.Expression) is EntityShaperExpression ese)
1414+
{
1415+
expression = memberExpression.Update(ese);
1416+
entityShaperExpression = ese;
1417+
return true;
1418+
}
1419+
1420+
if (expression is MethodCallExpression mce)
1421+
{
1422+
if (mce.TryGetEFPropertyArguments(out var source, out _)
1423+
&& Unwrap(source) is EntityShaperExpression ese1)
1424+
{
1425+
if (source != ese1)
1426+
{
1427+
var rewrittenArguments = mce.Arguments.ToArray();
1428+
rewrittenArguments[0] = ese1;
1429+
expression = mce.Update(mce.Object, rewrittenArguments);
1430+
}
1431+
1432+
entityShaperExpression = ese1;
1433+
return true;
1434+
}
1435+
1436+
if (mce.TryGetIndexerArguments(model, out var source2, out _)
1437+
&& Unwrap(source2) is EntityShaperExpression ese2)
1438+
{
1439+
expression = mce.Update(ese2, mce.Arguments);
1440+
entityShaperExpression = ese2;
1441+
return true;
1442+
}
1443+
}
1444+
1445+
entityShaperExpression = null;
1446+
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+
}
1459+
}
1460+
1461+
// Old quirked implementation only
13851462
static bool IsValidPropertyAccess(
13861463
IModel model,
13871464
Expression expression,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,26 @@ public virtual Task Update_where_keyless_entity_mapped_to_sql_query(bool async)
158158
s => s.SetProperty(e => e.Name, "Eagle"),
159159
rowsAffectedCount: 1));
160160

161+
[ConditionalTheory]
162+
[MemberData(nameof(IsAsyncData))]
163+
public virtual Task Update_with_interface_in_property_expression(bool async)
164+
=> AssertUpdate(
165+
async,
166+
ss => ss.Set<Coke>(),
167+
e => e,
168+
s => s.SetProperty(c => ((ISugary)c).SugarGrams, 0),
169+
rowsAffectedCount: 1);
170+
171+
[ConditionalTheory]
172+
[MemberData(nameof(IsAsyncData))]
173+
public virtual Task Update_with_interface_in_EF_Property_in_property_expression(bool async)
174+
=> AssertUpdate(
175+
async,
176+
ss => ss.Set<Coke>(),
177+
e => e,
178+
// ReSharper disable once RedundantCast
179+
s => s.SetProperty(c => EF.Property<int>((ISugary)c, nameof(ISugary.SugarGrams)), 0),
180+
rowsAffectedCount: 1);
181+
161182
protected abstract void ClearLog();
162183
}

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.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,14 @@ public override Task Update_where_hierarchy_derived(bool async)
5656
=> AssertTranslationFailed(
5757
RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Kiwi"),
5858
() => base.Update_where_hierarchy_derived(async));
59+
60+
public override Task Update_with_interface_in_property_expression(bool async)
61+
=> AssertTranslationFailed(
62+
RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Coke"),
63+
() => base.Update_with_interface_in_property_expression(async));
64+
65+
public override Task Update_with_interface_in_EF_Property_in_property_expression(bool async)
66+
=> AssertTranslationFailed(
67+
RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Coke"),
68+
() => base.Update_with_interface_in_EF_Property_in_property_expression(async));
5969
}

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates;
55

66
public class InheritanceBulkUpdatesSqlServerTest : InheritanceBulkUpdatesTestBase<InheritanceBulkUpdatesSqlServerFixture>
77
{
8-
public InheritanceBulkUpdatesSqlServerTest(InheritanceBulkUpdatesSqlServerFixture fixture)
8+
public InheritanceBulkUpdatesSqlServerTest(
9+
InheritanceBulkUpdatesSqlServerFixture fixture,
10+
ITestOutputHelper testOutputHelper)
11+
912
: base(fixture)
1013
{
1114
ClearLog();
15+
// Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
1216
}
1317

1418
[ConditionalFact]
@@ -205,6 +209,32 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool
205209
AssertExecuteUpdateSql();
206210
}
207211

212+
public override async Task Update_with_interface_in_property_expression(bool async)
213+
{
214+
await base.Update_with_interface_in_property_expression(async);
215+
216+
AssertExecuteUpdateSql(
217+
"""
218+
UPDATE [d]
219+
SET [d].[SugarGrams] = 0
220+
FROM [Drinks] AS [d]
221+
WHERE [d].[Discriminator] = N'Coke'
222+
""");
223+
}
224+
225+
public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async)
226+
{
227+
await base.Update_with_interface_in_EF_Property_in_property_expression(async);
228+
229+
AssertExecuteUpdateSql(
230+
"""
231+
UPDATE [d]
232+
SET [d].[SugarGrams] = 0
233+
FROM [Drinks] AS [d]
234+
WHERE [d].[Discriminator] = N'Coke'
235+
""");
236+
}
237+
208238
protected override void ClearLog()
209239
=> Fixture.TestSqlLoggerFactory.Clear();
210240

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);

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates;
55

66
public class TPCInheritanceBulkUpdatesSqlServerTest : TPCInheritanceBulkUpdatesTestBase<TPCInheritanceBulkUpdatesSqlServerFixture>
77
{
8-
public TPCInheritanceBulkUpdatesSqlServerTest(TPCInheritanceBulkUpdatesSqlServerFixture fixture)
8+
public TPCInheritanceBulkUpdatesSqlServerTest(
9+
TPCInheritanceBulkUpdatesSqlServerFixture fixture,
10+
ITestOutputHelper testOutputHelper)
911
: base(fixture)
1012
{
1113
ClearLog();
14+
// Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
1215
}
1316

1417
[ConditionalFact]
@@ -176,6 +179,30 @@ FROM [Kiwi] AS [k]
176179
""");
177180
}
178181

182+
public override async Task Update_with_interface_in_property_expression(bool async)
183+
{
184+
await base.Update_with_interface_in_property_expression(async);
185+
186+
AssertExecuteUpdateSql(
187+
"""
188+
UPDATE [c]
189+
SET [c].[SugarGrams] = 0
190+
FROM [Coke] AS [c]
191+
""");
192+
}
193+
194+
public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async)
195+
{
196+
await base.Update_with_interface_in_EF_Property_in_property_expression(async);
197+
198+
AssertExecuteUpdateSql(
199+
"""
200+
UPDATE [c]
201+
SET [c].[SugarGrams] = 0
202+
FROM [Coke] AS [c]
203+
""");
204+
}
205+
179206
public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async)
180207
{
181208
await base.Update_where_keyless_entity_mapped_to_sql_query(async);

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool
144144
AssertExecuteUpdateSql();
145145
}
146146

147+
public override async Task Update_with_interface_in_property_expression(bool async)
148+
{
149+
await base.Update_with_interface_in_property_expression(async);
150+
151+
AssertExecuteUpdateSql();
152+
}
153+
154+
public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async)
155+
{
156+
await base.Update_with_interface_in_EF_Property_in_property_expression(async);
157+
158+
AssertExecuteUpdateSql();
159+
}
160+
147161
protected override void ClearLog()
148162
=> Fixture.TestSqlLoggerFactory.Clear();
149163

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates;
55

66
public class InheritanceBulkUpdatesSqliteTest : InheritanceBulkUpdatesTestBase<InheritanceBulkUpdatesSqliteFixture>
77
{
8-
public InheritanceBulkUpdatesSqliteTest(InheritanceBulkUpdatesSqliteFixture fixture)
8+
public InheritanceBulkUpdatesSqliteTest(
9+
InheritanceBulkUpdatesSqliteFixture fixture,
10+
ITestOutputHelper testOutputHelper)
911
: base(fixture)
1012
{
1113
ClearLog();
14+
// Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
1215
}
1316

1417
[ConditionalFact]
@@ -196,6 +199,30 @@ public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool
196199
AssertExecuteUpdateSql();
197200
}
198201

202+
public override async Task Update_with_interface_in_property_expression(bool async)
203+
{
204+
await base.Update_with_interface_in_property_expression(async);
205+
206+
AssertExecuteUpdateSql(
207+
"""
208+
UPDATE "Drinks" AS "d"
209+
SET "SugarGrams" = 0
210+
WHERE "d"."Discriminator" = 'Coke'
211+
""");
212+
}
213+
214+
public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async)
215+
{
216+
await base.Update_with_interface_in_EF_Property_in_property_expression(async);
217+
218+
AssertExecuteUpdateSql(
219+
"""
220+
UPDATE "Drinks" AS "d"
221+
SET "SugarGrams" = 0
222+
WHERE "d"."Discriminator" = 'Coke'
223+
""");
224+
}
225+
199226
protected override void ClearLog()
200227
=> Fixture.TestSqlLoggerFactory.Clear();
201228

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ public override async Task Delete_aggregate_root_when_table_sharing_with_non_own
3939
AssertSql();
4040
}
4141

42+
public override async Task Update_non_owned_property_on_entity_with_owned(bool async)
43+
{
44+
await base.Update_non_owned_property_on_entity_with_owned(async);
45+
46+
AssertSql(
47+
"""
48+
UPDATE "Owner" AS "o"
49+
SET "Title" = 'SomeValue'
50+
""");
51+
}
52+
4253
public override async Task Delete_predicate_based_on_optional_navigation(bool async)
4354
{
4455
await base.Delete_predicate_based_on_optional_navigation(async);

0 commit comments

Comments
 (0)