Skip to content

Commit 2af7eb1

Browse files
committed
Throw proper exceptions for ExecuteUpdate on owned JSON
1 parent 78f22f8 commit 2af7eb1

File tree

7 files changed

+117
-11
lines changed

7 files changed

+117
-11
lines changed

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,9 @@
424424
<data name="ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator" xml:space="preserve">
425425
<value>The operation '{operation}' cannot be performed on keyless entity type '{entityType}', since it contains an operator not natively supported by the database provider.</value>
426426
</data>
427+
<data name="ExecuteOperationOnOwnedJsonIsNotSupported" xml:space="preserve">
428+
<value>'{operation}' used over owned type '{entityType}' which is mapped to JSON; '{operation}' on JSON-mapped owned entities is not supported. Consider mapping your type as a complex type instead.</value>
429+
</data>
427430
<data name="ExecuteOperationOnTPC" xml:space="preserve">
428431
<value>The operation '{operation}' is being applied on entity type '{entityType}', which is using the TPC mapping strategy and is not a leaf type. 'ExecuteDelete'/'ExecuteUpdate' operations on entity types participating in TPC hierarchies is only supported for leaf types.</value>
429432
</data>

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteDelete.cs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,27 @@ public partial class RelationalQueryableMethodTranslatingExpressionVisitor
1919
return null;
2020
}
2121

22-
var mappingStrategy = entityType.GetMappingStrategy();
23-
if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy)
22+
if (entityType.IsMappedToJson())
2423
{
2524
AddTranslationErrorDetails(
26-
RelationalStrings.ExecuteOperationOnTPT(
27-
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
25+
RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteDelete", entityType.DisplayName()));
2826
return null;
2927
}
3028

31-
if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy
32-
&& entityType.GetDirectlyDerivedTypes().Any())
29+
switch (entityType.GetMappingStrategy())
3330
{
34-
// We allow TPC is it is leaf type
35-
AddTranslationErrorDetails(
36-
RelationalStrings.ExecuteOperationOnTPC(
37-
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
38-
return null;
31+
case RelationalAnnotationNames.TptMappingStrategy:
32+
AddTranslationErrorDetails(
33+
RelationalStrings.ExecuteOperationOnTPT(
34+
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
35+
return null;
36+
37+
// Note that we do allow TPC if the target is a leaf type
38+
case RelationalAnnotationNames.TpcMappingStrategy when entityType.GetDirectlyDerivedTypes().Any():
39+
AddTranslationErrorDetails(
40+
RelationalStrings.ExecuteOperationOnTPC(
41+
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
42+
return null;
3943
}
4044

4145
// Find the table model that maps to the entity type; there must be exactly one (e.g. no entity splitting).

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.EntityFrameworkCore.Metadata.Internal;
56
using Microsoft.EntityFrameworkCore.Query.Internal;
67
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
78
using Microsoft.EntityFrameworkCore.Storage.Json;
@@ -279,6 +280,12 @@ protected virtual bool TryTranslateSetters(
279280
return false;
280281
}
281282

283+
if (targetProperty.DeclaringType is IEntityType entityType && entityType.IsMappedToJson())
284+
{
285+
AddTranslationErrorDetails(RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteUpdate", entityType.DisplayName()));
286+
return false;
287+
}
288+
282289
// Hack: when returning a StructuralTypeShaperExpression, _sqlTranslator returns it wrapped by a
283290
// StructuralTypeReferenceExpression, which is supposed to be a private wrapper only with the SQL translator.
284291
// Call TranslateProjection to unwrap it (need to look into getting rid StructuralTypeReferenceExpression altogether).
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.BulkUpdates;
5+
6+
namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson;
7+
8+
public abstract class OwnedJsonBulkUpdateRelationalTestBase<TFixture> : BulkUpdatesTestBase<TFixture>
9+
where TFixture : OwnedJsonRelationalFixtureBase, new()
10+
{
11+
public OwnedJsonBulkUpdateRelationalTestBase(TFixture fixture, ITestOutputHelper testOutputHelper)
12+
: base(fixture)
13+
{
14+
fixture.TestSqlLoggerFactory.Clear();
15+
fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
16+
}
17+
18+
// Bulk update is not supported with owned JSON.
19+
// We just have a couple of tests here to verify that the correct exceptions are thrown, and don't extend
20+
// the actual AssociationsBulkUpdateTestBase with all the different tests.
21+
22+
[ConditionalFact]
23+
public virtual Task Delete_association()
24+
=> AssertTranslationFailedWithDetails(
25+
RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteDelete", "RootEntity.RequiredRelated#RelatedType"),
26+
() => AssertDelete(
27+
ss => ss.Set<RootEntity>().Select(c => c.RequiredRelated),
28+
rowsAffectedCount: 0));
29+
30+
[ConditionalFact]
31+
public virtual Task Update_property_inside_association()
32+
=> AssertTranslationFailedWithDetails(
33+
RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteUpdate", "RootEntity.RequiredRelated#RelatedType"),
34+
() => AssertUpdate(
35+
ss => ss.Set<RootEntity>(),
36+
e => e,
37+
s => s.SetProperty(c => c.RequiredRelated.String, "foo_updated"),
38+
rowsAffectedCount: 0));
39+
40+
[ConditionalFact]
41+
public virtual async Task Update_association()
42+
{
43+
var newNested = new NestedType
44+
{
45+
Name = "Updated nested name",
46+
Int = 80,
47+
String = "Updated nested string"
48+
};
49+
50+
await AssertTranslationFailedWithDetails(
51+
RelationalStrings.InvalidPropertyInSetProperty("x => x.RequiredRelated.RequiredNested"),
52+
// RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteUpdate", "RootEntity.RequiredRelated#RelatedType"),
53+
() => AssertUpdate(
54+
ss => ss.Set<RootEntity>(),
55+
c => c,
56+
s => s.SetProperty(x => x.RequiredRelated.RequiredNested, newNested),
57+
rowsAffectedCount: 0));
58+
}
59+
60+
protected static async Task AssertTranslationFailedWithDetails(string details, Func<Task> query)
61+
{
62+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(query);
63+
Assert.Contains(CoreStrings.NonQueryTranslationFailedWithDetails("", details)[21..], exception.Message);
64+
}
65+
}
66+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson;
5+
6+
public class OwnedJsonBulkUpdateSqlServerTest(
7+
OwnedJsonSqlServerFixture fixture,
8+
ITestOutputHelper testOutputHelper)
9+
: OwnedJsonBulkUpdateRelationalTestBase<OwnedJsonSqlServerFixture>(fixture, testOutputHelper);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson;
5+
6+
public class OwnedJsonBulkUpdateSqliteTest(
7+
OwnedJsonSqliteFixture fixture,
8+
ITestOutputHelper testOutputHelper)
9+
: OwnedJsonBulkUpdateRelationalTestBase<OwnedJsonSqliteFixture>(fixture, testOutputHelper);

0 commit comments

Comments
 (0)