Skip to content

Commit 83102c7

Browse files
committed
Experimental support for the Azure SQL json type
Fixes #28452 Fixes #32150 Remaining work: - Test reverse engineering from an existing database - Output a warning when the native JSON type is used - Replace the ToJson overload with HasColumnType() - Move the type mapping visitation to another visitor Known issues: - Various issues communicated with the SQL team--see TODO:SQLJSON - Testing is disabled until we have an appropriate server and driver to test against
1 parent ee2cf41 commit 83102c7

27 files changed

+9305
-778
lines changed

NuGet.config

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<configuration>
33
<packageSources>
44
<clear />
5+
<add key="localnuget" value="C:\tempnuget" />
56
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
67
<!-- Begin: Package sources from dotnet-runtime -->
78
<!-- End: Package sources from dotnet-runtime -->

src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs

+40
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,46 @@ public static void SetContainerColumnName(this IMutableEntityType entityType, st
16101610
? columnName
16111611
: (entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnName());
16121612

1613+
/// <summary>
1614+
/// Sets the column type to use for the container column to which the entity type is mapped.
1615+
/// </summary>
1616+
/// <param name="entityType">The entity type.</param>
1617+
/// <param name="columnType">The database column type.</param>
1618+
public static void SetContainerColumnType(this IMutableEntityType entityType, string? columnType)
1619+
=> entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.ContainerColumnType, columnType);
1620+
1621+
/// <summary>
1622+
/// Sets the column type to use for the container column to which the entity type is mapped.
1623+
/// </summary>
1624+
/// <param name="entityType">The entity type.</param>
1625+
/// <param name="columnType">The database column type.</param>
1626+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
1627+
/// <returns>The configured value.</returns>
1628+
public static string? SetContainerColumnType(
1629+
this IConventionEntityType entityType,
1630+
string? columnType,
1631+
bool fromDataAnnotation = false)
1632+
=> (string?)entityType.SetAnnotation(RelationalAnnotationNames.ContainerColumnType, columnType, fromDataAnnotation)?.Value;
1633+
1634+
/// <summary>
1635+
/// Gets the <see cref="ConfigurationSource" /> for the container column type.
1636+
/// </summary>
1637+
/// <param name="entityType">The entity type.</param>
1638+
/// <returns>The <see cref="ConfigurationSource" />.</returns>
1639+
public static ConfigurationSource? GetContainerColumnTypeConfigurationSource(this IConventionEntityType entityType)
1640+
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType)
1641+
?.GetConfigurationSource();
1642+
1643+
/// <summary>
1644+
/// Gets the column type to use for the container column to which the entity type is mapped.
1645+
/// </summary>
1646+
/// <param name="entityType">The entity type.</param>
1647+
/// <returns>The database column type.</returns>
1648+
public static string? GetContainerColumnType(this IReadOnlyEntityType entityType)
1649+
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType)?.Value is string columnType
1650+
? columnType
1651+
: (entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnType());
1652+
16131653
/// <summary>
16141654
/// Sets the type mapping for the container column to which the entity type is mapped.
16151655
/// </summary>

src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs

+42-17
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,7 @@ public static class RelationalOwnedNavigationBuilderExtensions
2323
/// <param name="builder">The builder for the owned navigation being configured.</param>
2424
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
2525
public static OwnedNavigationBuilder ToJson(this OwnedNavigationBuilder builder)
26-
{
27-
var navigationName = builder.Metadata.GetNavigation(pointsToPrincipal: false)!.Name;
28-
builder.ToJson(navigationName);
29-
30-
return builder;
31-
}
26+
=> builder.ToJson(builder.Metadata.GetNavigation(pointsToPrincipal: false)!.Name);
3227

3328
/// <summary>
3429
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
@@ -45,12 +40,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwn
4540
this OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> builder)
4641
where TOwnerEntity : class
4742
where TDependentEntity : class
48-
{
49-
var navigationName = builder.Metadata.GetNavigation(pointsToPrincipal: false)!.Name;
50-
builder.ToJson(navigationName);
51-
52-
return builder;
53-
}
43+
=> (OwnedNavigationBuilder<TOwnerEntity, TDependentEntity>)((OwnedNavigationBuilder)builder).ToJson();
5444

