Skip to content

Commit 9a34953

Browse files
authored
Implement ExecuteUpdate support for complex JSON (#36659)
And introduce new TypeTests Closes #28766
1 parent 0acfafc commit 9a34953

File tree

61 files changed

+4155
-1262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4155
-1262
lines changed

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

Lines changed: 40 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: 18 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>
@@ -433,6 +436,12 @@
433436
<data name="ExecuteOperationWithUnsupportedOperatorInSqlGeneration" xml:space="preserve">
434437
<value>The operation '{operation}' contains a select expression feature that isn't supported in the query SQL generator, but has been declared as supported by provider during translation phase. This is a bug in your EF Core provider, file an issue at https://aka.ms/efcorefeedback.</value>
435438
</data>
439+
<data name="ExecuteUpdateCannotSetJsonPropertyToNonJsonColumn" xml:space="preserve">
440+
<value>'ExecuteUpdate' cannot currently set a property in a JSON column to a regular, non-JSON column; see https://github.com/dotnet/efcore/issues/36688.</value>
441+
</data>
442+
<data name="ExecuteUpdateCannotSetJsonPropertyToArbitraryExpression" xml:space="preserve">
443+
<value>'ExecuteUpdate' cannot currently set a property in a JSON column to arbitrary expressions; only constants, parameters and other JSON properties are supported; see https://github.com/dotnet/efcore/issues/36688.</value>
444+
</data>
436445
<data name="ExecuteUpdateDeleteOnEntityNotMappedToTable" xml:space="preserve">
437446
<value>'ExecuteUpdate' or 'ExecuteDelete' was called on entity type '{entityType}', but that entity type is not mapped to a table.</value>
438447
</data>
@@ -979,6 +988,9 @@
979988
<data name="MissingResultSetWhenSaving" xml:space="preserve">
980989
<value>A result set was missing when reading the results of a SaveChanges operation; this may indicate that a stored procedure was configured to return results in the EF model, but did not. Check your stored procedure definitions.</value>
981990
</data>
991+
<data name="MultipleColumnsWithSameJsonContainerName" xml:space="preserve">
992+
<value>Entity type '{entityType}' is mapped to multiple columns with name '{columnName}', and one of them is configured as a JSON column. Assign different names to the columns.</value>
993+
</data>
982994
<data name="ModificationCommandBatchAlreadyComplete" xml:space="preserve">
983995
<value>Commands cannot be added to a completed 'ModificationCommandBatch'.</value>
984996
</data>
@@ -1072,6 +1084,12 @@
10721084
<data name="ParameterNotObjectArray" xml:space="preserve">
10731085
<value>The value provided for parameter '{parameter}' cannot be used because it isn't assignable to type 'object[]'.</value>
10741086
</data>
1087+
<data name="JsonPartialExecuteUpdateNotSupportedByProvider" xml:space="preserve">
1088+
<value>The provider in use does not support partial updates with ExecuteUpdate within JSON columns.</value>
1089+
</data>
1090+
<data name="JsonExecuteUpdateNotSupportedWithOwnedEntities" xml:space="preserve">
1091+
<value>ExecuteUpdate over JSON columns is not supported when the column is mapped as an owned entity. Map the column as a complex type instead.</value>
1092+
</data>
10751093
<data name="PendingAmbientTransaction" xml:space="preserve">
10761094
<value>This connection was used with an ambient transaction. The original ambient transaction needs to be completed before this connection can be used outside of it.</value>
10771095
</data>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 System.Collections;
5+
using System.Text;
6+
using System.Text.Json;
7+
8+
namespace Microsoft.EntityFrameworkCore.Query.Internal;
9+
10+
/// <summary>
11+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
12+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
13+
/// any release. You should only use it directly in your code with extreme caution and knowing that
14+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
15+
/// </summary>
16+
public static class RelationalJsonUtilities
17+
{
18+
/// <summary>
19+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
20+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
21+
/// any release. You should only use it directly in your code with extreme caution and knowing that
22+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
23+
/// </summary>
24+
public static readonly MethodInfo SerializeComplexTypeToJsonMethod =
25+
typeof(RelationalJsonUtilities).GetTypeInfo().GetDeclaredMethod(nameof(SerializeComplexTypeToJson))!;
26+
27+
/// <summary>
28+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
29+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
30+
/// any release. You should only use it directly in your code with extreme caution and knowing that
31+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
32+
/// </summary>
33+
public static string? SerializeComplexTypeToJson(IComplexType complexType, object? value, bool collection)
34+
{
35+
// Note that we treat toplevel null differently: we return a relational NULL for that case. For nested nulls,
36+
// we return JSON null string (so you get { "foo": null })
37+
if (value is null)
38+
{
39+
return null;
40+
}
41+
42+
var stream = new MemoryStream();
43+
var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false });
44+
45+
WriteJson(writer, complexType, value, collection);
46+
47+
writer.Flush();
48+
49+
return Encoding.UTF8.GetString(stream.ToArray());
50+
51+
void WriteJson(Utf8JsonWriter writer, IComplexType complexType, object? value, bool collection)
52+
{
53+
if (collection)
54+
{
55+
if (value is null)
56+
{
57+
writer.WriteNullValue();
58+
59+
return;
60+
}
61+
62+
writer.WriteStartArray();
63+
64+
foreach (var element in (IEnumerable)value)
65+
{
66+
WriteJsonObject(writer, complexType, element);
67+
}
68+
69+
writer.WriteEndArray();
70+
return;
71+
}
72+
73+
WriteJsonObject(writer, complexType, value);
74+
}
75+
76+
void WriteJsonObject(Utf8JsonWriter writer, IComplexType complexType, object? objectValue)
77+
{
78+
if (objectValue is null)
79+
{
80+
writer.WriteNullValue();
81+
return;
82+
}
83+
84+
writer.WriteStartObject();
85+
86+
foreach (var property in complexType.GetProperties())
87+
{
88+
var jsonPropertyName = property.GetJsonPropertyName();
89+
Check.DebugAssert(jsonPropertyName is not null);
90+
writer.WritePropertyName(jsonPropertyName);
91+
92+
var propertyValue = property.GetGetter().GetClrValue(objectValue);
93+
if (propertyValue is null)
94+
{
95+
writer.WriteNullValue();
96+
}
97+
else
98+
{
99+
var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter;
100+
Check.DebugAssert(jsonValueReaderWriter is not null, "Missing JsonValueReaderWriter on JSON property");
101+
jsonValueReaderWriter.ToJson(writer, propertyValue);
102+
}
103+
}
104+
105+
foreach (var complexProperty in complexType.GetComplexProperties())
106+
{
107+
var jsonPropertyName = complexProperty.GetJsonPropertyName();
108+
Check.DebugAssert(jsonPropertyName is not null);
109+
writer.WritePropertyName(jsonPropertyName);
110+
111+
var propertyValue = complexProperty.GetGetter().GetClrValue(objectValue);
112+
113+
WriteJson(writer, complexProperty.ComplexType, propertyValue, complexProperty.IsCollection);
114+
}
115+
116+
writer.WriteEndObject();
117+
}
118+
}
119+
}

