Skip to content

Commit eef5adf

Browse files
committed
Implement ExecuteUpdate support for complex JSON
Closes #28776
1 parent 7ed4c25 commit eef5adf

File tree

40 files changed

+2238
-894
lines changed

40 files changed

+2238
-894
lines changed

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

Lines changed: 14 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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,9 @@
979979
<data name="MissingResultSetWhenSaving" xml:space="preserve">
980980
<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>
981981
</data>
982+
<data name="MultipleColumnsWithSameJsonContainerName" xml:space="preserve">
983+
<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>
984+
</data>
982985
<data name="ModificationCommandBatchAlreadyComplete" xml:space="preserve">
983986
<value>Commands cannot be added to a completed 'ModificationCommandBatch'.</value>
984987
</data>
@@ -1072,6 +1075,9 @@
10721075
<data name="ParameterNotObjectArray" xml:space="preserve">
10731076
<value>The value provided for parameter '{parameter}' cannot be used because it isn't assignable to type 'object[]'.</value>
10741077
</data>
1078+
<data name="JsonPartialUpdateNotSupportedByProvider" xml:space="preserve">
1079+
<value>The provider in use does not support partial updates within JSON columns.</value>
1080+
</data>
10751081
<data name="PendingAmbientTransaction" xml:space="preserve">
10761082
<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>
10771083
</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;

0 commit comments

Comments
 (0)