Skip to content

Commit f0315cf

Browse files
authored
Support JSON columns in compiled models (#30254)
1 parent ca32394 commit f0315cf

23 files changed

+1434
-172
lines changed

src/EFCore.Relational/Design/AnnotationCodeGenerator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ public class AnnotationCodeGenerator : IAnnotationCodeGenerator
3535
RelationalAnnotationNames.UpdateStoredProcedure,
3636
RelationalAnnotationNames.MappingFragments,
3737
RelationalAnnotationNames.RelationalOverrides,
38+
#pragma warning disable CS0618
3839
RelationalAnnotationNames.ContainerColumnTypeMapping
40+
#pragma warning restore CS0618
3941
};
4042

4143
/// <summary>
@@ -225,7 +227,9 @@ public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
225227
containerColumnName));
226228

227229
annotations.Remove(RelationalAnnotationNames.ContainerColumnName);
230+
#pragma warning disable CS0618
228231
annotations.Remove(RelationalAnnotationNames.ContainerColumnTypeMapping);
232+
#pragma warning restore CS0618
229233
}
230234

231235
methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(entityType, annotations, GenerateFluentApi));

src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,7 @@ public static void SetContainerColumnName(this IMutableEntityType entityType, st
16821682
/// </summary>
16831683
/// <param name="entityType">The entity type to set the container column type mapping for.</param>
16841684
/// <param name="typeMapping">The type mapping to set.</param>
1685+
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
16851686
public static void SetContainerColumnTypeMapping(this IMutableEntityType entityType, RelationalTypeMapping typeMapping)
16861687
=> entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.ContainerColumnTypeMapping, typeMapping);
16871688

@@ -1692,6 +1693,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT
16921693
/// <param name="typeMapping">The type mapping to set.</param>
16931694
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
16941695
/// <returns>The configured value.</returns>
1696+
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
16951697
public static RelationalTypeMapping? SetContainerColumnTypeMapping(
16961698
this IConventionEntityType entityType,
16971699
RelationalTypeMapping? typeMapping,
@@ -1704,6 +1706,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT
17041706
/// </summary>
17051707
/// <param name="entityType">The entity type to set the container column type mapping for.</param>
17061708
/// <returns>The <see cref="ConfigurationSource" /> for the container column type mapping.</returns>
1709+
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
17071710
public static ConfigurationSource? GetContainerColumnTypeMappingConfigurationSource(this IConventionEntityType entityType)
17081711
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnTypeMapping)
17091712
?.GetConfigurationSource();
@@ -1713,6 +1716,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT
17131716
/// </summary>
17141717
/// <param name="entityType">The entity type to get the container column type mapping for.</param>
17151718
/// <returns>The container column type mapping to which the entity type is mapped.</returns>
1719+
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
17161720
public static RelationalTypeMapping? GetContainerColumnTypeMapping(this IReadOnlyEntityType entityType)
17171721
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnTypeMapping)?.Value is RelationalTypeMapping typeMapping
17181722
? typeMapping

src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,14 @@ public RelationalMapToJsonConvention(
3737
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }
3838

3939
/// <inheritdoc />
40+
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
4041
public virtual void ProcessEntityTypeAnnotationChanged(
4142
IConventionEntityTypeBuilder entityTypeBuilder,
4243
string name,
4344
IConventionAnnotation? annotation,
4445
IConventionAnnotation? oldAnnotation,
4546
IConventionContext<IConventionAnnotation> context)
4647
{
47-
if (name != RelationalAnnotationNames.ContainerColumnName)
48-
{
49-
return;
50-
}
51-
52-
var jsonColumnName = annotation?.Value as string;
53-
if (!string.IsNullOrEmpty(jsonColumnName))
54-
{
55-
var jsonColumnTypeMapping = ((IRelationalTypeMappingSource)Dependencies.TypeMappingSource).FindMapping(
56-
typeof(JsonElement))!;
57-
58-
entityTypeBuilder.Metadata.SetContainerColumnTypeMapping(jsonColumnTypeMapping);
59-
}
60-
else
61-
{
62-
entityTypeBuilder.Metadata.SetContainerColumnTypeMapping(null);
63-
}
6448
}
6549

