diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
index c899390b9bb..40f64bc96d7 100644
--- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
@@ -1903,4 +1903,36 @@ public static bool CanHaveTrigger(
tableName,
tableSchema,
fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// TODO
+ ///
+ public static EntityTypeBuilder MapReferenceToJson(
+ this EntityTypeBuilder entityTypeBuilder,
+ Expression> navigationExpression,
+ string jsonColumnName)
+ where TEntity : class
+ where TRelatedEntity : class
+ {
+ var ownedNavigationBuilder = entityTypeBuilder.OwnsOne(navigationExpression);
+ ownedNavigationBuilder.OwnedEntityType.SetAnnotation(RelationalAnnotationNames.MapToJsonColumnName, jsonColumnName);
+
+ return entityTypeBuilder;
+ }
+
+ ///
+ /// TODO
+ ///
+ public static EntityTypeBuilder MapCollectionToJson(
+ this EntityTypeBuilder entityTypeBuilder,
+ Expression?>> navigationExpression,
+ string jsonColumnName)
+ where TEntity : class
+ where TRelatedEntity : class
+ {
+ var ownedNavigationBuilder = entityTypeBuilder.OwnsMany(navigationExpression);
+ ownedNavigationBuilder.OwnedEntityType.SetAnnotation(RelationalAnnotationNames.MapToJsonColumnName, jsonColumnName);
+
+ return entityTypeBuilder;
+ }
}
diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
index 5e368799897..543625da7e8 100644
--- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
@@ -62,7 +62,7 @@ public static class RelationalEntityTypeExtensions
var ownership = entityType.FindOwnership();
if (ownership != null
- && ownership.IsUnique)
+ && (ownership.IsUnique || entityType.MappedToJson()))
{
return ownership.PrincipalEntityType.GetTableName();
}
@@ -846,12 +846,18 @@ public static IEnumerable FindRowInternalForeignKeys(
foreach (var foreignKey in entityType.GetForeignKeys())
{
+ var mappedToJson = entityType.MappedToJson();
var principalEntityType = foreignKey.PrincipalEntityType;
+
+ var pkPropertiesToMatch = mappedToJson
+ ? primaryKey.Properties.Take(foreignKey.Properties.Count).ToList().AsReadOnly()
+ : primaryKey.Properties;
+
if (!foreignKey.PrincipalKey.IsPrimaryKey()
|| principalEntityType == foreignKey.DeclaringEntityType
- || !foreignKey.IsUnique
+ || (!foreignKey.IsUnique && !mappedToJson)
#pragma warning disable EF1001 // Internal EF Core API usage.
- || !PropertyListComparer.Instance.Equals(foreignKey.Properties, primaryKey.Properties))
+ || !PropertyListComparer.Instance.Equals(foreignKey.Properties, pkPropertiesToMatch))
#pragma warning restore EF1001 // Internal EF Core API usage.
{
continue;
@@ -1263,4 +1269,34 @@ public static IEnumerable GetDeclaredTriggers(this IEntityType entityT
=> Trigger.GetDeclaredTriggers(entityType).Cast();
#endregion Trigger
+
+ ///
+ /// TODO
+ ///
+ public static bool MappedToJson(this IConventionEntityType entityType)
+ => !string.IsNullOrEmpty(entityType.MappedToJsonColumnName());
+
+ ///
+ /// TODO
+ ///
+ public static bool MappedToJson(this IReadOnlyEntityType entityType)
+ => !string.IsNullOrEmpty(entityType.MappedToJsonColumnName());
+
+ ///
+ /// TODO
+ ///
+ public static string? MappedToJsonColumnName(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(RelationalAnnotationNames.MapToJsonColumnName)?.Value as string;
+
+ ///
+ /// TODO
+ ///
+ public static string? MappedToJsonColumnName(this IReadOnlyEntityType entityType)
+ => entityType.FindAnnotation(RelationalAnnotationNames.MapToJsonColumnName)?.Value as string;
+
+ ///
+ /// TODO
+ ///
+ public static string? MappedToJsonColumnTypeName(this IReadOnlyEntityType entityType)
+ => entityType.FindAnnotation(RelationalAnnotationNames.MapToJsonColumnTypeName)?.Value as string;
}
diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
index e173ed51b4c..040a80c23b9 100644
--- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
@@ -113,7 +113,13 @@ public static string GetColumnBaseName(this IReadOnlyProperty property)
return (string?)columnAnnotation.Value;
}
- return GetDefaultColumnName(property, storeObject);
+ // GetDefaultColumnName returns non-nullable, so for json collection entity ordinal keys (which are not mapped)
+ // just make it return emtpty string and convert to null here
+ var result = GetDefaultColumnName(property, storeObject);
+
+ return result == string.Empty && property.DeclaringEntityType.MappedToJson()
+ ? null
+ : result;
}
///
@@ -144,6 +150,14 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property, in St
return sharedTablePrincipalConcurrencyProperty.GetColumnName(storeObject)!;
}
+ if (property.DeclaringEntityType.MappedToJson())
+ {
+ // only PKs that are unhandled at this point should be ordinal keys - not mapped to anything
+ return property.IsPrimaryKey()
+ ? string.Empty
+ : property.GetDefaultColumnBaseName();
+ }
+
var entityType = property.DeclaringEntityType;
StringBuilder? builder = null;
while (true)
@@ -1323,6 +1337,14 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
var column = property.GetColumnName(storeObject);
if (column == null)
{
+ // for entities mapped to json, if column name was not returned the property is either contained in json,
+ // or, in case of collection, a synthesized key based on ordinal
+ // so just skip this, there is no shared objct root to speak of
+ if (property.DeclaringEntityType.MappedToJson())
+ {
+ return null;
+ }
+
throw new InvalidOperationException(
RelationalStrings.PropertyNotMappedToTable(
property.Name, property.DeclaringEntityType.DisplayName(), storeObject.DisplayName()));
@@ -1379,7 +1401,21 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
break;
}
- principalProperty = linkingRelationship.PrincipalKey.Properties[linkingRelationship.Properties.IndexOf(principalProperty)];
+ if (property.DeclaringEntityType.MappedToJson())
+ {
+ // for json collection entities don't match the ordinal key
+ var index = linkingRelationship.Properties.IndexOf(principalProperty);
+ if (index == -1)
+ {
+ return null;
+ }
+
+ principalProperty = linkingRelationship.PrincipalKey.Properties[index];
+ }
+ else
+ {
+ principalProperty = linkingRelationship.PrincipalKey.Properties[linkingRelationship.Properties.IndexOf(principalProperty)];
+ }
}
return principalProperty == property ? null : principalProperty;
diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs
index 773eb861576..8ae774b19a6 100644
--- a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs
+++ b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs
@@ -62,7 +62,11 @@ protected override void InitializeModel(IModel model, bool designTime, bool prev
}
else
{
- RelationalModel.Add(model, RelationalDependencies.RelationalAnnotationProvider, designTime);
+ RelationalModel.Add(
+ model,
+ RelationalDependencies.RelationalAnnotationProvider,
+ RelationalDependencies.RelationalTypeMappingSource,
+ designTime);
}
}
}
diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializerDependencies.cs b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializerDependencies.cs
index 986fb12faf3..eff60df0435 100644
--- a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializerDependencies.cs
+++ b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializerDependencies.cs
@@ -47,10 +47,12 @@ public sealed record RelationalModelRuntimeInitializerDependencies
[EntityFrameworkInternal]
public RelationalModelRuntimeInitializerDependencies(
RelationalModelDependencies relationalModelDependencies,
- IRelationalAnnotationProvider relationalAnnotationProvider)
+ IRelationalAnnotationProvider relationalAnnotationProvider,
+ IRelationalTypeMappingSource relationalTypeMappingSource)
{
RelationalModelDependencies = relationalModelDependencies;
RelationalAnnotationProvider = relationalAnnotationProvider;
+ RelationalTypeMappingSource = relationalTypeMappingSource;
}
///
@@ -62,4 +64,9 @@ public RelationalModelRuntimeInitializerDependencies(
/// The relational annotation provider.
///
public IRelationalAnnotationProvider RelationalAnnotationProvider { get; init; }
+
+ ///
+ /// The relational type mapping source.
+ ///
+ public IRelationalTypeMappingSource RelationalTypeMappingSource { get; init; }
}
diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
index 7a77dfccea6..c0fd7321c27 100644
--- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
+++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
@@ -310,6 +310,12 @@ protected virtual void ValidateSharedTableCompatibility(
continue;
}
+ // no need to check json-mapped types (or is there, just different?)
+ if (entityType.MappedToJson())
+ {
+ continue;
+ }
+
var (principalEntityTypes, optional) = GetPrincipalEntityTypes(entityType);
if (!optional)
{
@@ -401,7 +407,8 @@ protected virtual void ValidateSharedTableCompatibility(
}
var storeObject = StoreObjectIdentifier.Table(tableName, schema);
- var unvalidatedTypes = new HashSet(mappedTypes);
+ // TODO: add proper validation for json-mapped types instead of filtering them out
+ var unvalidatedTypes = new HashSet(mappedTypes.Where(x => !x.MappedToJson()));
IEntityType? root = null;
foreach (var mappedType in mappedTypes)
{
@@ -410,6 +417,13 @@ protected virtual void ValidateSharedTableCompatibility(
continue;
}
+ // HACK (do it properly, i.e. add some validation to this scenario, not everything should be allowed presumably)
+ // owned types mapped to json can share table with parent even in 1-many scenario
+ if (mappedType.MappedToJsonColumnName() != null)
+ {
+ continue;
+ }
+
var primaryKey = mappedType.FindPrimaryKey();
if (primaryKey != null
&& (mappedType.FindForeignKeys(primaryKey.Properties)
@@ -710,9 +724,20 @@ protected virtual void ValidateSharedColumnsCompatibility(
}
}
+ // TODO: add validation for json columns (JsonColumn has mappings for individual properties for the entire owned structure)
+ if (entityType.MappedToJson())
+ {
+ continue;
+ }
+
foreach (var property in entityType.GetDeclaredProperties())
{
var columnName = property.GetColumnName(storeObject)!;
+ if (columnName == null)
+ {
+ continue;
+ }
+
missingConcurrencyTokens?.Remove(columnName);
if (!propertyMappings.TryGetValue(columnName, out var duplicateProperty))
{
diff --git a/src/EFCore.Relational/Infrastructure/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Infrastructure/RelationalPropertyExtensions.cs
index 7b0eeb40cbd..47642172050 100644
--- a/src/EFCore.Relational/Infrastructure/RelationalPropertyExtensions.cs
+++ b/src/EFCore.Relational/Infrastructure/RelationalPropertyExtensions.cs
@@ -33,12 +33,24 @@ public static string FormatColumns(
foreach (var property in properties)
{
var columnName = property.GetColumnName(storeObject);
- if (columnName == null)
+ // entity mapped to json can have part of it's key mapped to a column (i.e. part that is FK pointing to the PK of the owner)
+ // and part that is not (i.e. synthesized key property based on ordinal in the collection)
+ if (property.DeclaringEntityType.MappedToJson())
{
- return null;
+ if (columnName != null)
+ {
+ propertyNames.Add(columnName);
+ }
}
+ else
+ {
+ if (columnName == null)
+ {
+ return null;
+ }
- propertyNames.Add(columnName);
+ propertyNames.Add(columnName);
+ }
}
return propertyNames;
diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs
index 3937f1281e1..e1675f766ce 100644
--- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs
@@ -81,6 +81,9 @@ public override ConventionSet CreateConventionSet()
conventionSet.EntityTypeBaseTypeChangedConventions.Add(checkConstraintConvention);
conventionSet.EntityTypeBaseTypeChangedConventions.Add(triggerConvention);
+ var mapToJsonConvention = new RelationalMapToJsonConvention();
+ conventionSet.EntityTypeAnnotationChangedConventions.Add(mapToJsonConvention);
+
ReplaceConvention(conventionSet.ForeignKeyPropertiesChangedConventions, valueGenerationConvention);
ReplaceConvention(conventionSet.ForeignKeyOwnershipChangedConventions, valueGenerationConvention);
diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs
index f94b4ba3068..5b044a1f4b3 100644
--- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs
@@ -45,13 +45,21 @@ public sealed record RelationalConventionSetBuilderDependencies
/// the constructor at any point in this process.
///
[EntityFrameworkInternal]
- public RelationalConventionSetBuilderDependencies(IRelationalAnnotationProvider relationalAnnotationProvider)
+ public RelationalConventionSetBuilderDependencies(
+ IRelationalAnnotationProvider relationalAnnotationProvider,
+ IRelationalTypeMappingSource relationalTypeMappingSource)
{
RelationalAnnotationProvider = relationalAnnotationProvider;
+ RelationalTypeMappingSource = relationalTypeMappingSource;
}
///
/// The relational annotation provider.
///
public IRelationalAnnotationProvider RelationalAnnotationProvider { get; init; }
+
+ ///
+ /// The relational type mapping source.
+ ///
+ public IRelationalTypeMappingSource RelationalTypeMappingSource { get; init; }
}
diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs
new file mode 100644
index 00000000000..c81a1d13664
--- /dev/null
+++ b/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
+{
+ ///
+ /// TODO
+ ///
+ public class RelationalMapToJsonConvention : IEntityTypeAnnotationChangedConvention
+ {
+ ///
+ /// TODO
+ ///
+ public void ProcessEntityTypeAnnotationChanged(
+ IConventionEntityTypeBuilder entityTypeBuilder,
+ string name,
+ IConventionAnnotation? annotation,
+ IConventionAnnotation? oldAnnotation,
+ IConventionContext context)
+ {
+ if (name == RelationalAnnotationNames.MapToJsonColumnName)
+ {
+ // TODO: if json type name was specified (via attribute) propagate it here and add it as annotation on the entity itself
+ // needed for postgres, since it has two json types
+ var tableName = entityTypeBuilder.Metadata.GetTableName();
+ if (tableName == null)
+ {
+ throw new InvalidOperationException("need table name");
+ }
+
+ var jsonColumnName = annotation?.Value as string;
+ if (!string.IsNullOrEmpty(jsonColumnName))
+ {
+ foreach (var navigation in entityTypeBuilder.Metadata.GetNavigations()
+ .Where(n => n.ForeignKey.IsOwnership
+ && n.DeclaringEntityType == entityTypeBuilder.Metadata
+ && n.TargetEntityType.IsOwned()))
+ {
+ var currentJsonColumnName = navigation.TargetEntityType.MappedToJsonColumnName();
+ if (currentJsonColumnName == null || currentJsonColumnName != jsonColumnName)
+ {
+ navigation.TargetEntityType.SetAnnotation(RelationalAnnotationNames.MapToJsonColumnName, jsonColumnName);
+ }
+ }
+ }
+ else
+ {
+ // TODO: unwind everything
+ }
+ }
+ }
+ }
+}
diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs
index 3c8dd8dd923..f5e070c59c9 100644
--- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs
@@ -52,7 +52,11 @@ protected override void ProcessModelAnnotations(
if (runtime)
{
annotations[RelationalAnnotationNames.RelationalModel] =
- RelationalModel.Create(runtimeModel, RelationalDependencies.RelationalAnnotationProvider, designTime: false);
+ RelationalModel.Create(
+ runtimeModel,
+ RelationalDependencies.RelationalAnnotationProvider,
+ RelationalDependencies.RelationalTypeMappingSource,
+ designTime: false);
}
else
{
diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
index 653887063b7..4c599191cc4 100644
--- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
@@ -205,6 +205,12 @@ private static void TryUniquifyColumnNames(
continue;
}
+ // don't uniquify json columns
+ if (entityType.MappedToJson())
+ {
+ continue;
+ }
+
if (!properties.TryGetValue(columnName, out var otherProperty))
{
properties[columnName] = property;
diff --git a/src/EFCore.Relational/Metadata/Conventions/StoreGenerationConvention.cs b/src/EFCore.Relational/Metadata/Conventions/StoreGenerationConvention.cs
index d3a29d8945a..a9a1d68e5ac 100644
--- a/src/EFCore.Relational/Metadata/Conventions/StoreGenerationConvention.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/StoreGenerationConvention.cs
@@ -120,6 +120,12 @@ protected virtual void Validate(
IConventionProperty property,
in StoreObjectIdentifier storeObject)
{
+ if (property.DeclaringEntityType.MappedToJson())
+ {
+ // maumar: skip this validation for now
+ return;
+ }
+
if (property.TryGetDefaultValue(storeObject, out _))
{
if (property.GetDefaultValueSql(storeObject) != null)
diff --git a/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs b/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs
new file mode 100644
index 00000000000..e3a4a0aeaf3
--- /dev/null
+++ b/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class JsonColumn : Column
+ {
+ private readonly Dictionary> _containedColumns = new();
+
+ ///
+ /// 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.
+ ///
+ public JsonColumn(string name, string type, Table table)
+ : base(name, type, table)
+ {
+ }
+
+ /////
+ ///// 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.
+ /////
+ //public Dictionary> ContainedColumns = new();
+
+ ///
+ /// 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.
+ ///
+ public Column? FindColumn(IForeignKey ownership, string columnName)
+ {
+ if (_containedColumns.TryGetValue(ownership, out var inner))
+ {
+ if (inner.TryGetValue(columnName, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 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.
+ ///
+ public void AddColumn(IForeignKey ownership, string columnName, Column column)
+ {
+ if (!_containedColumns.TryGetValue(ownership, out var inner))
+ {
+ inner = new Dictionary();
+ _containedColumns.Add(ownership, inner);
+ }
+
+ if (!inner.TryGetValue(columnName, out var existingColumn))
+ {
+ inner.Add(columnName, column);
+ }
+ else
+ {
+ // TODO: resource string
+ throw new InvalidOperationException("column already exists");
+ }
+ }
+ }
+}
diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs
index 96ccf2e0c18..b48d9d280e1 100644
--- a/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs
+++ b/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs
@@ -123,19 +123,28 @@ public static bool AreCompatible(
if (foreignKey.IsUnique != duplicateForeignKey.IsUnique)
{
- if (shouldThrow)
+ // in case of entities mapped to json, unique and non unique FKs can be compatible
+ // so filter out this check if the FKs we are testing are for those json mapped entities
+ // TODO: make the correct validation here - don't just allow all of those, there still could be some incompatibilities between json FKs
+ if (!foreignKey.IsOwnership
+ || !foreignKey.DeclaringEntityType.MappedToJson()
+ || !duplicateForeignKey.IsOwnership
+ || !duplicateForeignKey.DeclaringEntityType.MappedToJson())
{
- throw new InvalidOperationException(
- RelationalStrings.DuplicateForeignKeyUniquenessMismatch(
- foreignKey.Properties.Format(),
- foreignKey.DeclaringEntityType.DisplayName(),
- duplicateForeignKey.Properties.Format(),
- duplicateForeignKey.DeclaringEntityType.DisplayName(),
- foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
- foreignKey.GetConstraintName(storeObject, principalTable.Value)));
+ if (shouldThrow)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.DuplicateForeignKeyUniquenessMismatch(
+ foreignKey.Properties.Format(),
+ foreignKey.DeclaringEntityType.DisplayName(),
+ duplicateForeignKey.Properties.Format(),
+ duplicateForeignKey.DeclaringEntityType.DisplayName(),
+ foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
+ foreignKey.GetConstraintName(storeObject, principalTable.Value)));
+ }
+
+ return false;
}
-
- return false;
}
var referentialAction = RelationalModel.ToReferentialAction(foreignKey.DeleteBehavior);
diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
index 7b266507996..e5f77dd35e3 100644
--- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
+++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Text.Json;
+
namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
///
@@ -114,9 +116,16 @@ public override bool IsReadOnly
public static IModel Add(
IModel model,
IRelationalAnnotationProvider? relationalAnnotationProvider,
+ IRelationalTypeMappingSource? relationalTypeMappingSource,
bool designTime)
{
- model.AddRuntimeAnnotation(RelationalAnnotationNames.RelationalModel, Create(model, relationalAnnotationProvider, designTime));
+ model.AddRuntimeAnnotation(
+ RelationalAnnotationNames.RelationalModel,
+ Create(
+ model,
+ relationalAnnotationProvider,
+ relationalTypeMappingSource,
+ designTime));
return model;
}
@@ -129,6 +138,7 @@ public static IModel Add(
public static IRelationalModel Create(
IModel model,
IRelationalAnnotationProvider? relationalAnnotationProvider,
+ IRelationalTypeMappingSource? relationalTypeMappingSource,
bool designTime)
{
var databaseModel = new RelationalModel(model);
@@ -137,7 +147,7 @@ public static IRelationalModel Create(
{
AddDefaultMappings(databaseModel, entityType);
- AddTables(databaseModel, entityType);
+ AddTables(databaseModel, entityType, relationalTypeMappingSource);
AddViews(databaseModel, entityType);
@@ -336,7 +346,7 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp
tableMappings.Reverse();
}
- private static void AddTables(RelationalModel databaseModel, IEntityType entityType)
+ private static void AddTables(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource? relationalTypeMappingSource)
{
var tableName = entityType.GetTableName();
if (tableName == null)
@@ -379,6 +389,40 @@ private static void AddTables(RelationalModel databaseModel, IEntityType entityT
{
IsSplitEntityTypePrincipal = true
};
+
+ var mapToJsonColumnName = mappedType.MappedToJsonColumnName();
+ var jsonColumn = default(JsonColumn);
+ var ownership = default(IForeignKey);
+ if (!string.IsNullOrEmpty(mapToJsonColumnName))
+ {
+ var jsonColumnTypeName = mappedType.MappedToJsonColumnTypeName();
+ var jsonColumnTypeMapping = (RelationalTypeMapping?)mappedType.FindRuntimeAnnotationValue(RelationalAnnotationNames.MapToJsonTypeMapping);
+ if (jsonColumnTypeMapping == null)
+ {
+ jsonColumnTypeMapping = relationalTypeMappingSource!.FindMapping(
+ typeof(JsonElement),
+ jsonColumnTypeName);
+
+ if (jsonColumnTypeMapping == null)
+ {
+ throw new InvalidOperationException("Mapping for json column could not be found.");
+ }
+
+ mappedType.AddRuntimeAnnotation(RelationalAnnotationNames.MapToJsonTypeMapping, jsonColumnTypeMapping);
+ }
+
+ jsonColumn = (JsonColumn?)table.FindColumn(mapToJsonColumnName);
+ if (jsonColumn == null)
+ {
+ jsonColumn = new JsonColumn(mapToJsonColumnName, jsonColumnTypeMapping.StoreType, table);
+ table.Columns.Add(mapToJsonColumnName, jsonColumn);
+ jsonColumn.IsNullable = false;
+ }
+
+ ownership = mappedType.GetForeignKeys().Where(fk => fk.IsOwnership).Single();
+ jsonColumn.IsNullable = jsonColumn.IsNullable || !ownership.IsRequired || !ownership.IsUnique;
+ }
+
foreach (var property in mappedType.GetProperties())
{
var columnName = property.GetColumnName(mappedTable);
@@ -387,14 +431,34 @@ private static void AddTables(RelationalModel databaseModel, IEntityType entityT
continue;
}
- var column = (Column?)table.FindColumn(columnName);
+ Column? column;
+ if (jsonColumn != null && ownership != null && !property.IsPrimaryKey())
+ {
+ column = jsonColumn.FindColumn(ownership, columnName);
+ }
+ else
+ {
+ column = (Column?)table.FindColumn(columnName);
+ }
+
if (column == null)
{
+ // TODO: for json entities there is some abiguity - if there is property on the owner entity with the same name as the property on the owned, json mapped entity
+ // the columns will look the same - column name, type and table are the same, but they are two different columns!
+ // Should we add faux table for those columns or something to distinguish or just rely on property mappings to tell them apart?
column = new(columnName, property.GetColumnType(mappedTable), table)
{
IsNullable = property.IsColumnNullable(mappedTable)
};
- table.Columns.Add(columnName, column);
+
+ if (jsonColumn != null && ownership != null && !property.IsPrimaryKey())
+ {
+ jsonColumn.AddColumn(ownership, columnName, column);
+ }
+ else
+ {
+ table.Columns.Add(columnName, column);
+ }
}
else if (!property.IsColumnNullable(mappedTable))
{
@@ -962,9 +1026,18 @@ private static void PopulateRowInternalForeignKeys(TableBase tab
}
SortedSet? rowInternalForeignKeys = null;
- foreach (var foreignKey in entityType.FindForeignKeys(primaryKey.Properties))
+
+ // for json mapped entities filter out collection key
+ // TODO: only do it for entities that are collections (and not references)
+ // TODO2: what about 3 level deep hierarchy (for middle entity PK is also FK), should we just chop off the last property of the collection key?
+ var foreignKeys = entityType.MappedToJson()
+ ? entityType.FindForeignKeys(primaryKey.Properties.Where(p => p.IsForeignKey()).ToList().AsReadOnly())
+ : entityType.FindForeignKeys(primaryKey.Properties);
+
+ foreach (var foreignKey in foreignKeys)
{
- if (foreignKey.IsUnique
+ // for json mapped entities we can have row internal FKs for collection navigations
+ if ((foreignKey.IsUnique || entityType.MappedToJson())
&& foreignKey.PrincipalKey.IsPrimaryKey()
&& !foreignKey.DeclaringEntityType.IsAssignableFrom(foreignKey.PrincipalEntityType)
&& !foreignKey.PrincipalEntityType.IsAssignableFrom(foreignKey.DeclaringEntityType)
diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
index 2bcc0a903c9..31c623c4bfa 100644
--- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
+++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
@@ -258,4 +258,19 @@ public static class RelationalAnnotationNames
/// The name for the reader get value delegate annotations.
///
public const string GetReaderFieldValue = Prefix + "GetReaderFieldValue";
+
+ ///
+ /// The name for the annotation specifying JSON column name to which the object is mapped.
+ ///
+ public const string MapToJsonColumnName = Prefix + "MapToJsonColumnName";
+
+ ///
+ /// The name for the annotation specifying JSON column type name to which the object is mapped.
+ ///
+ public const string MapToJsonColumnTypeName = Prefix + "MapToJsonColumnTypeName";
+
+ ///
+ /// The name for the annotation specifying JSON column type mapping.
+ ///
+ public const string MapToJsonTypeMapping = Prefix + "MapToJsonTypeMapping";
}
diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
index 941add0b3ad..404eef67f45 100644
--- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
+++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
@@ -5,6 +5,7 @@
using System.Globalization;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Update.Internal;
namespace Microsoft.EntityFrameworkCore.Migrations.Internal;
@@ -686,6 +687,7 @@ private static IEnumerable GetSortedColumns(ITable table)
{
var columns = table.Columns.ToHashSet();
var sortedColumns = new List(columns.Count);
+
foreach (var property in GetSortedProperties(GetMainType(table).GetRootType(), table))
{
var column = table.FindColumn(property)!;
@@ -695,6 +697,11 @@ private static IEnumerable GetSortedColumns(ITable table)
}
}
+ //// TODO: how to do this properly?
+ //Check.DebugAssert(
+ // columns.Count(x => x.FindAnnotation(RelationalAnnotationNames.MapToJsonColumnName)?.Value != null) == 0,
+ // "columns is not empty");
+
Check.DebugAssert(columns.Count == 0, "columns is not empty");
return sortedColumns.Where(c => c.Order.HasValue).OrderBy(c => c.Order)
@@ -1062,6 +1069,47 @@ protected virtual IEnumerable Add(
operation, target, targetTypeMapping, target.IsNullable,
target.GetAnnotations(), inline);
+ //// maumar: DRY this! also identify json column properly (with annotation or something)
+ //if (target.PropertyMappings.Count() == 0)
+ //{
+ // var targetTypeMapping = TypeMappingSource.GetMapping(target.StoreType);
+
+ // operation.ClrType = targetTypeMapping.ClrType.UnwrapNullableType();
+
+ // if (!target.TryGetDefaultValue(out var defaultValue))
+ // {
+ // defaultValue = null;
+ // }
+
+ // operation.ColumnType = target.StoreType;
+ // operation.MaxLength = null;// target.MaxLength;
+ // operation.Precision = null;// target.Precision;
+ // operation.Scale = null;//target.Scale;
+ // operation.IsUnicode = null;// target.IsUnicode;
+ // operation.IsFixedLength = null;// target.IsFixedLength;
+ // operation.IsRowVersion = false;// target.IsRowVersion;
+ // operation.IsNullable = target.IsNullable;
+ // operation.DefaultValue = defaultValue
+ // ?? (inline || target.IsNullable
+ // ? null
+ // : GetDefaultValue(operation.ClrType));
+ // operation.DefaultValueSql = null;// target.DefaultValueSql;
+ // operation.ComputedColumnSql = null;// target.ComputedColumnSql;
+ // operation.IsStored = null;// target.IsStored;
+ // operation.Comment = null;// target.Comment;
+ // operation.Collation = null;// target.Collation;
+ // operation.AddAnnotations(target.GetAnnotations());
+ //}
+ //else
+ //{
+ // var targetMapping = target.PropertyMappings.First();
+ // var targetTypeMapping = targetMapping.TypeMapping;
+
+ // Initialize(
+ // operation, target, targetTypeMapping, target.IsNullable,
+ // target.GetAnnotations(), inline);
+ //}
+
if (!inline && target.Order.HasValue)
{
operation.AddAnnotation(RelationalAnnotationNames.ColumnOrder, target.Order.Value);
diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs
index 5022ca16d4b..670886cb7da 100644
--- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs
@@ -138,14 +138,50 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio
throw new InvalidOperationException(CoreStrings.TranslationFailed(projectionBindingExpression.Print()));
case MaterializeCollectionNavigationExpression materializeCollectionNavigationExpression:
+ if (materializeCollectionNavigationExpression.Navigation.TargetEntityType.MappedToJson()
+ && materializeCollectionNavigationExpression.Subquery is MethodCallExpression methodCallSubquery
+ && methodCallSubquery.Method.IsGenericMethod)
+ {
+ // strip .Select(x => x) and .AsQueryable() from the JsonCollectionResultExpression
+ if (methodCallSubquery.Method.GetGenericMethodDefinition() == QueryableMethods.Select
+ && methodCallSubquery.Arguments[0] is MethodCallExpression selectSourceMethod
+ && selectSourceMethod.Method.IsGenericMethod
+ && methodCallSubquery.Arguments[1].UnwrapLambdaFromQuote() is LambdaExpression selectLambda
+ // if this MCNE is part of json include chain, there could me more json includes below - we need to prune them
+ // to get to the root parameter
+ && RemoveJsonIncludes(selectLambda.Body) == selectLambda.Parameters[0])
+ {
+ methodCallSubquery = selectSourceMethod;
+ }
+
+ if (methodCallSubquery.Method.IsGenericMethod
+ && methodCallSubquery.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable
+ && methodCallSubquery.Arguments[0] is JsonCollectionResultExpression jcre)
+ {
+ return Visit(jcre);
+ }
+
+ throw new InvalidOperationException(CoreStrings.TranslationFailed(materializeCollectionNavigationExpression.Print()));
+ }
+
_clientProjections!.Add(
_queryableMethodTranslatingExpressionVisitor.TranslateSubquery(
materializeCollectionNavigationExpression.Subquery)!);
+
return new CollectionResultExpression(
// expression.Type will be CLR type of the navigation here so that is fine.
new ProjectionBindingExpression(_selectExpression, _clientProjections.Count - 1, expression.Type),
materializeCollectionNavigationExpression.Navigation,
materializeCollectionNavigationExpression.Navigation.ClrType.GetSequenceType());
+
+ case JsonCollectionResultExpression jsonCollectionResultExpression:
+
+ _clientProjections!.Add(jsonCollectionResultExpression.JsonProjectionExpression);
+
+ return new CollectionResultExpression(
+ new ProjectionBindingExpression(_selectExpression, _clientProjections!.Count - 1, jsonCollectionResultExpression.Navigation.ClrType),
+ jsonCollectionResultExpression.Navigation,
+ jsonCollectionResultExpression.Navigation.ClrType.GetSequenceType());
}
var translation = _sqlTranslator.Translate(expression);
@@ -214,6 +250,12 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio
}
return base.Visit(expression);
+
+ static Expression RemoveJsonIncludes(Expression expression)
+ => expression is IncludeExpression includeExpression
+ && includeExpression.Navigation.DeclaringEntityType.MappedToJson()
+ ? RemoveJsonIncludes(includeExpression.EntityExpression)
+ : expression;
}
///
@@ -267,6 +309,21 @@ protected override Expression VisitExtension(Expression extensionExpression)
{
// TODO: Make this easier to understand some day.
EntityProjectionExpression entityProjectionExpression;
+
+ if (entityShaperExpression.ValueBufferExpression is JsonProjectionExpression jsonProjectionExpression)
+ {
+ if (_indexBasedBinding)
+ {
+ var projectionBinding = AddClientProjection(jsonProjectionExpression, typeof(ValueBuffer));
+
+ return entityShaperExpression.Update(projectionBinding);
+ }
+ else
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+ }
+
if (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression)
{
if (projectionBindingExpression.ProjectionMember == null
@@ -303,8 +360,19 @@ protected override Expression VisitExtension(Expression extensionExpression)
new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer)));
}
- case IncludeExpression:
- return _indexBasedBinding ? base.VisitExtension(extensionExpression) : QueryCompilationContext.NotTranslatedExpression;
+ case IncludeExpression includeExpression:
+ {
+ if (_indexBasedBinding)
+ {
+ // we prune nested json includes - we only need the first level of include so that we know the json column
+ // and the json entity that is the start of the include chain - the rest will be added in the shaper phase
+ return includeExpression.Navigation.DeclaringEntityType.MappedToJson()
+ ? Visit(includeExpression.EntityExpression)
+ : base.VisitExtension(extensionExpression);
+ }
+
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
case CollectionResultExpression collectionResultExpression:
{
diff --git a/src/EFCore.Relational/Query/JsonCollectionResultExpression.cs b/src/EFCore.Relational/Query/JsonCollectionResultExpression.cs
new file mode 100644
index 00000000000..fbf717f5847
--- /dev/null
+++ b/src/EFCore.Relational/Query/JsonCollectionResultExpression.cs
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query
+{
+ ///
+ /// TODO
+ ///
+ public class JsonCollectionResultExpression : Expression, IPrintableExpression
+ {
+ ///
+ /// TODO
+ ///
+ public JsonCollectionResultExpression(
+ JsonProjectionExpression jsonProjectionExpression,
+ INavigation navigation,
+ Type elementType)
+ {
+ JsonProjectionExpression = jsonProjectionExpression;
+ Navigation = navigation;
+ ElementType = elementType;
+ }
+
+ ///
+ /// TODO
+ ///
+ public virtual JsonProjectionExpression JsonProjectionExpression { get; }
+
+ ///
+ /// TODO
+ ///
+ public virtual INavigation Navigation { get; }
+
+ ///
+ /// TODO
+ ///
+ public virtual Type ElementType { get; }
+
+ ///
+ public override Type Type
+ => typeof(IEnumerable<>).MakeGenericType(ElementType);
+
+ ///
+ public override ExpressionType NodeType
+ => ExpressionType.Extension;
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var jsonProjectionExpression = (JsonProjectionExpression)visitor.Visit(JsonProjectionExpression);
+
+ return Update(jsonProjectionExpression);
+ }
+
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ public virtual JsonCollectionResultExpression Update(JsonProjectionExpression jsonProjectionExpression)
+ => jsonProjectionExpression != JsonProjectionExpression
+ ? new JsonCollectionResultExpression(jsonProjectionExpression, Navigation, ElementType)
+ : this;
+
+ ///
+ public virtual void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.AppendLine("JsonCollectionResultExpression:");
+ using (expressionPrinter.Indent())
+ {
+ expressionPrinter.Append("JsonProjectionExpression:");
+ expressionPrinter.Visit(JsonProjectionExpression);
+ expressionPrinter.AppendLine();
+ if (Navigation != null)
+ {
+ expressionPrinter.Append("Navigation:").AppendLine(Navigation.ToString()!);
+ }
+
+ expressionPrinter.Append("ElementType:").AppendLine(ElementType.ShortDisplayName());
+ }
+ }
+ }
+}
diff --git a/src/EFCore.Relational/Query/JsonProjectionExpression.cs b/src/EFCore.Relational/Query/JsonProjectionExpression.cs
new file mode 100644
index 00000000000..88cfc6544d9
--- /dev/null
+++ b/src/EFCore.Relational/Query/JsonProjectionExpression.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.Query
+{
+ ///
+ /// TODO
+ ///
+ public class JsonProjectionExpression : Expression
+ {
+ ///
+ /// TODO
+ ///
+ public JsonProjectionExpression(IEntityType entityType, JsonPathExpression jsonPathExpression, bool isCollection)
+ {
+ EntityType = entityType;
+ JsonPathExpression = jsonPathExpression;
+ IsCollection = isCollection;
+ }
+
+ ///
+ /// The entity type being projected out.
+ ///
+ public virtual IEntityType EntityType { get; }
+
+ ///
+ /// TODO
+ ///
+ public virtual JsonPathExpression JsonPathExpression { get; }
+
+ ///
+ /// TODO
+ ///
+ public bool IsCollection { get; }
+
+ ///
+ public sealed override ExpressionType NodeType
+ => ExpressionType.Extension;
+
+ ///
+ public override Type Type
+ => IsCollection ? typeof(IEnumerable<>).MakeGenericType(EntityType.ClrType) : EntityType.ClrType;
+
+ ///
+ /// TODO
+ ///
+ public virtual JsonProjectionExpression BuildJsonProjectionExpressionForNavigation(INavigation navigation)
+ {
+ var pathSegment = navigation.Name;
+ var entityType = navigation.TargetEntityType;
+
+ var newPath = JsonPathExpression.JsonPath.ToList();
+ newPath.Add(pathSegment);
+
+ var newKeyPropertyMap = new Dictionary();
+ var primaryKey = entityType.FindPrimaryKey();
+ if (primaryKey == null || primaryKey.Properties.Count < JsonPathExpression.KeyPropertyMap.Count)
+ {
+ // TODO: debug or remove
+ throw new InvalidOperationException("shouldnt happen");
+ }
+
+ // TODO: fix this (i.e. key property map should be sorted in the first place List ??)
+ var oldValues = JsonPathExpression.KeyPropertyMap.Values.ToList();
+
+ for (var i = 0; i < oldValues.Count; i++)
+ {
+ newKeyPropertyMap[primaryKey.Properties[i]] = oldValues[i];
+ }
+
+ var newJsonPathExpression = new JsonPathExpression(
+ JsonPathExpression.JsonColumn,
+ JsonPathExpression.Type,
+ JsonPathExpression.TypeMapping,
+ newKeyPropertyMap,
+ newPath);
+
+ return new JsonProjectionExpression(entityType, newJsonPathExpression, navigation.IsCollection);
+ }
+
+ ///
+ /// TODO
+ ///
+ public virtual SqlExpression BindProperty(IProperty property)
+ {
+ if (JsonPathExpression.KeyPropertyMap.TryGetValue(property, out var keyColumn))
+ {
+ return keyColumn;
+ }
+
+ var pathSegment = property.Name;
+ var newPath = JsonPathExpression.JsonPath.ToList();
+ newPath.Add(pathSegment);
+
+ return new JsonPathExpression(
+ JsonPathExpression.JsonColumn,
+ property.ClrType,
+ property.FindRelationalTypeMapping(), // TODO: use column information we should have somewhere
+ new Dictionary(),
+ newPath);
+ }
+ }
+}
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index 626b2e8e15e..88efb043674 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -1251,4 +1251,8 @@ protected override Expression VisitUnion(UnionExpression unionExpression)
return unionExpression;
}
+
+ ///
+ protected override Expression VisitJsonPathExpression(JsonPathExpression jsonPathExpression)
+ => jsonPathExpression;
}
diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
index a4123902336..7690d08423e 100644
--- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
+++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Text.Json;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace Microsoft.EntityFrameworkCore.Query;
@@ -100,6 +101,12 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType
{
// Optional dependent
var valueBufferParameter = baseCondition.Parameters[0];
+ if (entityType.MappedToJson())
+ {
+ // for json entities, conditional materialization for optional dependent is handled in the materialize method itself
+ return baseCondition;
+ }
+
var condition = entityType.GetNonPrincipalSharedNonPkProperties(table)
.Where(e => !e.IsNullable)
.Select(
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index c1f94c9a06f..2284f35b7ff 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -151,6 +151,9 @@ when queryRootExpression.GetType() == typeof(QueryRootExpression)
new QueryExpressionReplacingExpressionVisitor(shapedQueryExpression.QueryExpression, clonedSelectExpression)
.Visit(shapedQueryExpression.ShaperExpression));
+ case JsonCollectionResultExpression jsonCollectionResultExpression:
+ return jsonCollectionResultExpression;
+
default:
return base.VisitExtension(extensionExpression);
}
@@ -1080,10 +1083,58 @@ protected override Expression VisitExtension(Expression extensionExpression)
return null;
}
+ if (entityShaperExpression.ValueBufferExpression is JsonProjectionExpression jsonProjection)
+ {
+ // accessing json entity in the query rather than from auto-include expansion
+ var newJsonProjection = jsonProjection.BuildJsonProjectionExpressionForNavigation(navigation);
+ if (navigation.IsCollection)
+ {
+ return new JsonCollectionResultExpression(newJsonProjection, navigation, targetEntityType.ClrType);
+ }
+ else
+ {
+ var newShaper = new RelationalEntityShaperExpression(targetEntityType, newJsonProjection, nullable: true);
+
+ return newShaper;
+
+ //// ???
+ //return doee is not null
+ // ? doee.AddNavigation(targetEntityType, navigation)
+ // : new DeferredOwnedExpansionExpression(
+ // targetEntityType,
+ // (ProjectionBindingExpression)newShaper.ValueBufferExpression,
+ // navigation);
+ }
+ }
+
var entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression);
+
var foreignKey = navigation.ForeignKey;
if (navigation.IsCollection)
{
+ if (targetEntityType.MappedToJson())
+ {
+ var table = FindTable(navigation, entityType);
+ var identifyingColumn = entityProjectionExpression.BindProperty(entityType.FindPrimaryKey()!.Properties.First());
+ var principalNullable = identifyingColumn.IsNullable;
+
+ var jsonColumnName = targetEntityType.GetAnnotation(RelationalAnnotationNames.MapToJsonColumnName).Value as string;
+ var jsonColumnTypeMapping = targetEntityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.MapToJsonTypeMapping) as RelationalTypeMapping;
+
+ var jsonColumn = table.Columns.Single(x => x.Name == jsonColumnName);
+
+ var jsonProjectionExpression = _selectExpression.GenerateJsonProjectionExpression(
+ targetEntityType,
+ jsonColumnName!,
+ jsonColumnTypeMapping!,
+ table,
+ identifyingColumn.Table,
+ principalNullable,
+ isCollection: true);
+
+ return new JsonCollectionResultExpression(jsonProjectionExpression, navigation, targetEntityType.ClrType);
+ }
+
var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
entityProjectionExpression,
targetEntityType.GetViewOrTableMappings().Single().Table,
@@ -1141,16 +1192,7 @@ outerKey is NewArrayExpression newArrayExpression
var innerShaper = entityProjectionExpression.BindNavigation(navigation);
if (innerShaper == null)
{
- // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630
- // So there is no handling for dependent having TPT/TPC
- // If navigation is defined on derived type and entity type is part of TPT then we need to get ITableBase for derived type.
- // TODO: The following code should also handle Function and SqlQuery mappings
- var table = navigation.DeclaringEntityType.BaseType == null
- || entityType.FindDiscriminatorProperty() != null
- ? navigation.DeclaringEntityType.GetViewOrTableMappings().Single().Table
- : navigation.DeclaringEntityType.GetViewOrTableMappings().Select(tm => tm.Table)
- .Except(navigation.DeclaringEntityType.BaseType.GetViewOrTableMappings().Select(tm => tm.Table))
- .Single();
+ var table = FindTable(navigation, entityType);
if (table.GetReferencingRowInternalForeignKeys(foreignKey.PrincipalEntityType).Contains(foreignKey) == true)
{
// Mapped to same table
@@ -1164,12 +1206,32 @@ outerKey is NewArrayExpression newArrayExpression
|| (entityType.FindDiscriminatorProperty() == null
&& navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType));
- var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression(
- targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable);
-
- if (entityProjection != null)
+ if (targetEntityType.MappedToJson())
{
- innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable);
+ var jsonColumnName = targetEntityType.MappedToJsonColumnName();
+ var jsonColumnTypeMapping = targetEntityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.MapToJsonTypeMapping) as RelationalTypeMapping;
+ var jsonColumn = table.Columns.Single(x => x.Name == jsonColumnName);
+
+ var jsonProjectionExpression = _selectExpression.GenerateJsonProjectionExpression(
+ targetEntityType,
+ jsonColumnName!,
+ jsonColumnTypeMapping!,
+ table,
+ identifyingColumn.Table,
+ principalNullable,
+ isCollection: false);
+
+ innerShaper = new RelationalEntityShaperExpression(targetEntityType, jsonProjectionExpression, nullable: true);
+ }
+ else
+ {
+ var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression(
+ targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable);
+
+ if (entityProjection != null)
+ {
+ innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable);
+ }
}
}
@@ -1264,6 +1326,18 @@ SelectExpression BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
return _sqlExpressionFactory.Select(targetEntityType, ownedTable);
}
+ // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630
+ // So there is no handling for dependent having TPT/TPC
+ // If navigation is defined on derived type and entity type is part of TPT then we need to get ITableBase for derived type.
+ // TODO: The following code should also handle Function and SqlQuery mappings
+ static ITableBase FindTable(INavigation navigation, IEntityType entityType)
+ => navigation.DeclaringEntityType.BaseType == null
+ || entityType.FindDiscriminatorProperty() != null
+ ? navigation.DeclaringEntityType.GetViewOrTableMappings().Single().Table
+ : navigation.DeclaringEntityType.GetViewOrTableMappings().Select(tm => tm.Table)
+ .Except(navigation.DeclaringEntityType.BaseType.GetViewOrTableMappings().Select(tm => tm.Table))
+ .Single();
+
static TableExpressionBase FindRootTableExpressionForColumn(ColumnExpression column)
{
var table = column.Table;
@@ -1362,7 +1436,7 @@ public DeferredOwnedExpansionRemovingVisitor(SelectExpression selectExpression)
{
DeferredOwnedExpansionExpression doee => UnwrapDeferredEntityProjectionExpression(doee),
// For the source entity shaper or owned collection expansion
- EntityShaperExpression or ShapedQueryExpression or GroupByShaperExpression => expression,
+ EntityShaperExpression or ShapedQueryExpression or GroupByShaperExpression or JsonProjectionExpression => expression,
_ => base.Visit(expression)
};
diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.JsonInfra.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.JsonInfra.cs
new file mode 100644
index 00000000000..ce0c60e2027
--- /dev/null
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.JsonInfra.cs
@@ -0,0 +1,391 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public partial class RelationalShapedQueryCompilingExpressionVisitor
+{
+ private sealed class JsonCollectionResultInternalExpression : Expression
+ {
+ private readonly Type _type;
+
+ public JsonCollectionResultInternalExpression(
+ JsonValueBufferExpression valueBufferExpression,
+ INavigationBase? navigation,
+ Type elementType,
+ Type type)
+ {
+ ValueBufferExpression = valueBufferExpression;
+ Navigation = navigation;
+ ElementType = elementType;
+ _type = type;
+ }
+
+ public JsonValueBufferExpression ValueBufferExpression { get; }
+
+ public INavigationBase? Navigation { get; }
+
+ public Type ElementType { get; }
+
+ public override Type Type => _type;
+ }
+
+ private sealed class JsonValueBufferExpression : Expression
+ {
+ public JsonValueBufferExpression(
+ ParameterExpression keyValuesParameter,
+ ParameterExpression jsonElementParameter,
+ Expression entityExpression,
+ INavigationBase? navigation)
+ {
+ KeyValuesParameter = keyValuesParameter;
+ JsonElementParameter = jsonElementParameter;
+ EntityExpression = entityExpression;
+ Navigation = navigation;
+ }
+
+ public ParameterExpression KeyValuesParameter { get; }
+ public ParameterExpression JsonElementParameter { get; }
+ public Expression EntityExpression { get; }
+ public INavigationBase? Navigation { get; }
+
+ public override Type Type => typeof(ValueBuffer);
+ }
+
+ private class JsonMappedEntityCompilingExpressionVisitor : ExpressionVisitor
+ {
+ private readonly MethodInfo _materializeIncludedJsonEntityMethodInfo = typeof(JsonMappedEntityCompilingExpressionVisitor).GetMethod(nameof(MaterializeIncludedJsonEntity))!;
+ private readonly MethodInfo _materializeIncludedJsonEntityCollectionMethodInfo = typeof(JsonMappedEntityCompilingExpressionVisitor).GetMethod(nameof(MaterializeIncludedJsonEntityCollection))!;
+ private readonly MethodInfo _materializeRootJsonEntityMethodInfo = typeof(JsonMappedEntityCompilingExpressionVisitor).GetMethod(nameof(MaterializeRootJsonEntity))!;
+ private readonly MethodInfo _materializeRootJsonEntityCollectionMethodInfo = typeof(JsonMappedEntityCompilingExpressionVisitor).GetMethod(nameof(MaterializeRootJsonEntityCollection))!;
+
+ private readonly MethodInfo _extractJsonPropertyMethodInfo = typeof(ShaperProcessingExpressionVisitor).GetMethod(nameof(ShaperProcessingExpressionVisitor.ExtractJsonProperty))!;
+ private readonly MethodInfo _jsonElementGetPropertyMethod = typeof(JsonElement).GetMethod(nameof(JsonElement.GetProperty), new[] { typeof(string) })!;
+
+ private readonly RelationalShapedQueryCompilingExpressionVisitor _relationalShapedQueryCompilingExpressionVisitor;
+ private readonly Dictionary _valueBufferParameterMapping = new();
+ private readonly Dictionary _materializationContextParameterMapping = new();
+
+ public JsonMappedEntityCompilingExpressionVisitor(RelationalShapedQueryCompilingExpressionVisitor relationalShapedQueryCompilingExpressionVisitor)
+ {
+ _relationalShapedQueryCompilingExpressionVisitor = relationalShapedQueryCompilingExpressionVisitor;
+ }
+
+ protected override Expression VisitBinary(BinaryExpression binaryExpression)
+ {
+ if (binaryExpression.NodeType == ExpressionType.Assign
+ && binaryExpression.Left is ParameterExpression parameterExpression
+ && parameterExpression.Type == typeof(MaterializationContext))
+ {
+ var newExpression = (NewExpression)binaryExpression.Right;
+ var valueBufferParameter = (ParameterExpression)newExpression.Arguments[0];
+ _materializationContextParameterMapping[parameterExpression] = _valueBufferParameterMapping[valueBufferParameter];
+
+ var updatedExpression = newExpression.Update(
+ new[] { Expression.Constant(ValueBuffer.Empty), newExpression.Arguments[1] });
+
+ return Expression.Assign(binaryExpression.Left, updatedExpression);
+ }
+
+ return base.VisitBinary(binaryExpression);
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ if (methodCallExpression.Method.IsGenericMethod
+ && methodCallExpression.Method.GetGenericMethodDefinition()
+ == Infrastructure.ExpressionExtensions.ValueBufferTryReadValueMethod)
+ {
+ var index = methodCallExpression.Arguments[1].GetConstantValue();
+ var property = methodCallExpression.Arguments[2].GetConstantValue()!;
+ var mappingParameter = (ParameterExpression)((MethodCallExpression)methodCallExpression.Arguments[0]).Object!;
+
+ (var keyPropertyValuesParameter, var jsonElementParameter) = _materializationContextParameterMapping[mappingParameter];
+
+ if (property.IsPrimaryKey())
+ {
+ return Expression.MakeIndex(
+ keyPropertyValuesParameter,
+ keyPropertyValuesParameter.Type.GetProperty("Item"),
+ new[] { Expression.Constant(index) });
+ }
+
+ return
+ Expression.Convert(
+ Expression.Call(
+ null,
+ _extractJsonPropertyMethodInfo,
+ jsonElementParameter,
+ Expression.Constant(property.Name),
+ Expression.Constant(property.ClrType)),
+ property.ClrType);
+ }
+
+ return base.VisitMethodCall(methodCallExpression);
+ }
+
+ protected override Expression VisitExtension(Expression extensionExpression)
+ {
+ if (extensionExpression is JsonCollectionResultInternalExpression or RelationalEntityShaperExpression)
+ {
+ var valueBufferExpression = (extensionExpression as JsonCollectionResultInternalExpression)?.ValueBufferExpression
+ ?? (JsonValueBufferExpression)((RelationalEntityShaperExpression)extensionExpression).ValueBufferExpression;
+
+ var valueBufferParameter = Expression.Parameter(typeof(ValueBuffer));
+ var keyValuesShaperLambdaParameter = Expression.Parameter(typeof(object[]));
+ var jsonElementShaperLambdaParameter = Expression.Parameter(typeof(JsonElement));
+
+ _valueBufferParameterMapping[valueBufferParameter] = (keyValuesShaperLambdaParameter, jsonElementShaperLambdaParameter);
+
+ var targetEntityType = extensionExpression is JsonCollectionResultInternalExpression
+ ? ((JsonCollectionResultInternalExpression)extensionExpression).Navigation!.TargetEntityType
+ : ((RelationalEntityShaperExpression)extensionExpression).EntityType;
+
+ var nullable = (extensionExpression as RelationalEntityShaperExpression)?.IsNullable ?? false;
+ var shaperExpression = new RelationalEntityShaperExpression(
+ targetEntityType,
+ valueBufferParameter,//TODO!!!
+ nullable);
+
+ var shaperBlockVariables = new List();
+ var shaperBlockExpressions = new List();
+
+ var injectedMaterializer = _relationalShapedQueryCompilingExpressionVisitor.InjectEntityMaterializers(shaperExpression);
+
+ var visited = (BlockExpression)Visit(injectedMaterializer);
+
+ // the result of the visitation (i.e. the owner entity) will be added to the block at the very end, once we process all it's owned navigations
+ var visitedExpressionsArray = visited.Expressions.ToArray();
+ shaperBlockVariables.AddRange(visited.Variables);
+ shaperBlockExpressions.AddRange(visitedExpressionsArray[..^1]);
+ var shaperBlockResult = visitedExpressionsArray[^1];
+
+ foreach (var ownedNavigation in targetEntityType.GetNavigations().Where(
+ n => n.TargetEntityType.MappedToJson() && n.ForeignKey.IsOwnership && n == n.ForeignKey.PrincipalToDependent))
+ {
+ var innerJsonElement = Expression.Variable(
+ typeof(JsonElement));
+
+ shaperBlockVariables.Add(innerJsonElement);
+
+ // TODO: do TryGetProperty and short circuit if failed instead
+ var innerJsonElementAssignment = Expression.Assign(
+ innerJsonElement,
+ Expression.Call(jsonElementShaperLambdaParameter, _jsonElementGetPropertyMethod, Expression.Constant(ownedNavigation.Name)));
+
+ shaperBlockExpressions.Add(innerJsonElementAssignment);
+
+ if (ownedNavigation.IsCollection)
+ {
+ var nestedJsonCollectionShaper = new JsonCollectionResultInternalExpression(
+ new JsonValueBufferExpression(keyValuesShaperLambdaParameter, innerJsonElement, visited.Result, ownedNavigation),
+ ownedNavigation,
+ ownedNavigation.TargetEntityType.ClrType,
+ ownedNavigation.ClrType);
+
+ var nestedResult = Visit(nestedJsonCollectionShaper);
+ shaperBlockExpressions.Add(nestedResult);
+ }
+ else
+ {
+ var nestedJsonEntityShaper = new RelationalEntityShaperExpression(
+ ownedNavigation.TargetEntityType,
+ new JsonValueBufferExpression(keyValuesShaperLambdaParameter, innerJsonElement, visited.Result, ownedNavigation),
+ nullable: false);// TODO: fix nullability
+
+ var nestedResult = Visit(nestedJsonEntityShaper);
+ shaperBlockExpressions.Add(nestedResult);
+ }
+ }
+
+ shaperBlockExpressions.Add(shaperBlockResult);
+
+ var shaperBlock = Expression.Block(
+ shaperBlockVariables,
+ shaperBlockExpressions);
+
+ var innerShaperLambda = Expression.Lambda(
+ shaperBlock,
+ QueryCompilationContext.QueryContextParameter,
+ keyValuesShaperLambdaParameter,
+ jsonElementShaperLambdaParameter);
+
+ var jsonValueBufferExpression = (extensionExpression as JsonCollectionResultInternalExpression)?.ValueBufferExpression
+ ?? (JsonValueBufferExpression)((RelationalEntityShaperExpression)extensionExpression).ValueBufferExpression;
+
+ if (valueBufferExpression.Navigation != null)
+ {
+ var fixup = ShaperProcessingExpressionVisitor.GenerateFixup(
+ valueBufferExpression.EntityExpression.Type,
+ valueBufferExpression.Navigation.TargetEntityType.ClrType,
+ valueBufferExpression.Navigation,
+ valueBufferExpression.Navigation.Inverse);
+
+ if (valueBufferExpression.Navigation.IsCollection)
+ {
+ var materializeIncludedJsonEntityCollectionMethodCall = Expression.Call(
+ null,
+ _materializeIncludedJsonEntityCollectionMethodInfo.MakeGenericMethod(
+ valueBufferExpression.EntityExpression.Type,
+ valueBufferExpression.Navigation.TargetEntityType.ClrType),
+ QueryCompilationContext.QueryContextParameter,
+ jsonValueBufferExpression.JsonElementParameter,
+ jsonValueBufferExpression.KeyValuesParameter,
+ valueBufferExpression.EntityExpression,
+ innerShaperLambda,
+ fixup);
+
+ return materializeIncludedJsonEntityCollectionMethodCall;
+ }
+
+ var entityType = valueBufferExpression.Navigation.DeclaringEntityType;
+ var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
+ ?? entityType.GetDefaultMappings().Single().Table;
+
+ var isOptionalDependent = table.IsOptional(entityType);
+ var materializeIncludedJsonEntityMethodCall = Expression.Call(
+ null,
+ _materializeIncludedJsonEntityMethodInfo.MakeGenericMethod(
+ valueBufferExpression.EntityExpression.Type,
+ valueBufferExpression.Navigation.TargetEntityType.ClrType),
+ QueryCompilationContext.QueryContextParameter,
+ jsonValueBufferExpression.JsonElementParameter,
+ jsonValueBufferExpression.KeyValuesParameter,
+ valueBufferExpression.EntityExpression,
+ Expression.Constant(isOptionalDependent),
+ innerShaperLambda,
+ fixup);
+
+ return materializeIncludedJsonEntityMethodCall;
+ }
+ else
+ {
+ if (extensionExpression is JsonCollectionResultInternalExpression jsonCollectionResultInternalExpression)
+ {
+ var materializedRootJsonEntityCollection = Expression.Call(
+ null,
+ _materializeRootJsonEntityCollectionMethodInfo.MakeGenericMethod(
+ jsonCollectionResultInternalExpression.ElementType,
+ jsonCollectionResultInternalExpression.Navigation!.ClrType),
+ QueryCompilationContext.QueryContextParameter,
+ jsonValueBufferExpression.JsonElementParameter,
+ jsonValueBufferExpression.KeyValuesParameter,
+ Expression.Constant(jsonCollectionResultInternalExpression.Navigation),
+ innerShaperLambda);
+
+ return materializedRootJsonEntityCollection;
+ }
+
+ // TODO: just remap the shaper and return it instead
+ var materializedRootJsonEntity = Expression.Call(
+ null,
+ _materializeRootJsonEntityMethodInfo.MakeGenericMethod(valueBufferExpression.EntityExpression.Type),
+ QueryCompilationContext.QueryContextParameter,
+ jsonValueBufferExpression.JsonElementParameter,
+ jsonValueBufferExpression.KeyValuesParameter,
+ innerShaperLambda);
+
+ return materializedRootJsonEntity;
+ }
+ }
+
+ return base.VisitExtension(extensionExpression);
+ }
+
+ public static void MaterializeIncludedJsonEntity(
+ QueryContext queryContext,
+ JsonElement jsonElement,
+ object[] keyPropertyValues,
+ TIncludingEntity entity,
+ bool isOptionalDependent,
+ Func innerShaper,
+ Action fixup)
+ where TIncludingEntity : class
+ where TIncludedEntity : class
+ {
+ if (jsonElement.ValueKind == JsonValueKind.Null)
+ {
+ if (isOptionalDependent)
+ {
+ return;
+ }
+ else
+ {
+ throw new InvalidOperationException("Required Json entity not found.");
+ }
+ }
+ else
+ {
+ var included = innerShaper(queryContext, keyPropertyValues, jsonElement);
+ fixup(entity, included);
+ }
+ }
+
+ public static void MaterializeIncludedJsonEntityCollection(
+ QueryContext queryContext,
+ JsonElement jsonElement,
+ object[] keyPropertyValues,
+ TIncludingEntity entity,
+ Func innerShaper,
+ Action fixup)
+ where TIncludingEntity : class
+ where TIncludedCollectionElement : class
+ {
+ var newKeyPropertyValues = new object[keyPropertyValues.Length + 1];
+ Array.Copy(keyPropertyValues, newKeyPropertyValues, keyPropertyValues.Length);
+
+ var i = 0;
+ foreach (var jsonArrayElement in jsonElement.EnumerateArray())
+ {
+ newKeyPropertyValues[^1] = ++i;
+
+ var resultElement = innerShaper(queryContext, newKeyPropertyValues, jsonArrayElement);
+
+ fixup(entity, resultElement);
+ }
+ }
+
+ public static TEntity MaterializeRootJsonEntity(
+ QueryContext queryContext,
+ JsonElement jsonElement,
+ object[] keyPropertyValues,
+ Func shaper)
+ where TEntity : class
+ {
+ var result = shaper(queryContext, keyPropertyValues, jsonElement);
+
+ return result;
+ }
+
+ public static TResult MaterializeRootJsonEntityCollection(
+ QueryContext queryContext,
+ JsonElement jsonElement,
+ object[] keyPropertyValues,
+ INavigationBase navigation,
+ Func elementShaper)
+ where TEntity : class
+ where TResult : ICollection
+ {
+ var collectionAccessor = navigation.GetCollectionAccessor();
+ var result = (TResult)collectionAccessor!.Create();
+
+ var newKeyPropertyValues = new object[keyPropertyValues.Length + 1];
+ Array.Copy(keyPropertyValues, newKeyPropertyValues, keyPropertyValues.Length);
+
+ var i = 0;
+ foreach (var jsonArrayElement in jsonElement.EnumerateArray())
+ {
+ newKeyPropertyValues[^1] = ++i;
+
+ var resultElement = elementShaper(queryContext, newKeyPropertyValues, jsonArrayElement);
+
+ result.Add(resultElement);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
index b9fca300552..9093e5b0980 100644
--- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
@@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -118,6 +119,12 @@ private readonly IDictionary _entityTypeIdentifying
private readonly IDictionary _singleEntityTypeDiscriminatorValues
= new Dictionary();
+ private readonly IDictionary _materializationContextToJsonElementVariableMap
+ = new Dictionary();
+
+ private readonly MethodInfo _extractJsonElementMethod = typeof(ShaperProcessingExpressionVisitor).GetMethod(nameof(ExtractJsonElement))!;
+ private readonly MethodInfo _extractJsonPropertyMethodInfo = typeof(ShaperProcessingExpressionVisitor).GetMethod(nameof(ExtractJsonProperty))!;
+
public ShaperProcessingExpressionVisitor(
RelationalShapedQueryCompilingExpressionVisitor parentVisitor,
SelectExpression selectExpression,
@@ -387,12 +394,105 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
return base.VisitBinary(binaryExpression);
}
+ public static JsonElement ExtractJsonElement(DbDataReader dataReader, int index, string[] additionalPath)
+ {
+ var jsonString = dataReader.GetString(index);
+ var jsonDocument = JsonDocument.Parse(jsonString);
+ var jsonElement = jsonDocument.RootElement;
+
+ foreach (var pathElement in additionalPath)
+ {
+ jsonElement = jsonElement.GetProperty(pathElement);
+ }
+
+ return jsonElement;
+ }
+
+ private (ParameterExpression, ParameterExpression, Expression, Expression) PrepareForJsonShaping(ProjectionBindingExpression projectionBindingExpression, IEntityType entityType)
+ {
+ var projectionIndex = (ValueTuple, string[]>)GetProjectionIndex(projectionBindingExpression);
+
+ var jsonColumnProjectionIndex = projectionIndex.Item1;
+ var keyPropertyIndexMap = projectionIndex.Item2;
+ var additionalPath = projectionIndex.Item3;
+
+ var jsonElementVariable = Expression.Variable(
+ typeof(JsonElement));
+
+ var keyValuesParameter = Expression.Parameter(typeof(object[]));
+ var keyValues = new Expression[keyPropertyIndexMap.Count];
+
+ var i = 0;
+ foreach (var keyProperty in entityType.FindPrimaryKey()!.Properties.Where(p => p.IsForeignKey()))
+ {
+ var projection = _selectExpression.Projection[keyPropertyIndexMap[keyProperty]];
+
+ keyValues[i] = Expression.Convert(
+ CreateGetValueExpression(
+ _dataReaderParameter,
+ keyPropertyIndexMap[keyProperty],
+ IsNullableProjection(projection),
+ projection.Expression.TypeMapping!,
+ keyProperty.ClrType,
+ keyProperty),
+ typeof(object));
+
+ i++;
+ }
+
+ var keyValuesInitialize = Expression.NewArrayInit(typeof(object), keyValues);
+ var keyValuesAssignment = Expression.Assign(keyValuesParameter, keyValuesInitialize);
+
+ var jsonElementAssignment = Expression.Assign(
+ jsonElementVariable,
+ Expression.Call(
+ null,
+ _extractJsonElementMethod,
+ _dataReaderParameter,
+ Expression.Constant(jsonColumnProjectionIndex),
+ Expression.Constant(additionalPath)));
+
+
+ return (jsonElementVariable, keyValuesParameter, jsonElementAssignment, keyValuesAssignment);
+ }
+
protected override Expression VisitExtension(Expression extensionExpression)
{
switch (extensionExpression)
{
case RelationalEntityShaperExpression entityShaperExpression:
{
+ if (entityShaperExpression.EntityType.MappedToJson())
+ {
+ // json entity at the root
+ var entityParameter = Expression.Parameter(entityShaperExpression.Type);
+ _variables.Add(entityParameter);
+
+ var (jsonElementVariable, keyValuesParameter, jsonElementAssignment, keyValuesAssignment) = PrepareForJsonShaping(
+ (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression,
+ entityShaperExpression.EntityType);
+
+ _variables.Add(jsonElementVariable);
+ _expressions.Add(jsonElementAssignment);
+
+ _variables.Add(keyValuesParameter);
+ _expressions.Add(keyValuesAssignment);
+
+ var jsonMappedEntityCompilingExpressionVisitor = new JsonMappedEntityCompilingExpressionVisitor(_parentVisitor);
+
+ var updatedEntityShaperExpression = new RelationalEntityShaperExpression(
+ entityShaperExpression.EntityType,
+ new JsonValueBufferExpression(keyValuesParameter, jsonElementVariable, entityParameter, navigation: null),
+ entityShaperExpression.IsNullable);
+
+ var myResult = jsonMappedEntityCompilingExpressionVisitor.Visit(updatedEntityShaperExpression);
+ var resultAssignment = Expression.Assign(entityParameter, myResult);
+
+ _expressions.Add(resultAssignment);
+
+ return entityParameter;
+ }
+
if (!_variableShaperMapping.TryGetValue(entityShaperExpression.ValueBufferExpression, out var accessor))
{
var entityParameter = Expression.Parameter(entityShaperExpression.Type);
@@ -432,6 +532,41 @@ protected override Expression VisitExtension(Expression extensionExpression)
return accessor;
}
+
+ case CollectionResultExpression collectionResultExpression
+ when collectionResultExpression.Navigation is INavigation navigation
+ && navigation.ForeignKey.IsOwnership && navigation.TargetEntityType.MappedToJson():
+ {
+ // json entity collection at the root
+ var entityCollectionParameter = Expression.Parameter(collectionResultExpression.Type);
+ _variables.Add(entityCollectionParameter);
+
+ var (jsonElementVariable, keyValuesParameter, jsonElementAssignment, keyValuesAssignment) = PrepareForJsonShaping(
+ collectionResultExpression.ProjectionBindingExpression,
+ navigation.TargetEntityType);
+
+ _variables.Add(jsonElementVariable);
+ _expressions.Add(jsonElementAssignment);
+
+ _variables.Add(keyValuesParameter);
+ _expressions.Add(keyValuesAssignment);
+
+ var jsonMappedEntityCompilingExpressionVisitor = new JsonMappedEntityCompilingExpressionVisitor(_parentVisitor);
+
+ var updatedCollectionResultExpression = new JsonCollectionResultInternalExpression(
+ new JsonValueBufferExpression(keyValuesParameter, jsonElementVariable, entityCollectionParameter, navigation: null),
+ collectionResultExpression.Navigation,
+ collectionResultExpression.ElementType,
+ collectionResultExpression.Type);
+
+ var myResult = jsonMappedEntityCompilingExpressionVisitor.Visit(updatedCollectionResultExpression);
+ var resultAssignment = Expression.Assign(entityCollectionParameter, myResult);
+
+ _expressions.Add(resultAssignment);
+
+ return entityCollectionParameter;
+ }
+
case ProjectionBindingExpression projectionBindingExpression
when _inline:
{
@@ -670,6 +805,33 @@ protected override Expression VisitExtension(Expression extensionExpression)
includingEntityType, relatedEntityType, navigation, inverseNavigation).Compile()),
Expression.Constant(_isTracking)));
}
+ else if (includeExpression.Navigation.TargetEntityType.MappedToJson())
+ {
+ var projectionBindingExpression = (includeExpression.NavigationExpression as CollectionResultExpression)?.ProjectionBindingExpression
+ ?? (ProjectionBindingExpression)((RelationalEntityShaperExpression)includeExpression.NavigationExpression).ValueBufferExpression;
+
+ var (jsonElementVariable, keyValuesParameter, jsonElementAssignment, keyValuesAssignment) = PrepareForJsonShaping(
+ projectionBindingExpression,
+ includeExpression.Navigation.TargetEntityType);
+
+ _variables.Add(jsonElementVariable);
+ _expressions.Add(jsonElementAssignment);
+
+ _variables.Add(keyValuesParameter);
+ _expressions.Add(keyValuesAssignment);
+
+ var jsonMappedEntityCompilingExpressionVisitor = new JsonMappedEntityCompilingExpressionVisitor(_parentVisitor);
+
+ var updatedValueBufferExpression = new JsonValueBufferExpression(keyValuesParameter, jsonElementVariable, entity, includeExpression.Navigation);
+ var updatedNavigationExpression = includeExpression.NavigationExpression is CollectionResultExpression cre
+ ? (Expression)new JsonCollectionResultInternalExpression(updatedValueBufferExpression, cre.Navigation, cre.ElementType, cre.Type)
+ : ((RelationalEntityShaperExpression)includeExpression.NavigationExpression).Update(updatedValueBufferExpression);
+
+ var myResult = jsonMappedEntityCompilingExpressionVisitor.Visit(updatedNavigationExpression);
+ _expressions.Add(myResult);
+
+ return entity;
+ }
else
{
var navigationExpression = Visit(includeExpression.NavigationExpression);
@@ -882,6 +1044,37 @@ protected override Expression VisitExtension(Expression extensionExpression)
return base.VisitExtension(extensionExpression);
}
+ public static object? ExtractJsonProperty(JsonElement element, string propertyName, Type returnType)
+ {
+ var jsonElementProperty = element.GetProperty(propertyName);
+ if (returnType == typeof(int))
+ {
+ return jsonElementProperty.GetInt32();
+ }
+ if (returnType == typeof(DateTime))
+ {
+ return jsonElementProperty.GetDateTime();
+ }
+ if (returnType == typeof(bool))
+ {
+ return jsonElementProperty.GetBoolean();
+ }
+ if (returnType == typeof(decimal))
+ {
+ return jsonElementProperty.GetDecimal();
+ }
+ if (returnType == typeof(string))
+ {
+ return jsonElementProperty.GetString();
+ }
+ else
+ {
+ // TODO: do for other types also
+ // later, just codegen the propery access for better perf
+ throw new InvalidOperationException("unsupported type");
+ }
+ }
+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
if (methodCallExpression.Method.IsGenericMethod
@@ -890,46 +1083,87 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
{
var property = methodCallExpression.Arguments[2].GetConstantValue();
var mappingParameter = (ParameterExpression)((MethodCallExpression)methodCallExpression.Arguments[0]).Object!;
- int projectionIndex;
- if (property == null)
+
+ if (property != null
+ && property.DeclaringEntityType.MappedToJson())
{
- // This is trying to read the computed discriminator value
- var storedInfo = _entityTypeIdentifyingExpressionInfo[mappingParameter];
- if (storedInfo is string s)
+ if (property.IsPrimaryKey())
{
- // If the value is fixed then there is single entity type and discriminator is not present in query
- // We just return the value as-is.
- return Expression.Constant(s);
+ var keyBindings = _materializationContextBindings[mappingParameter];
+ var keyProjectionIndex = keyBindings[property];
+
+ var projection = _selectExpression.Projection[keyProjectionIndex];
+ var nullable = IsNullableProjection(projection);
+
+ Check.DebugAssert(
+ !nullable || property != null || methodCallExpression.Type.IsNullableType(),
+ "For nullable reads the return type must be null unless property is specified.");
+
+ return CreateGetValueExpression(
+ _dataReaderParameter,
+ keyProjectionIndex,
+ nullable,
+ projection.Expression.TypeMapping!,
+ methodCallExpression.Type,
+ property);
}
- projectionIndex = (int)_entityTypeIdentifyingExpressionInfo[mappingParameter]
- + methodCallExpression.Arguments[1].GetConstantValue();
+ var jsonElementParameter = _materializationContextToJsonElementVariableMap[mappingParameter];
+
+ // TODO: check mapping on the property to see the *actual* json property (could be different than the c-space property name)
+ return
+ Expression.Convert(
+ Expression.Call(
+ null,
+ _extractJsonPropertyMethodInfo,
+ jsonElementParameter,
+ Expression.Constant(property.Name),
+ Expression.Constant(property.ClrType)),
+ property.ClrType);
}
else
{
- projectionIndex = _materializationContextBindings[mappingParameter][property];
- }
+ int projectionIndex;
+ if (property == null)
+ {
+ // This is trying to read the computed discriminator value
+ var storedInfo = _entityTypeIdentifyingExpressionInfo[mappingParameter];
+ if (storedInfo is string s)
+ {
+ // If the value is fixed then there is single entity type and discriminator is not present in query
+ // We just return the value as-is.
+ return Expression.Constant(s);
+ }
+
+ projectionIndex = (int)_entityTypeIdentifyingExpressionInfo[mappingParameter]
+ + methodCallExpression.Arguments[1].GetConstantValue();
+ }
+ else
+ {
+ projectionIndex = _materializationContextBindings[mappingParameter][property];
+ }
- var projection = _selectExpression.Projection[projectionIndex];
- var nullable = IsNullableProjection(projection);
+ var projection = _selectExpression.Projection[projectionIndex];
+ var nullable = IsNullableProjection(projection);
- Check.DebugAssert(
- !nullable || property != null || methodCallExpression.Type.IsNullableType(),
- "For nullable reads the return type must be null unless property is specified.");
+ Check.DebugAssert(
+ !nullable || property != null || methodCallExpression.Type.IsNullableType(),
+ "For nullable reads the return type must be null unless property is specified.");
- return CreateGetValueExpression(
- _dataReaderParameter,
- projectionIndex,
- nullable,
- projection.Expression.TypeMapping!,
- methodCallExpression.Type,
- property);
+ return CreateGetValueExpression(
+ _dataReaderParameter,
+ projectionIndex,
+ nullable,
+ projection.Expression.TypeMapping!,
+ methodCallExpression.Type,
+ property);
+ }
}
return base.VisitMethodCall(methodCallExpression);
}
- private static LambdaExpression GenerateFixup(
+ internal static LambdaExpression GenerateFixup(
Type entityType,
Type relatedEntityType,
INavigationBase navigation,
diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
index c2e18b64437..06e78edf83e 100644
--- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -51,9 +53,12 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery
var querySplittingBehavior = ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior;
var splitQuery = querySplittingBehavior == QuerySplittingBehavior.SplitQuery;
var collectionCount = 0;
- var shaper = new ShaperProcessingExpressionVisitor(this, selectExpression, _tags, splitQuery, nonComposedFromSql).ProcessShaper(
+
+ var shaperProcessingExpressionVisitor = new ShaperProcessingExpressionVisitor(this, selectExpression, _tags, splitQuery, nonComposedFromSql);
+ var shaper = shaperProcessingExpressionVisitor.ProcessShaper(
shapedQueryExpression.ShaperExpression,
out var relationalCommandCache, out var readerColumns, out var relatedDataLoaders, ref collectionCount);
+
if (querySplittingBehavior == null
&& collectionCount > 1)
{
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 4db87cba93a..978a5a60c69 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -1302,6 +1302,11 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
{
if (entityReferenceExpression.ParameterEntity != null)
{
+ if (entityReferenceExpression.ParameterEntity.ValueBufferExpression is JsonProjectionExpression jsonProjectionExpression)
+ {
+ return jsonProjectionExpression.BindProperty(property);
+ }
+
var valueBufferExpression = Visit(entityReferenceExpression.ParameterEntity.ValueBufferExpression);
var entityProjectionExpression = (EntityProjectionExpression)valueBufferExpression;
diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
index eeddeae5ab0..53dd11ac6ea 100644
--- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
@@ -63,6 +63,7 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping),
SqlConstantExpression e => e.ApplyTypeMapping(typeMapping),
SqlFragmentExpression e => e,
+ // maumar - apply default mapping should apply it to arguments also, like we do for other expressions
SqlFunctionExpression e => e.ApplyTypeMapping(typeMapping),
SqlParameterExpression e => e.ApplyTypeMapping(typeMapping),
SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping),
diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
index c4872f20a41..edcdfff6b6e 100644
--- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
@@ -113,6 +113,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
case UnionExpression unionExpression:
return VisitUnion(unionExpression);
+
+ case JsonPathExpression jsonPathExpression:
+ return VisitJsonPathExpression(jsonPathExpression);
}
return base.VisitExtension(extensionExpression);
@@ -328,4 +331,11 @@ protected override Expression VisitExtension(Expression extensionExpression)
/// The expression to visit.
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
protected abstract Expression VisitUnion(UnionExpression unionExpression);
+
+ ///
+ /// Visits the children of the json path expression.
+ ///
+ /// The expression to visit.
+ /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ protected abstract Expression VisitJsonPathExpression(JsonPathExpression jsonPathExpression);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/JsonPathExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/JsonPathExpression.cs
new file mode 100644
index 00000000000..9de61d7746a
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/JsonPathExpression.cs
@@ -0,0 +1,116 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions
+{
+ ///
+ /// TODO
+ ///
+ public class JsonPathExpression : SqlExpression
+ {
+// private readonly List _jsonPath;
+
+ ///
+ /// TODO
+ ///
+ public virtual ColumnExpression JsonColumn { get; }
+
+ ///
+ /// TODO
+ ///
+ public virtual IReadOnlyList JsonPath { get; }
+
+ ///
+ /// TODO
+ ///
+ public virtual IReadOnlyDictionary KeyPropertyMap { get; } // TODO: this should be sorted!
+
+ ///
+ /// TODO
+ ///
+ public JsonPathExpression(
+ ColumnExpression jsonColumn,
+ Type type,
+ RelationalTypeMapping? typeMapping,
+ IReadOnlyDictionary keyPropertyMap)
+ : this(jsonColumn, type, typeMapping, keyPropertyMap, new List())
+ {
+ }
+
+ ///
+ /// TODO
+ ///
+ public JsonPathExpression(
+ ColumnExpression jsonColumn,
+ Type type,
+ RelationalTypeMapping? typeMapping,
+ IReadOnlyDictionary keyPropertyMap,
+ List jsonPath)
+ : base(type, typeMapping)
+ {
+ JsonColumn = jsonColumn;
+ KeyPropertyMap = keyPropertyMap;
+ JsonPath = jsonPath.AsReadOnly();
+ }
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn);
+ var keyPropertyMapChanged = false;
+
+ var newKeyPropertyMap = new Dictionary();
+ foreach (var keyPropertyMapElement in KeyPropertyMap)
+ {
+ var newColumn = (ColumnExpression)visitor.Visit(keyPropertyMapElement.Value);
+ if (newColumn != keyPropertyMapElement.Value)
+ {
+ newKeyPropertyMap[keyPropertyMapElement.Key] = newColumn;
+ keyPropertyMapChanged = true;
+ }
+ else
+ {
+ newKeyPropertyMap[keyPropertyMapElement.Key] = keyPropertyMapElement.Value;
+ }
+ }
+
+ return jsonColumn != JsonColumn || keyPropertyMapChanged
+ ? new JsonPathExpression(jsonColumn, Type, TypeMapping, newKeyPropertyMap)
+ : this;
+ }
+
+ ///
+ /// TODO
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("SqlPathExpression(column: " + JsonColumn.Name + " Path: " + string.Join(".", JsonPath) + ")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is JsonPathExpression jsonPathExpression)
+ {
+ var result = true;
+ result = result && JsonColumn.Equals(jsonPathExpression.JsonColumn);
+ result = result && JsonPath.Count == jsonPathExpression.JsonPath.Count;
+
+ if (result)
+ {
+ result = result && JsonPath.Zip(jsonPathExpression.JsonPath, (l, r) => l == r).All(x => true);
+ }
+
+ return result;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), JsonColumn, JsonPath);
+ }
+}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index 06d036d5585..63c46be114c 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
+using System.Text.Json;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
@@ -628,6 +629,8 @@ public Expression ApplyProjection(
var pushdownOccurred = false;
var containsCollection = false;
var containsSingleResult = false;
+ var jsonClientProjectionsCount = 0;
+
foreach (var projection in _clientProjections)
{
if (projection is ShapedQueryExpression sqe)
@@ -643,6 +646,11 @@ public Expression ApplyProjection(
containsSingleResult = true;
}
}
+
+ if (projection is JsonProjectionExpression)
+ {
+ jsonClientProjectionsCount++;
+ }
}
if (containsSingleResult
@@ -697,6 +705,30 @@ static void UpdateLimit(SelectExpression selectExpression)
}
}
+ var jsonClientProjectionDeduduplicationMap = new Dictionary>();
+ if (jsonClientProjectionsCount > 0)
+ {
+ var ordered = _clientProjections
+ .OfType()
+ .OrderBy(x => $"{x.JsonPathExpression.JsonColumn.TableAlias}.{x.JsonPathExpression.JsonColumn.Name}")
+ .ThenBy(x => x.JsonPathExpression.JsonPath.Count);
+
+ foreach (var orderedElement in ordered)
+ {
+ var match = jsonClientProjectionDeduduplicationMap.FirstOrDefault(x => JsonEntityContainedIn(x.Key, orderedElement));
+ if (match.Key == null)
+ {
+ jsonClientProjectionDeduduplicationMap[orderedElement] = new List { orderedElement };
+ }
+ else
+ {
+ match.Value.Add(orderedElement);
+ }
+ }
+
+ // TODO: find which projections map to which json columns
+ }
+
var earlierClientProjectionCount = _clientProjections.Count;
var newClientProjections = new List();
var clientProjectionIndexMap = new List