5545
/// <summary>
5646
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
@@ -68,11 +58,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwn
6858
string? jsonColumnName)
6959
where TOwnerEntity : class
7060
where TDependentEntity : class
71-
{
72-
builder.OwnedEntityType.SetContainerColumnName(jsonColumnName);
73-
74-
return builder;
75-
}
61+
=> builder.ToJson(jsonColumnName, null);
7662

7763
/// <summary>
7864
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
@@ -88,8 +74,47 @@ public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwn
8874
public static OwnedNavigationBuilder ToJson(
8975
this OwnedNavigationBuilder builder,
9076
string? jsonColumnName)
77+
=> builder.ToJson(jsonColumnName, null);
78+
79+
/// <summary>
80+
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
81+
/// </summary>
82+
/// <remarks>
83+
/// This method should only be specified for the outer-most owned entity in the given ownership structure.
84+
/// All entities owned by this will be automatically mapped to the same JSON column.
85+
/// The ownerships must still be explicitly defined.
86+
/// </remarks>
87+
/// <param name="builder">The builder for the owned navigation being configured.</param>
88+
/// <param name="jsonColumnName">JSON column name to use.</param>
89+
/// <param name="jsonColumnType">The database type for the JSON column, or <see langword="null"/> to use the database default.</param>
90+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
91+
public static OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> ToJson<TOwnerEntity, TDependentEntity>(
92+
this OwnedNavigationBuilder<TOwnerEntity, TDependentEntity> builder,
93+
string? jsonColumnName,
94+
string? jsonColumnType)
95+
where TOwnerEntity : class
96+
where TDependentEntity : class
97+
=> (OwnedNavigationBuilder<TOwnerEntity, TDependentEntity>)((OwnedNavigationBuilder)builder).ToJson(jsonColumnName, jsonColumnType);
98+
99+
/// <summary>
100+
/// Configures a relationship where this entity type and the entities that it owns are mapped to a JSON column in the database.
101+
/// </summary>
102+
/// <remarks>
103+
/// This method should only be specified for the outer-most owned entity in the given ownership structure.
104+
/// All entities owned by this will be automatically mapped to the same JSON column.
105+
/// The ownerships must still be explicitly defined.
106+
/// </remarks>
107+
/// <param name="builder">The builder for the owned navigation being configured.</param>
108+
/// <param name="jsonColumnName">JSON column name to use.</param>
109+
/// <param name="jsonColumnType">The database type for the JSON column, or <see langword="null"/> to use the database default.</param>
110+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
111+
public static OwnedNavigationBuilder ToJson(
112+
this OwnedNavigationBuilder builder,
113+
string? jsonColumnName,
114+
string? jsonColumnType)
91115
{
92116
builder.OwnedEntityType.SetContainerColumnName(jsonColumnName);
117+
builder.OwnedEntityType.SetContainerColumnType(jsonColumnType);
93118

94119
return builder;
95120
}

src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs

+11
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,17 @@ public static bool IsMappedToJson(this IReadOnlyTypeBase typeBase)
368368
? entityType.GetContainerColumnName()
369369
: ((IReadOnlyComplexType)typeBase).GetContainerColumnName();
370370

371+
372+
/// <summary>
373+
/// Gets the column type to use for the container column to which the type is mapped.
374+
/// </summary>
375+
/// <param name="typeBase">The type.</param>
376+
/// <returns>The database column type.</returns>
377+
public static string? GetContainerColumnType(this IReadOnlyTypeBase typeBase)
378+
=> typeBase is IReadOnlyEntityType entityType
379+
? entityType.GetContainerColumnType()
380+
: null;
381+
371382
/// <summary>
372383
/// Gets the value of JSON property name used for the given entity mapped to a JSON column.
373384
/// </summary>

src/EFCore.Relational/Metadata/Internal/RelationalModel.cs

