Skip to content

Commit

Permalink
Support JSON columns in compiled models
Browse files Browse the repository at this point in the history
Fixes #29602

The main change here is to store the JSON type mapping in the relational model, and obsolete the previous storage in the underlying EF model.
  • Loading branch information
ajcvickers committed Feb 11, 2023
1 parent 8664081 commit 9166ddc
Show file tree
Hide file tree
Showing 13 changed files with 1,358 additions and 57 deletions.
4 changes: 4 additions & 0 deletions src/EFCore.Relational/Design/AnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public class AnnotationCodeGenerator : IAnnotationCodeGenerator
RelationalAnnotationNames.UpdateStoredProcedure,
RelationalAnnotationNames.MappingFragments,
RelationalAnnotationNames.RelationalOverrides,
#pragma warning disable CS0618
RelationalAnnotationNames.ContainerColumnTypeMapping
#pragma warning restore CS0618
};

/// <summary>
Expand Down Expand Up @@ -225,7 +227,9 @@ public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
containerColumnName));

annotations.Remove(RelationalAnnotationNames.ContainerColumnName);
#pragma warning disable CS0618
annotations.Remove(RelationalAnnotationNames.ContainerColumnTypeMapping);
#pragma warning restore CS0618
}

methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(entityType, annotations, GenerateFluentApi));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,7 @@ public static void SetContainerColumnName(this IMutableEntityType entityType, st
/// </summary>
/// <param name="entityType">The entity type to set the container column type mapping for.</param>
/// <param name="typeMapping">The type mapping to set.</param>
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
public static void SetContainerColumnTypeMapping(this IMutableEntityType entityType, RelationalTypeMapping typeMapping)
=> entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.ContainerColumnTypeMapping, typeMapping);

Expand All @@ -1692,6 +1693,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT
/// <param name="typeMapping">The type mapping to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
public static RelationalTypeMapping? SetContainerColumnTypeMapping(
this IConventionEntityType entityType,
RelationalTypeMapping? typeMapping,
Expand All @@ -1704,6 +1706,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT
/// </summary>
/// <param name="entityType">The entity type to set the container column type mapping for.</param>
/// <returns>The <see cref="ConfigurationSource" /> for the container column type mapping.</returns>
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
public static ConfigurationSource? GetContainerColumnTypeMappingConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnTypeMapping)
?.GetConfigurationSource();
Expand All @@ -1713,6 +1716,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT
/// </summary>
/// <param name="entityType">The entity type to get the container column type mapping for.</param>
/// <returns>The container column type mapping to which the entity type is mapped.</returns>
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
public static RelationalTypeMapping? GetContainerColumnTypeMapping(this IReadOnlyEntityType entityType)
=> entityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnTypeMapping)?.Value is RelationalTypeMapping typeMapping
? typeMapping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,14 @@ public RelationalMapToJsonConvention(
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }

/// <inheritdoc />
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
public virtual void ProcessEntityTypeAnnotationChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
string name,
IConventionAnnotation? annotation,
IConventionAnnotation? oldAnnotation,
IConventionContext<IConventionAnnotation> context)
{
if (name != RelationalAnnotationNames.ContainerColumnName)
{
return;
}

var jsonColumnName = annotation?.Value as string;
if (!string.IsNullOrEmpty(jsonColumnName))
{
var jsonColumnTypeMapping = ((IRelationalTypeMappingSource)Dependencies.TypeMappingSource).FindMapping(
typeof(JsonElement))!;

entityTypeBuilder.Metadata.SetContainerColumnTypeMapping(jsonColumnTypeMapping);
}
else
{
entityTypeBuilder.Metadata.SetContainerColumnTypeMapping(null);
}
}