6650
/// <inheritdoc />

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public class Column : ColumnBase<ColumnMapping>, IColumn
2323
/// any release. You should only use it directly in your code with extreme caution and knowing that
2424
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2525
/// </summary>
26-
public Column(string name, string type, Table table)
27-
: base(name, type, table)
26+
public Column(string name, string type, Table table, RelationalTypeMapping? storeTypeMapping = null)
27+
: base(name, type, table, storeTypeMapping)
2828
{
2929
}
3030

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@ public class ColumnBase<TColumnMappingBase> : Annotatable, IColumnBase
1313
where TColumnMappingBase : class, IColumnMappingBase
1414
{
1515
private Type? _providerClrType;
16+
private RelationalTypeMapping? _storeTypeMapping;
1617

1718
/// <summary>
1819
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1920
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
2021
/// any release. You should only use it directly in your code with extreme caution and knowing that
2122
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2223
/// </summary>
23-
public ColumnBase(string name, string type, TableBase table)
24+
public ColumnBase(string name, string type, TableBase table, RelationalTypeMapping? storeTypeMapping = null)
2425
{
2526
Name = name;
2627
StoreType = type;
2728
Table = table;
29+
_storeTypeMapping = storeTypeMapping;
2830
}
2931

3032
/// <summary>
@@ -97,7 +99,7 @@ public virtual Type ProviderClrType
9799
/// doing so can result in application failures when updating to a new Entity Framework Core release.
98100
/// </summary>
99101
public virtual RelationalTypeMapping StoreTypeMapping
100-
=> PropertyMappings.First().TypeMapping;
102+
=> _storeTypeMapping ??= PropertyMappings.First().TypeMapping;
101103

102104
/// <summary>
103105
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public class FunctionColumn : ColumnBase<FunctionColumnMapping>, IFunctionColumn
1717
/// any release. You should only use it directly in your code with extreme caution and knowing that
1818
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1919
/// </summary>
20-
public FunctionColumn(string name, string type, StoreFunction function)
21-
: base(name, type, function)
20+
public FunctionColumn(string name, string type, StoreFunction function, RelationalTypeMapping? storeTypeMapping = null)
21+
: base(name, type, function, storeTypeMapping)
2222
{
2323
}
2424

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,15 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
1111
/// </summary>
1212
public class JsonColumn : Column, IColumn
1313
{
14-
private readonly ValueComparer _providerValueComparer;
15-
1614
/// <summary>
1715
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1816
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1917
/// any release. You should only use it directly in your code with extreme caution and knowing that
2018
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2119
/// </summary>
22-
public JsonColumn(string name, string type, Table table, ValueComparer provierValueComparer)
23-
: base(name, type, table)
20+
public JsonColumn(string name, Table table, RelationalTypeMapping storeTypeMapping)
21+
: base(name, storeTypeMapping.StoreType, table, storeTypeMapping)
2422
{
25-
_providerValueComparer = provierValueComparer;
2623
}
2724

2825
/// <summary>
@@ -149,7 +146,7 @@ bool IColumn.IsRowVersion
149146
/// doing so can result in application failures when updating to a new Entity Framework Core release.
150147
/// </summary>
151148
ValueComparer IColumnBase.ProviderValueComparer
152-
=> _providerValueComparer;
149+
=> StoreTypeMapping.ProviderValueComparer;
153150

154151
/// <summary>
155152
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public class JsonViewColumn : ViewColumn, IViewColumn
1717
/// any release. You should only use it directly in your code with extreme caution and knowing that
1818
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1919
/// </summary>
20-
public JsonViewColumn(string name, string type, View view)
21-
: base(name, type, view)
20+
public JsonViewColumn(string name, View view, RelationalTypeMapping storeTypeMapping)
21+
: base(name, storeTypeMapping.StoreType, view, storeTypeMapping)
2222
{
2323
}
2424
}

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

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public static IRelationalModel Create(
160160

161161
foreach (var entityType in model.GetEntityTypes())
162162
{
163-
AddDefaultMappings(databaseModel, entityType);
163+
AddDefaultMappings(databaseModel, entityType, relationalTypeMappingSource);
164164

165165
AddTables(databaseModel, entityType, relationalTypeMappingSource);
166166

@@ -291,7 +291,10 @@ public static IRelationalModel Create(
291291
return databaseModel;
292292
}
293293

294-
private static void AddDefaultMappings(RelationalModel databaseModel, IEntityType entityType)
294+
private static void AddDefaultMappings(
295+
RelationalModel databaseModel,
296+
IEntityType entityType,
297+
IRelationalTypeMappingSource relationalTypeMappingSource)
295298
{
296299
var mappedType = entityType;
297300
Check.DebugAssert(entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultMappings) == null, "not null");
@@ -312,42 +315,52 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp
312315
var tableMapping = new TableMappingBase<ColumnMappingBase>(
313316
entityType, defaultTable, includesDerivedTypes: !isTpc && mappedType == entityType);
314317

315-
foreach (var property in entityType.GetProperties())
318+
var containerColumnName = mappedType.GetContainerColumnName();
319+
if (!string.IsNullOrEmpty(containerColumnName))
316320
{
317-
var columnName = property.IsPrimaryKey() || isTpc || isTph || property.DeclaringEntityType == mappedType
318-
? property.GetColumnName()
319-
: null;
320-
if (columnName == null)
321+
CreateContainerColumn(
322+
defaultTable, containerColumnName, mappedType, relationalTypeMappingSource,
323+
static (c, t, m) => new ColumnBase<ColumnMappingBase>(c, m.StoreType, t, m));
324+
}
325+
else
326+
{
327+
foreach (var property in entityType.GetProperties())
321328
{
322-
continue;
323-
}
329+
var columnName = property.IsPrimaryKey() || isTpc || isTph || property.DeclaringEntityType == mappedType
330+
? property.GetColumnName()
331+
: null;
332+
if (columnName == null)
333+
{
334+
continue;
335+
}
324336

325-
var column = (ColumnBase<ColumnMappingBase>?)defaultTable.FindColumn(columnName);
326-
if (column == null)
327-
{
328-
column = new ColumnBase<ColumnMappingBase>(columnName, property.GetColumnType(), defaultTable)
337+
var column = (ColumnBase<ColumnMappingBase>?)defaultTable.FindColumn(columnName);
338+
if (column == null)
329339
{
330-
IsNullable = property.IsColumnNullable()
331-
};
332-
defaultTable.Columns.Add(columnName, column);
333-
}
334-
else if (!property.IsColumnNullable())
335-
{
336-
column.IsNullable = false;
337-
}
340+
column = new ColumnBase<ColumnMappingBase>(columnName, property.GetColumnType(), defaultTable)
341+
{
342+
IsNullable = property.IsColumnNullable()
343+
};
344+
defaultTable.Columns.Add(columnName, column);
345+
}
346+
else if (!property.IsColumnNullable())
347+
{
348+
column.IsNullable = false;
349+
}
338350

339-
var columnMapping = new ColumnMappingBase(property, column, tableMapping);
340-
tableMapping.AddColumnMapping(columnMapping);
341-
column.AddPropertyMapping(columnMapping);
351+
var columnMapping = new ColumnMappingBase(property, column, tableMapping);
352+
tableMapping.AddColumnMapping(columnMapping);
353+
column.AddPropertyMapping(columnMapping);
342354

343-
if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultColumnMappings)
344-
is not SortedSet<ColumnMappingBase> columnMappings)
345-
{
346-
columnMappings = new SortedSet<ColumnMappingBase>(ColumnMappingBaseComparer.Instance);
347-
property.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultColumnMappings, columnMappings);
348-
}
355+
if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultColumnMappings)
356+
is not SortedSet<ColumnMappingBase> columnMappings)
357+
{
358+
columnMappings = new SortedSet<ColumnMappingBase>(ColumnMappingBaseComparer.Instance);
359+
property.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultColumnMappings, columnMappings);
360+
}
349361