+14-9
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,14 @@ private static void AddDefaultMappings(
286286
includesDerivedTypes: entityType.GetDirectlyDerivedTypes().Any()
287287
? !isTpc && mappedType == entityType
288288
: null);
289+
289290
var containerColumnName = mappedType.GetContainerColumnName();
291+
var containerColumnType = mappedType.GetContainerColumnType();
290292
if (!string.IsNullOrEmpty(containerColumnName))
291293
{
292294
CreateContainerColumn(
293-
defaultTable, containerColumnName, mappedType, relationalTypeMappingSource,
294-
static (c, t, m) => new JsonColumnBase(c, m.StoreType, t, m));
295+
defaultTable, containerColumnName, containerColumnType, mappedType, relationalTypeMappingSource,
296+
static (colName, colType, table, mapping) => new JsonColumnBase(colName, colType ?? mapping.StoreType, table, mapping));
295297
}
296298
else
297299
{
@@ -492,11 +494,12 @@ private static void CreateTableMapping(
492494
};
493495

494496
var containerColumnName = mappedType.GetContainerColumnName();
497+
var containerColumnType = mappedType.GetContainerColumnType();
495498
if (!string.IsNullOrEmpty(containerColumnName))
496499
{
497500
CreateContainerColumn(
498-
table, containerColumnName, (IEntityType)mappedType, relationalTypeMappingSource,
499-
static (c, t, m) => new JsonColumn(c, m.StoreType, (Table)t, m));
501+
table, containerColumnName, containerColumnType, (IEntityType)mappedType, relationalTypeMappingSource,
502+
static (colName, colType, table, mapping) => new JsonColumn(colName, colType ?? mapping.StoreType, (Table)table, mapping));
500503
}
501504
else
502505
{
@@ -567,18 +570,19 @@ private static void CreateTableMapping(
567570
private static void CreateContainerColumn<TColumnMappingBase>(
568571
TableBase tableBase,
569572
string containerColumnName,
573+
string? containerColumnType,
570574
IEntityType mappedType,
571575
IRelationalTypeMappingSource relationalTypeMappingSource,
572-
Func<string, TableBase, RelationalTypeMapping, ColumnBase<TColumnMappingBase>> createColumn)
576+
Func<string, string?, TableBase, RelationalTypeMapping, ColumnBase<TColumnMappingBase>> createColumn)
573577
where TColumnMappingBase : class, IColumnMappingBase
574578
{
575579
var ownership = mappedType.GetForeignKeys().Single(fk => fk.IsOwnership);
576580
if (!ownership.PrincipalEntityType.IsMappedToJson())
577581
{
578582
Check.DebugAssert(tableBase.FindColumn(containerColumnName) == null, $"Table does not have column '{containerColumnName}'.");
579583

580-
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement), mappedType.Model)!;
581-
var jsonColumn = createColumn(containerColumnName, tableBase, jsonColumnTypeMapping);
584+
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement), storeTypeName: containerColumnType)!;
585+
var jsonColumn = createColumn(containerColumnName, containerColumnType, tableBase, jsonColumnTypeMapping);
582586
tableBase.Columns.Add(containerColumnName, jsonColumn);
583587
jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique;
584588

@@ -684,11 +688,12 @@ private static void CreateViewMapping(
684688
};
685689