/// <inheritdoc />
Expand Down
18 changes: 12 additions & 6 deletions src/EFCore.Relational/Metadata/Internal/JsonColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,26 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
/// </summary>
public class JsonColumn : Column, IColumn
{
private readonly ValueComparer _providerValueComparer;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public JsonColumn(string name, string type, Table table, ValueComparer provierValueComparer)
: base(name, type, table)
public JsonColumn(string name, Table table, RelationalTypeMapping typeMapping)
: base(name, typeMapping.StoreType, table)
{
_providerValueComparer = provierValueComparer;
StoreTypeMapping = typeMapping;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override RelationalTypeMapping StoreTypeMapping { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -149,7 +155,7 @@ bool IColumn.IsRowVersion
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
ValueComparer IColumnBase.ProviderValueComparer
=> _providerValueComparer;
=> StoreTypeMapping.ProviderValueComparer;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
13 changes: 11 additions & 2 deletions src/EFCore.Relational/Metadata/Internal/JsonViewColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ public class JsonViewColumn : ViewColumn, IViewColumn
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public JsonViewColumn(string name, string type, View view)
: base(name, type, view)
public JsonViewColumn(string name, View view, RelationalTypeMapping typeMapping)
: base(name, typeMapping.StoreType, view)
{
StoreTypeMapping = typeMapping;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override RelationalTypeMapping StoreTypeMapping { get; }
}
5 changes: 2 additions & 3 deletions src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,7 @@ private static void CreateTableMapping(
Debug.Assert(table.FindColumn(containerColumnName) == null);

var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!;
var jsonColumn = new JsonColumn(
containerColumnName, jsonColumnTypeMapping.StoreType, table, jsonColumnTypeMapping.ProviderValueComparer);
var jsonColumn = new JsonColumn(containerColumnName, table, jsonColumnTypeMapping);
table.Columns.Add(containerColumnName, jsonColumn);
jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique;

Expand Down Expand Up @@ -624,7 +623,7 @@ private static void CreateViewMapping(
Debug.Assert(view.FindColumn(containerColumnName) == null);

var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!;
var jsonColumn = new JsonViewColumn(containerColumnName, jsonColumnTypeMapping.StoreType, view);
var jsonColumn = new JsonViewColumn(containerColumnName, view, jsonColumnTypeMapping);
view.Columns.Add(containerColumnName, jsonColumn);
jsonColumn.IsNullable = !ownership.IsRequired || !ownership.IsUnique;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public static class RelationalAnnotationNames
/// <summary>
/// The name for the annotation specifying container column type mapping.
/// </summary>
[Obsolete("Container column mappings are now obtained from IColumnBase.StoreTypeMapping")]
public const string ContainerColumnTypeMapping = Prefix + "ContainerColumnTypeMapping";

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

Expand Down Expand Up @@ -1321,12 +1322,17 @@ private Expression CreateJsonShapers(

if (index == 0)
{
var jsonColumnName = entityType.GetContainerColumnName()!;
var jsonColumnTypeMapping = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? entityType.GetDefaultMappings().Single().Table)
.FindColumn(jsonColumnName)!.StoreTypeMapping;

// create the JsonElement for the initial entity
var jsonElementValueExpression = CreateGetValueExpression(
_dataReaderParameter,
jsonProjectionInfo.JsonColumnIndex,
nullable: true,
entityType.GetContainerColumnTypeMapping()!,
jsonColumnTypeMapping,
typeof(JsonElement?),
property: null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,9 @@ private void AddJsonNavigationBindings(
{
var targetEntityType = ownedJsonNavigation.TargetEntityType;
var jsonColumnName = targetEntityType.GetContainerColumnName()!;
var jsonColumnTypeMapping = targetEntityType.GetContainerColumnTypeMapping()!;
var jsonColumnTypeMapping = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? entityType.GetDefaultMappings().Single().Table)
.FindColumn(jsonColumnName)!.StoreTypeMapping;

var jsonColumn = new ConcreteColumnExpression(
jsonColumnName,
Expand Down
11 changes: 5 additions & 6 deletions src/EFCore.Relational/Update/ModificationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ private List<IColumnModification> GenerateColumnModifications()

if (jsonEntry)
{
var jsonColumnsUpdateMap = new Dictionary<string, JsonPartialUpdateInfo>();
var jsonColumnsUpdateMap = new Dictionary<IColumn, JsonPartialUpdateInfo>();
var processedEntries = new List<IUpdateEntry>();
foreach (var entry in _entries.Where(e => e.EntityType.IsMappedToJson()))
{
var jsonColumn = entry.EntityType.GetContainerColumnName()!;
var jsonColumn = GetTableMapping(entry.EntityType)!.Table.FindColumn(entry.EntityType.GetContainerColumnName()!)!;
var jsonPartialUpdateInfo = FindJsonPartialUpdateInfo(entry, processedEntries);

if (jsonPartialUpdateInfo == null)
Expand All @@ -316,12 +316,11 @@ private List<IColumnModification> GenerateColumnModifications()
jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo;
}

foreach (var (jsonColumnName, updateInfo) in jsonColumnsUpdateMap)
foreach (var (jsonColumn, updateInfo) in jsonColumnsUpdateMap)
{
var finalUpdatePathElement = updateInfo.Path.Last();
var navigation = finalUpdatePathElement.Navigation;

var jsonColumnTypeMapping = navigation.TargetEntityType.GetContainerColumnTypeMapping()!;
var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping;
var navigationValue = finalUpdatePathElement.ParentEntry.GetCurrentValue(navigation);

var json = default(JsonNode?);
Expand Down Expand Up @@ -367,7 +366,7 @@ private List<IColumnModification> GenerateColumnModifications()
}

var columnModificationParameters = new ColumnModificationParameters(
jsonColumnName,
jsonColumn.Name,
value: json?.ToJsonString(),
property: updateInfo.Property,
columnType: jsonColumnTypeMapping.StoreType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ public void Test_new_annotations_handled_for_entity_types()
RelationalAnnotationNames.JsonPropertyName,
// Appears on entity type but requires specific model (i.e. owned types that can map to json, otherwise validation throws)
RelationalAnnotationNames.ContainerColumnName,
#pragma warning disable CS0618
RelationalAnnotationNames.ContainerColumnTypeMapping,
#pragma warning restore CS0618
};

// Add a line here if the code generator is supposed to handle this annotation
Expand Down Expand Up @@ -262,7 +264,9 @@ public void Test_new_annotations_handled_for_properties()
RelationalAnnotationNames.ModelDependencies,
RelationalAnnotationNames.FieldValueGetter,
RelationalAnnotationNames.ContainerColumnName,
#pragma warning disable CS0618
RelationalAnnotationNames.ContainerColumnTypeMapping,
#pragma warning restore CS0618
RelationalAnnotationNames.JsonPropertyName,
};

Expand Down
Loading

0 comments on commit 9166ddc

Please sign in to comment.