src/EFCore.Relational/Query/QuerySqlGenerator.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,20 +1459,33 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
14591459
|| selectExpression.Tables[1] is CrossJoinExpression))
14601460
{
14611461
_relationalCommandBuilder.Append("UPDATE ");
1462+
14621463
Visit(updateExpression.Table);
1464+
14631465
_relationalCommandBuilder.AppendLine();
14641466
_relationalCommandBuilder.Append("SET ");
1465-
_relationalCommandBuilder.Append(
1466-
$"{_sqlGenerationHelper.DelimitIdentifier(updateExpression.ColumnValueSetters[0].Column.Name)} = ");
1467-
Visit(updateExpression.ColumnValueSetters[0].Value);
1468-
using (_relationalCommandBuilder.Indent())
1467+
1468+
for (var i = 0; i < updateExpression.ColumnValueSetters.Count; i++)
14691469
{
1470-
foreach (var columnValueSetter in updateExpression.ColumnValueSetters.Skip(1))
1470+
if (i == 1)
1471+
{
1472+
Sql.IncrementIndent();
1473+
}
1474+
1475+
if (i > 0)
14711476
{
14721477
_relationalCommandBuilder.AppendLine(",");
1473-
_relationalCommandBuilder.Append($"{_sqlGenerationHelper.DelimitIdentifier(columnValueSetter.Column.Name)} = ");
1474-
Visit(columnValueSetter.Value);
14751478
}
1479+
1480+
var (column, value) = updateExpression.ColumnValueSetters[i];
1481+
1482+
_relationalCommandBuilder.Append(_sqlGenerationHelper.DelimitIdentifier(column.Name)).Append(" = ");
1483+
Visit(value);
1484+
}
1485+
1486+
if (updateExpression.ColumnValueSetters.Count > 1)
1487+
{
1488+
Sql.DecrementIndent();
14761489
}
14771490

14781491
var predicate = selectExpression.Predicate;

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

0 commit comments

Comments
 (0)