350-
columnMappings.Add(columnMapping);
362+
columnMappings.Add(columnMapping);
363+
}
351364
}
352365

353366
if (((ITableMappingBase)tableMapping).ColumnMappings.Any()
@@ -460,23 +473,9 @@ private static void CreateTableMapping(
460473
var containerColumnName = mappedType.GetContainerColumnName();
461474
if (!string.IsNullOrEmpty(containerColumnName))
462475
{
463-
var ownership = mappedType.GetForeignKeys().Single(fk => fk.IsOwnership);
464-
if (!ownership.PrincipalEntityType.IsMappedToJson())
465-
{
466-
Debug.Assert(table.FindColumn(containerColumnName) == null);
467-
468-
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!;
469-
var jsonColumn = new JsonColumn(
470-
containerColumnName, jsonColumnTypeMapping.StoreType, table, jsonColumnTypeMapping.ProviderValueComparer);
471-
table.Columns.Add(containerColumnName, jsonColumn);
472-
jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique;
473-
474-
if (ownership.PrincipalEntityType.BaseType != null)
475-
{
476-
// if navigation is defined on a derived type, the column must be made nullable
477-
jsonColumn.IsNullable = true;
478-
}
479-
}
476+
CreateContainerColumn(
477+
table, containerColumnName, mappedType, relationalTypeMappingSource,
478+
static (c, t, m) => new JsonColumn(c, (Table)t, m));
480479
}
481480
else
482481
{
@@ -526,6 +525,32 @@ private static void CreateTableMapping(
526525
}
527526
}
528527

528+
private static void CreateContainerColumn<TColumnMappingBase>(
529+
TableBase tableBase,
530+
string containerColumnName,
531+
IEntityType mappedType,
532+
IRelationalTypeMappingSource relationalTypeMappingSource,
533+
Func<string, TableBase, RelationalTypeMapping, ColumnBase<TColumnMappingBase>> createColumn)
534+
where TColumnMappingBase : class, IColumnMappingBase
535+
{
536+
var ownership = mappedType.GetForeignKeys().Single(fk => fk.IsOwnership);
537+
if (!ownership.PrincipalEntityType.IsMappedToJson())
538+
{
539+
Check.DebugAssert(tableBase.FindColumn(containerColumnName) == null, $"Table does not have column '{containerColumnName}'.");
540+
541+
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!;
542+
var jsonColumn = createColumn(containerColumnName, tableBase, jsonColumnTypeMapping);// new JsonColumn(containerColumnName, table, jsonColumnTypeMapping);
543+
tableBase.Columns.Add(containerColumnName, jsonColumn);
544+
jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique;
545+
546+
if (ownership.PrincipalEntityType.BaseType != null)
547+
{
548+
// if navigation is defined on a derived type, the column must be made nullable
549+
jsonColumn.IsNullable = true;
550+
}
551+
}
552+
}
553+
529554
private static void AddViews(
530555
RelationalModel databaseModel,
531556
IEntityType entityType,
@@ -618,22 +643,9 @@ private static void CreateViewMapping(
618643
var containerColumnName = mappedType.GetContainerColumnName();
619644
if (!string.IsNullOrEmpty(containerColumnName))
620645
{
621-
var ownership = mappedType.GetForeignKeys().Single(fk => fk.IsOwnership);
622-
if (!ownership.PrincipalEntityType.IsMappedToJson())
623-
{
624-
Debug.Assert(view.FindColumn(containerColumnName) == null);
625-
626-
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!;
627-
var jsonColumn = new JsonViewColumn(containerColumnName, jsonColumnTypeMapping.StoreType, view);
628-
view.Columns.Add(containerColumnName, jsonColumn);
629-
jsonColumn.IsNullable = !ownership.IsRequired || !ownership.IsUnique;
630-
631-
if (ownership.PrincipalEntityType.BaseType != null)
632-
{
633-
// if navigation is defined on a derived type, the column must be made nullable
634-
jsonColumn.IsNullable = true;
635-
}
636-
}
646+
CreateContainerColumn(
647+
view, containerColumnName, mappedType, relationalTypeMappingSource,
648+
static (c, t, m) => new JsonViewColumn(c, (View)t, m));
637649
}
638650
else
639651
{

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public class SqlQueryColumn : ColumnBase<SqlQueryColumnMapping>, ISqlQueryColumn
1717
/// any release. You should only use it directly in your code with extreme caution and knowing that
1818
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1919
/// </summary>
20-
public SqlQueryColumn(string name, string type, SqlQuery sqlQuery)
21-
: base(name, type, sqlQuery)
20+
public SqlQueryColumn(string name, string type, SqlQuery sqlQuery, RelationalTypeMapping? storeTypeMapping = null)
21+
: base(name, type, sqlQuery, storeTypeMapping)
2222
{
2323
}
2424

0 commit comments

Comments
 (0)