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(); @@ -725,6 +757,13 @@ static void UpdateLimit(SelectExpression selectExpression) break; } + case JsonProjectionExpression jsonProjectionExpression: + var jsonProjectionResult = AddJsonProjection(jsonProjectionExpression, jsonClientProjectionDeduduplicationMap); + newClientProjections.Add(jsonProjectionResult); + clientProjectionIndexMap.Add(newClientProjections.Count - 1); + + break; + case SqlExpression sqlExpression: { var result = Constant(AddToProjection(sqlExpression, _aliasForClientProjections[i])); @@ -1191,7 +1230,9 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express { result[projectionMember] = expression is EntityProjectionExpression entityProjection ? AddEntityProjection(entityProjection) - : Constant(AddToProjection((SqlExpression)expression, projectionMember.Last?.Name)); + : expression is JsonProjectionExpression jsonProjectionExpression + ? AddJsonProjection(jsonProjectionExpression, null) + : Constant(AddToProjection((SqlExpression)expression, projectionMember.Last?.Name)); } _projectionMapping.Clear(); @@ -1213,8 +1254,75 @@ ConstantExpression AddEntityProjection(EntityProjectionExpression entityProjecti AddToProjection(entityProjectionExpression.DiscriminatorExpression, DiscriminatorColumnAlias); } + if (!entityProjectionExpression.EntityType.MappedToJson()) + { + return Constant(dictionary); + } + return Constant(dictionary); } + + ConstantExpression AddJsonProjection( + JsonProjectionExpression jsonProjectionExpression, + Dictionary>? jsonClientProjectionDeduduplicationMap) + { + // TODO: this should be based on JsonPathExpression not json projection. json path is sql expression that has equals overridden + if (jsonClientProjectionDeduduplicationMap == null || jsonClientProjectionDeduduplicationMap.ContainsKey(jsonProjectionExpression)) + { + var jsonColumnIndex = AddToProjection(jsonProjectionExpression.JsonPathExpression); + + var dictionary = new Dictionary(); + foreach (var keyPropertyMapElement in jsonProjectionExpression.JsonPathExpression.KeyPropertyMap) + { + dictionary[keyPropertyMapElement.Key] = AddToProjection(keyPropertyMapElement.Value); + } + + var additionalPath = new string[0]; + + return Constant((jsonColumnIndex, dictionary, additionalPath)); + } + else + { + var match = jsonClientProjectionDeduduplicationMap.Single(e => e.Value.Contains(jsonProjectionExpression)); + var jsonColumnIndex = AddToProjection(match.Key.JsonPathExpression); + + var dictionary = new Dictionary(); + foreach (var keyPropertyMapElement in jsonProjectionExpression.JsonPathExpression.KeyPropertyMap) + { + dictionary[keyPropertyMapElement.Key] = AddToProjection(keyPropertyMapElement.Value); + } + + var additionalPath = jsonProjectionExpression.JsonPathExpression.JsonPath.Skip(match.Key.JsonPathExpression.JsonPath.Count).ToArray(); + + return Constant((jsonColumnIndex, dictionary, additionalPath)); + } + } + + static bool JsonEntityContainedIn(JsonProjectionExpression sourceExpression, JsonProjectionExpression targetExpression) + { + if (sourceExpression.JsonPathExpression.JsonColumn != targetExpression.JsonPathExpression.JsonColumn) + { + return false; + } + + var sourcePath = sourceExpression.JsonPathExpression.JsonPath.ToList(); + var targetPath = targetExpression.JsonPathExpression.JsonPath.ToList(); + + if (targetPath.Count < sourcePath.Count) + { + return false; + } + + for (var i = 0; i < sourcePath.Count; i++) + { + if (targetPath[i] != sourcePath[i]) + { + return false; + } + } + + return true; + } } /// @@ -1248,7 +1356,9 @@ public void ReplaceProjection(IReadOnlyList clientProjections) Check.DebugAssert( expression is SqlExpression || expression is EntityProjectionExpression - || expression is ShapedQueryExpression, + || expression is ShapedQueryExpression + || expression is JsonProjectionExpression + || expression is JsonCollectionResultExpression, "Invalid operation in the projection."); _clientProjections.Add(expression); _aliasForClientProjections.Add(null); @@ -2170,6 +2280,53 @@ static IReadOnlyDictionary GetPropertyExpressionsFr } } + /// + /// 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 JsonProjectionExpression GenerateJsonProjectionExpression( + IEntityType targetEntityType, + string jsonColumnName, + RelationalTypeMapping jsonColumnTypeMapping, + ITableBase table, + TableExpressionBase tableExpressionBase, + bool nullable, + bool isCollection) + { + var jsonColumn = new ConcreteColumnExpression( + jsonColumnName, + FindTableReference(this, tableExpressionBase), + typeof(JsonElement), + jsonColumnTypeMapping, + nullable: true); + + var keyPropertyMap = new Dictionary(); + var tableReferenceExpression = FindTableReference(this, tableExpressionBase); + + foreach (var property in targetEntityType.GetDeclaredProperties().Where(p => p.IsPrimaryKey())) + { + var columnMapping = table.FindColumn(property); + if (columnMapping != null) + { + keyPropertyMap[property] = new ConcreteColumnExpression( + property, columnMapping, tableReferenceExpression, nullable); + } + } + + var jsonPathExpression = new JsonPathExpression(jsonColumn, typeof(JsonElement), jsonColumnTypeMapping, keyPropertyMap); + + return new JsonProjectionExpression(targetEntityType, jsonPathExpression, isCollection); + + static TableReferenceExpression FindTableReference(SelectExpression selectExpression, TableExpressionBase tableExpression) + { + var tableIndex = selectExpression._tables.FindIndex(e => ReferenceEquals(e, tableExpression)); + + return selectExpression._tableReferences[tableIndex]; + } + } + private enum JoinType { InnerJoin, diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index ae575a52c66..f7f515b1542 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -389,6 +389,8 @@ SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression, allowOptimizedExpansion, out nullable), SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression, allowOptimizedExpansion, out nullable), + JsonPathExpression jsonPathExpression + => VisitJsonPathExpression(jsonPathExpression, allowOptimizedExpansion, out nullable), _ => VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) }; @@ -1124,6 +1126,24 @@ protected virtual SqlExpression VisitSqlUnary( : updated; } + /// + /// Visits a and computes its nullability. + /// + /// A json path expression to visit. + /// A bool value indicating if optimized expansion which considers null value as false value is allowed. + /// A bool value indicating whether the sql expression is nullable. + /// An optimized sql expression. + protected virtual SqlExpression VisitJsonPathExpression( + JsonPathExpression jsonPathExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + // TODO: or should it always be marked as nullable (e.g. accessing non-existing json element for non-strict json) + nullable = jsonPathExpression.JsonColumn.IsNullable; + + return jsonPathExpression; + } + private static bool? TryGetBoolConstantValue(SqlExpression? expression) => expression is SqlConstantExpression constantExpression && constantExpression.Value is bool boolValue diff --git a/src/EFCore.Relational/Storage/JsonTypeMapping.cs b/src/EFCore.Relational/Storage/JsonTypeMapping.cs new file mode 100644 index 00000000000..a2be4e471f9 --- /dev/null +++ b/src/EFCore.Relational/Storage/JsonTypeMapping.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; + +namespace Microsoft.EntityFrameworkCore.Storage +{ + /// + /// TODO + /// + public abstract class JsonTypeMapping : RelationalTypeMapping + { + /// + /// TODO + /// + protected JsonTypeMapping(string storeType, Type clrType, DbType? dbType) + : base(storeType, clrType, dbType) + { + } + + /// + /// TODO + /// + protected JsonTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + } +} diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index e8c2670cd64..e67ef9d03ae 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -234,13 +234,23 @@ public override IEnumerable For(IColumn column, bool designTime) string.Format(CultureInfo.InvariantCulture, "{0}, {1}", seed ?? 1, increment ?? 1)); } + + // maumar: json column doesn't have property mappings (should it?) + // Model validation ensures that these facets are the same on all mapped properties - var property = column.PropertyMappings.First().Property; - if (property.IsSparse() is bool isSparse) + var property = column.PropertyMappings.FirstOrDefault()?.Property; + if (property?.IsSparse() is bool isSparse) { yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse); } + //// Model validation ensures that these facets are the same on all mapped properties + //var property = column.PropertyMappings.First().Property; + //if (property.IsSparse() is bool isSparse) + //{ + // yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse); + //} + var entityType = column.Table.EntityTypeMappings.First().EntityType; if (entityType.IsTemporal() && designTime) { diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 9a5bb6c9c91..0fc6a58786c 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -711,4 +711,31 @@ protected override Expression VisitUnion(UnionExpression unionExpression) return unionExpression.Update(source1, source2); } + + /// + /// 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. + /// + protected override Expression VisitJsonPathExpression(JsonPathExpression jsonPathExpression) + => jsonPathExpression; + + ///// + ///// 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. + ///// + //protected override Expression VisitJsonEntityExpression(JsonEntityExpression jsonEntityExpression) + // => jsonEntityExpression; + + ///// + ///// 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. + ///// + //protected override Expression VisitJsonMappedPropertyExpression(JsonMappedPropertyExpression jsonMappedPropertyExpression) + // => jsonMappedPropertyExpression; } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 9a2fb269e95..29f642277df 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.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.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; @@ -169,6 +170,45 @@ protected override Expression VisitExtension(Expression extensionExpression) return base.VisitExtension(extensionExpression); } + /// + protected override Expression VisitJsonPathExpression(JsonPathExpression jsonPathExpression) + { + // TODO: do this properly, i.e. using a flag or something? + if (jsonPathExpression.Type == typeof(JsonElement)) + { + Sql.Append("JSON_QUERY("); + } + else + { + Sql.Append("CAST(JSON_VALUE("); + } + + Visit(jsonPathExpression.JsonColumn); + Sql.Append(","); + + var jsonPath = string.Join(".", jsonPathExpression.JsonPath); + if (!string.IsNullOrEmpty(jsonPath)) + { + jsonPath = "$." + jsonPath; + } + else + { + jsonPath = "$"; + } + + Sql.Append("'" + jsonPath + "'"); + Sql.Append(")"); + + if (jsonPathExpression.Type != typeof(JsonElement)) + { + Sql.Append(" AS "); + Sql.Append(jsonPathExpression.TypeMapping!.StoreType); + Sql.Append(")"); + } + + return base.VisitJsonPathExpression(jsonPathExpression); + } + /// protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) { diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs new file mode 100644 index 00000000000..4cd7276e6eb --- /dev/null +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs @@ -0,0 +1,34 @@ +// 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.SqlServer.Storage.Internal +{ + /// + /// TODO + /// + public class SqlServerJsonTypeMapping : JsonTypeMapping + { + /// + /// TODO + /// + public SqlServerJsonTypeMapping(string storeType) + : base(storeType, typeof(JsonElement), System.Data.DbType.String) + { + } + + /// + /// TODO + /// + protected SqlServerJsonTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// TODO + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqlServerJsonTypeMapping(parameters); + } +} diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index 603f47f6d20..86710ab616f 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Data; +using System.Text.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -129,6 +130,9 @@ private readonly TimeSpanTypeMapping _time private readonly SqlServerStringTypeMapping _xml = new("xml", unicode: true, storeTypePostfix: StoreTypePostfix.None); + private readonly SqlServerJsonTypeMapping _json + = new("nvarchar(max)"); + private readonly Dictionary _clrTypeMappings; private readonly Dictionary _clrNoFacetTypeMappings; @@ -160,7 +164,8 @@ public SqlServerTypeMappingSource( { typeof(short), _short }, { typeof(float), _real }, { typeof(decimal), _decimal182 }, - { typeof(TimeSpan), _time } + { typeof(TimeSpan), _time }, + { typeof(JsonElement), _json } }; _clrNoFacetTypeMappings diff --git a/src/EFCore/Query/ExpressionPrinter.cs b/src/EFCore/Query/ExpressionPrinter.cs index a7121f6500a..ff04a2570c8 100644 --- a/src/EFCore/Query/ExpressionPrinter.cs +++ b/src/EFCore/Query/ExpressionPrinter.cs @@ -395,7 +395,11 @@ protected override Expression VisitBlock(BlockExpression blockExpression) if (blockExpression.Result != null) { - Append("return "); + if (blockExpression.Result.Type != typeof(void)) + { + Append("return "); + } + Visit(blockExpression.Result); AppendLine(";"); } diff --git a/src/EFCore/Query/IncludeExpression.cs b/src/EFCore/Query/IncludeExpression.cs index afb888a219a..c16cd724931 100644 --- a/src/EFCore/Query/IncludeExpression.cs +++ b/src/EFCore/Query/IncludeExpression.cs @@ -111,8 +111,10 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) expressionPrinter.AppendLine("IncludeExpression("); using (expressionPrinter.Indent()) { + expressionPrinter.AppendLine("EntityExpression:"); expressionPrinter.Visit(EntityExpression); expressionPrinter.AppendLine(", "); + expressionPrinter.AppendLine("NavigationExpression:"); expressionPrinter.Visit(NavigationExpression); expressionPrinter.AppendLine($", {Navigation.Name})"); } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index 6e3eadcd0c8..ce7d83b2fc0 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -459,7 +459,7 @@ public Expression GetExpression() /// Owned navigations are not expanded, since they map differently in different providers. /// This remembers such references so that they can still be treated like navigations. /// - private sealed class OwnedNavigationReference : Expression + private sealed class OwnedNavigationReference : Expression, IPrintableExpression { public OwnedNavigationReference(Expression parent, INavigation navigation, EntityReference entityReference) { @@ -484,5 +484,17 @@ public override Type Type public override ExpressionType NodeType => ExpressionType.Extension; + + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.AppendLine(nameof(OwnedNavigationReference)); + using (expressionPrinter.Indent()) + { + expressionPrinter.Append("Parent: "); + expressionPrinter.Visit(Parent); + expressionPrinter.AppendLine(); + expressionPrinter.Append("Navigation: " + Navigation.Name + " (OWNED)"); + } + } } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs new file mode 100644 index 00000000000..1356eb43309 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs @@ -0,0 +1,30 @@ +// 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.TestModels.JsonMappedEntitiesModel; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public abstract class JsonQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase + { + public Func GetContextCreator() + { + throw new NotImplementedException(); + } + + public IReadOnlyDictionary GetEntityAsserters() + { + throw new NotImplementedException(); + } + + public IReadOnlyDictionary GetEntitySorters() + { + throw new NotImplementedException(); + } + + public ISetSource GetExpectedData() + { + throw new NotImplementedException(); + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs new file mode 100644 index 00000000000..44fd5790239 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs @@ -0,0 +1,15 @@ +// 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 +{ + public class JsonQueryTestBase : QueryTestBase + where TFixture : JsonQueryFixtureBase, new() + { + + protected JsonQueryTestBase(TFixture fixture) + : base(fixture) + { + } + } +} diff --git a/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicEntity.cs b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicEntity.cs new file mode 100644 index 00000000000..5e57cc97d18 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicEntity.cs @@ -0,0 +1,10 @@ +// 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.TestModels.JsonMappedEntitiesModel +{ + public class JsonBasicEntityNesting + { + public int Id { get; set; } + } +} diff --git a/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedBranch.cs b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedBranch.cs new file mode 100644 index 00000000000..cd76684bd0b --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedBranch.cs @@ -0,0 +1,9 @@ +// 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.TestModels.JsonMappedEntitiesModel +{ + internal class JsonBasicOwnedBranch + { + } +} diff --git a/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedLeaf.cs b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedLeaf.cs new file mode 100644 index 00000000000..7380f443ae0 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedLeaf.cs @@ -0,0 +1,9 @@ +// 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.TestModels.JsonMappedEntitiesModel +{ + public class JsonBasicOwnedLeaf + { + } +} diff --git a/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedRoot.cs b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedRoot.cs new file mode 100644 index 00000000000..8709d18bb5a --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonBasicOwnedRoot.cs @@ -0,0 +1,9 @@ +// 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.TestModels.JsonMappedEntitiesModel +{ + internal class JsonBasicOwnedRoot + { + } +} diff --git a/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonMappedEntitiesContext.cs b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonMappedEntitiesContext.cs new file mode 100644 index 00000000000..a7b8072bc74 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonMappedEntitiesModel/JsonMappedEntitiesContext.cs @@ -0,0 +1,9 @@ +// 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.TestModels.JsonMappedEntitiesModel +{ + public class JsonMappedEntitiesContext : PoolableDbContext + { + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index a04598dca01..970cb193e9f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6,6 +6,8 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Data; using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; @@ -10349,6 +10351,556 @@ public class Entity #endregion + + [ConditionalFact] + public void JsonTest() + { + using (var ctx = new MyContext()) + { + //ctx.Database.EnsureDeleted(); + //ctx.Database.EnsureCreated(); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_r_r_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_r_r_shared" }; + var e1_r_r_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_r_c1_shared" }; + var e1_r_r_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_r_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_r_r_shared = new MyOwnedBranchShared + { + Date = new DateTime(2100, 1, 1), + Fraction = 10.0M, + OwnedReferenceSharedLeaf = e1_r_r_r_shared, + OwnedCollectionSharedLeaf = new List { e1_r_r_c1_shared, e1_r_r_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_r_c1_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_c1_r_shared" }; + var e1_r_c1_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_c1_c1_shared" }; + var e1_r_c1_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_c1_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_r_c1_shared = new MyOwnedBranchShared + { + Date = new DateTime(2101, 1, 1), + Fraction = 10.1M, + OwnedReferenceSharedLeaf = e1_r_c1_r_shared, + OwnedCollectionSharedLeaf = new List { e1_r_c1_c1_shared, e1_r_c1_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_r_c2_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_c2_r_shared" }; + var e1_r_c2_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_c2_c1_shared" }; + var e1_r_c2_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_r_c2_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_r_c2_shared = new MyOwnedBranchShared + { + Date = new DateTime(2102, 1, 1), + Fraction = 10.2M, + OwnedReferenceSharedLeaf = e1_r_c2_r_shared, + OwnedCollectionSharedLeaf = new List { e1_r_c2_c1_shared, e1_r_c2_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_r_shared = new MyOwnedRootShared + { + Name = "e1_r_shared", + Number = 10, + OwnedReferenceSharedBranch = e1_r_r_shared, + OwnedCollectionSharedBranch = new List { e1_r_c1_shared, e1_r_c2_shared } + }; + + + + + + + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c1_r_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_r_r_shared" }; + var e1_c1_r_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_r_c1_shared" }; + var e1_c1_r_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_r_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_c1_r_shared = new MyOwnedBranchShared + { + Date = new DateTime(2110, 1, 1), + Fraction = 11.0M, + OwnedReferenceSharedLeaf = e1_c1_r_r_shared, + OwnedCollectionSharedLeaf = new List { e1_c1_r_c1_shared, e1_c1_r_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c1_c1_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_c1_r_shared" }; + var e1_c1_c1_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_c1_c1_shared" }; + var e1_c1_c1_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_c1_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_c1_c1_shared = new MyOwnedBranchShared + { + Date = new DateTime(2111, 1, 1), + Fraction = 11.1M, + OwnedReferenceSharedLeaf = e1_c1_c1_r_shared, + OwnedCollectionSharedLeaf = new List { e1_c1_c1_c1_shared, e1_c1_c1_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c1_c2_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_c2_r_shared" }; + var e1_c1_c2_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_c2_c1_shared" }; + var e1_c1_c2_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c1_c2_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_c1_c2_shared = new MyOwnedBranchShared + { + Date = new DateTime(2112, 1, 1), + Fraction = 11.2M, + OwnedReferenceSharedLeaf = e1_c1_c2_r_shared, + OwnedCollectionSharedLeaf = new List { e1_c1_c2_c1_shared, e1_c1_c2_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c1_shared = new MyOwnedRootShared + { + Name = "e1_c1_shared", + Number = 11, + OwnedReferenceSharedBranch = e1_c1_r_shared, + OwnedCollectionSharedBranch = new List { e1_c1_c1_shared, e1_c1_c2_shared } + }; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c2_r_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_r_r_shared" }; + var e1_c2_r_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_r_c1_shared" }; + var e1_c2_r_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_r_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_c2_r_shared = new MyOwnedBranchShared + { + Date = new DateTime(2120, 1, 1), + Fraction = 12.0M, + OwnedReferenceSharedLeaf = e1_c2_r_r_shared, + OwnedCollectionSharedLeaf = new List { e1_c2_r_c1_shared, e1_c2_r_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c2_c1_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_c1_r_shared" }; + var e1_c2_c1_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_c1_c1_shared" }; + var e1_c2_c1_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_c1_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_c2_c1_shared = new MyOwnedBranchShared + { + Date = new DateTime(2121, 1, 1), + Fraction = 12.1M, + OwnedReferenceSharedLeaf = e1_c2_c1_r_shared, + OwnedCollectionSharedLeaf = new List { e1_c2_c1_c1_shared, e1_c2_c1_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c2_c2_r_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_c2_r_shared" }; + var e1_c2_c2_c1_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_c2_c1_shared" }; + var e1_c2_c2_c2_shared = new MyOwnedLeafShared { SomethingSomething = "e1_c2_c2_c2_shared" }; + + //------------------------------------------------------------------------------------------- + + var e1_c2_c2_shared = new MyOwnedBranchShared + { + Date = new DateTime(2122, 1, 1), + Fraction = 12.2M, + OwnedReferenceSharedLeaf = e1_c2_c2_r_shared, + OwnedCollectionSharedLeaf = new List { e1_c2_c2_c1_shared, e1_c2_c2_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c2_shared = new MyOwnedRootShared + { + Name = "e1_c2_shared", + Number = 12, + OwnedReferenceSharedBranch = e1_c2_r_shared, + OwnedCollectionSharedBranch = new List { e1_c2_c1_shared , e1_c2_c2_shared } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var entity1 = new MyEntity + { + Name = "MyEntity1", + OwnedReferenceSharedRoot = e1_r_shared, + OwnedCollectionSharedRoot = new List { e1_c1_shared, e1_c2_shared } + }; + + var jsonReference = JsonSerializer.Serialize(entity1.OwnedReferenceSharedRoot); + var jsonCollection = JsonSerializer.Serialize(entity1.OwnedCollectionSharedRoot); + + + + var optionalTopLevel = JsonSerializer.Serialize(default(MyOwnedRootShared)); + + var optionalBranch = new MyOwnedRootShared { Name = "OptionalRoot", Number = 20, OwnedReferenceSharedBranch = null, OwnedCollectionSharedBranch = null }; + var optionalBranchSerialized = JsonSerializer.Serialize(optionalBranch); + + + } + + using (var ctx = new MyContext()) + { + + var query0 = ctx.MyEntities.Select(x => x).ToList(); + + + var query1 = ctx.MyEntities.Select(x => x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch).AsNoTracking().ToList(); + var query2 = ctx.MyEntities.Select(x => x.OwnedReferenceSharedRoot).AsNoTracking().ToList(); + var query3 = ctx.MyEntities.Select(x => x.OwnedCollectionSharedRoot).AsNoTracking().ToList(); + var query4 = ctx.MyEntities.Select(x => x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch.OwnedCollectionSharedLeaf).AsNoTracking().ToList(); + var query4ipol = ctx.MyEntities.Select(x => x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf).AsNoTracking().ToList(); + + var query5 = ctx.MyEntities.Select(x => new + { + //x.OwnedReferenceSharedRoot, + x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch, + x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch.OwnedCollectionSharedLeaf + }).AsNoTracking().ToList(); + + + + + var query6 = ctx.MyEntities.AsNoTracking().Select(x => x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch.Date).ToList(); + var query7 = ctx.MyEntities.Where(x => x.OwnedReferenceSharedRoot.OwnedReferenceSharedBranch.Fraction < 20.5M).Select(x => x.Id).ToList(); + + } + } + + public class MyContext : DbContext + { + public DbSet MyEntities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(x => x.OwnedReferenceSharedRoot, b => + { + b.OwnsOne(x => x.OwnedReferenceSharedBranch, bb => + { + bb.OwnsOne(x => x.OwnedReferenceSharedLeaf); + bb.OwnsMany(x => x.OwnedCollectionSharedLeaf); + }); + + b.OwnsMany(x => x.OwnedCollectionSharedBranch, bb => + { + bb.OwnsOne(x => x.OwnedReferenceSharedLeaf); + bb.OwnsMany(x => x.OwnedCollectionSharedLeaf); + }); + }); + + //modelBuilder.Entity().Navigation(x => x.OwnedReferenceSharedRoot).IsRequired(); + + modelBuilder.Entity().OwnsMany(x => x.OwnedCollectionSharedRoot, b => + { + b.OwnsOne(x => x.OwnedReferenceSharedBranch, bb => + { + bb.OwnsOne(x => x.OwnedReferenceSharedLeaf); + bb.OwnsMany(x => x.OwnedCollectionSharedLeaf); + }); + + b.OwnsMany(x => x.OwnedCollectionSharedBranch, bb => + { + bb.OwnsOne(x => x.OwnedReferenceSharedLeaf); + bb.OwnsMany(x => x.OwnedCollectionSharedLeaf); + }); + }); + + // TODO: add validation - when using mapreferencetojson but using on a collection! + modelBuilder.Entity().MapReferenceToJson(x => x.OwnedReferenceSharedRoot, "json_reference_shared"); + modelBuilder.Entity().MapCollectionToJson(x => x.OwnedCollectionSharedRoot, "json_collection_shared"); + //modelBuilder.Entity().MapReferenceToJson(x => x.OwnedCollectionSharedRoot, "json_collection_shared"); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=JsonSandbox;Trusted_Connection=True;MultipleActiveResultSets=true"); + } + } + + public class MyEntity + { + public int Id { get; set; } + public string Name { get; set; } + //public MyOwnedRoot1 OwnedReferenceRoot { get; set; } + //public List OwnedCollectionRoot { get; set; } + + public MyOwnedRootShared OwnedReferenceSharedRoot { get; set; } + + public List OwnedCollectionSharedRoot { get; set; } + } + + public class MyOwnedRoot1 + { + public string Name1 { get; set; } + public int Number1 { get; set; } + + public MyOwnedBranch11 OwnedReferenceBranch { get; set; } + public List OwnedCollectionBranch { get; set; } + } + + public class MyOwnedRoot2 + { + public string Name2 { get; set; } + public int Number2 { get; set; } + + public MyOwnedBranch21 OwnedReferenceBranch { get; set; } + public List OwnedCollectionBranch { get; set; } + } + + public class MyOwnedRootShared + { + public string Name { get; set; } + public int Number { get; set; } + + public MyOwnedBranchShared OwnedReferenceSharedBranch { get; set; } + public List OwnedCollectionSharedBranch { get; set; } + } + + public class MyOwnedBranch11 + { + public DateTime Date1 { get; set; } + public decimal Fraction1 { get; set; } + + public MyOwnedLeaf111 OwnedReferenceLeaf { get; set; } + public List OwnedCollectionLeaf { get; set; } + } + + public class MyOwnedBranch12 + { + public DateTime Date2 { get; set; } + public decimal Fraction2 { get; set; } + public MyOwnedLeaf121 OwnedReferenceLeaf { get; set; } + public List OwnedCollectionLeaf { get; set; } + } + + public class MyOwnedBranch21 + { + public DateTime Date1 { get; set; } + public decimal Fraction1 { get; set; } + public MyOwnedLeaf211 OwnedReferenceLeaf { get; set; } + public List OwnedCollectionLeaf { get; set; } + } + + public class MyOwnedBranch22 + { + public DateTime Date2 { get; set; } + public decimal Fraction2 { get; set; } + + public MyOwnedLeaf221 OwnedReferenceLeaf { get; set; } + public List OwnedCollectionLeaf { get; set; } + } + + public class MyOwnedBranchShared + { + public DateTime Date { get; set; } + public decimal Fraction { get; set; } + + public MyOwnedLeafShared OwnedReferenceSharedLeaf { get; set; } + public List OwnedCollectionSharedLeaf { get; set; } + } + + public class MyOwnedLeaf111 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf112 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf121 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf122 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf211 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf212 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf221 + { + public string Something { get; set; } + } + + public class MyOwnedLeaf222 + { + public string Something { get; set; } + } + + public class MyOwnedLeafShared + { + public string SomethingSomething { get; set; } + } + protected override string StoreName => "QueryBugsTest";