686690
var containerColumnName = mappedType.GetContainerColumnName();
691+
var containerColumnType = mappedType.GetContainerColumnType();
687692
if (!string.IsNullOrEmpty(containerColumnName))
688693
{
689694
CreateContainerColumn(
690-
view, containerColumnName, mappedType, relationalTypeMappingSource,
691-
static (c, t, m) => new JsonViewColumn(c, m.StoreType, (View)t, m));
695+
view, containerColumnName, containerColumnType, mappedType, relationalTypeMappingSource,
696+
static (colName, colType, table, mapping) => new JsonViewColumn(colName, colType ?? mapping.StoreType, (View)table, mapping));
692697
}
693698
else
694699
{

src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs

+5
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ public static class RelationalAnnotationNames
324324
/// </summary>
325325
public const string ContainerColumnName = Prefix + "ContainerColumnName";
326326

327+
/// <summary>
328+
/// The column type for the container column to which the object is mapped.
329+
/// </summary>
330+
public const string ContainerColumnType = Prefix + nameof(ContainerColumnType);
331+
327332
/// <summary>
328333
/// The name for the annotation specifying container column type mapping.
329334
/// </summary>

src/EFCore.SqlServer/EFCore.SqlServer.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
<ItemGroup>
4343
<Compile Include="..\Shared\*.cs" />
44+
<Compile Remove="Storage\Internal\SqlServerJsonTypeMapping.cs" />
4445
</ItemGroup>
4546

4647
<ItemGroup>
@@ -49,7 +50,7 @@
4950
</ItemGroup>
5051

5152
<ItemGroup>
52-
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
53+
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-dev" />
5354
</ItemGroup>
5455

5556
<ItemGroup>

src/EFCore.SqlServer/Query/Internal/SqlExpressions/SqlServerOpenJsonExpression.cs

+10
Original file line numberDiff line numberDiff line change
@@ -321,4 +321,14 @@ public readonly record struct ColumnInfo(
321321
RelationalTypeMapping TypeMapping,
322322
IReadOnlyList<PathSegment>? Path = null,
323323
bool AsJson = false);
324+
325+
/// <summary>
326+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
327+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
328+
/// any release. You should only use it directly in your code with extreme caution and knowing that
329+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
330+
/// </summary>
331+
public virtual SqlServerOpenJsonExpression Update(SqlExpression sqlExpression)
332+
=> new(Alias, sqlExpression, Path, ColumnInfos);
333+
324334
}

src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
475475
return jsonScalarExpression;
476476
}
477477

478-
if (jsonScalarExpression.TypeMapping is SqlServerJsonTypeMapping
478+
if (jsonScalarExpression.TypeMapping is SqlServerJsonElementTypeMapping
479479
|| jsonScalarExpression.TypeMapping?.ElementTypeMapping is not null)
480480
{
481481
Sql.Append("JSON_QUERY(");
@@ -494,7 +494,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
494494
GenerateJsonPath(jsonScalarExpression.Path);
495495
Sql.Append(")");
496496

497-
if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
497+
if (jsonScalarExpression.TypeMapping is not SqlServerJsonElementTypeMapping and not StringTypeMapping)
498498
{
499499
Sql.Append(" AS ");
500500
Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);

src/EFCore.SqlServer/Query/Internal/SqlServerTypeMappingPostprocessor.cs

+15-7
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ protected override Expression VisitExtension(Expression expression)
4343
=> expression switch
4444
{
4545
SqlServerOpenJsonExpression openJsonExpression
46-
when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMapping)
47-
=> ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping }),
46+
=> ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression),
4847

4948
_ => base.VisitExtension(expression)
5049
};
@@ -55,12 +54,21 @@ when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMa
5554
/// any release. You should only use it directly in your code with extreme caution and knowing that
5655
/// doing so can result in application failures when updating to a new Entity Framework Core release.
5756
/// </summary>
58-
protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(
59-
SqlServerOpenJsonExpression openJsonExpression,
60-
IReadOnlyList<RelationalTypeMapping> typeMappings)
57+
protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression)
6158
{
62-
Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1");
63-
var elementTypeMapping = typeMappings[0];
59+
if (openJsonExpression is { JsonExpression.TypeMapping: SqlServerStringTypeMapping { StoreType: "json" } } or
60+
{ JsonExpression.TypeMapping: SqlServerJsonElementTypeMapping { StoreType: "json" } })
61+
{
62+
openJsonExpression = openJsonExpression.Update(
63+
new SqlUnaryExpression(
64+
ExpressionType.Convert, (SqlExpression)Visit(openJsonExpression.JsonExpression), typeof(string),
65+
_typeMappingSource.FindMapping(typeof(string))!));
66+
}
67+
68+
if (!TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var elementTypeMapping))
69+
{
70+
return openJsonExpression;
71+
}
6472

6573
// Constant queryables are translated to VALUES, no need for JSON.
6674
// Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.

0 commit comments

Comments
 (0)