diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 6eb569df105..9b570612e86 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -1758,10 +1758,10 @@ public static void SetJsonPropertyName(this IMutableEntityType entityType, strin Check.NullButNotEmpty(name, nameof(name))); /// - /// Gets the for the JSON property name for a given entity Type. + /// Gets the for the JSON property name for a given entity type. /// /// The entity type. - /// The for the JSON property name for a given navigation. + /// The for the JSON property name for a given entity type. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionEntityType entityType) => entityType.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs index f963eaa1960..6f47420c29f 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs @@ -124,6 +124,11 @@ public static bool AreCompatible( return null; } + if (key.DeclaringEntityType.IsMappedToJson()) + { + return null; + } + string? name; if (key.IsPrimaryKey()) { diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index d8a818fde0c..592d2fd0e5c 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -468,7 +468,7 @@ private static void CreateTableMapping( var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!; var jsonColumn = new JsonColumn(containerColumnName, jsonColumnTypeMapping.StoreType, table, jsonColumnTypeMapping.ProviderValueComparer); table.Columns.Add(containerColumnName, jsonColumn); - jsonColumn.IsNullable = !ownership.IsRequired || !ownership.IsUnique; + jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique; if (ownership.PrincipalEntityType.BaseType != null) { diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 2c4c843fed8..86b0ea4614f 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -973,6 +973,14 @@ public static string InvalidPropertyInSetProperty(object? propertyExpression) GetString("InvalidPropertyInSetProperty", nameof(propertyExpression)), propertyExpression); + /// + /// Can't navigate from JSON-mapped entity '{jsonEntity}' to its parent entity '{parentEntity}' using navigation '{navigation}'. Entities mapped to JSON can only navigate to their children. + /// + public static string JsonCantNavigateToParentEntity(object? jsonEntity, object? parentEntity, object? navigation) + => string.Format( + GetString("JsonCantNavigateToParentEntity", nameof(jsonEntity), nameof(parentEntity), nameof(navigation)), + jsonEntity, parentEntity, navigation); + /// /// Entity '{jsonType}' is mapped to JSON and also to a view '{viewName}', but its owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as its owner. /// @@ -1067,12 +1075,34 @@ public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity) public static string JsonEntityWithTableSplittingIsNotSupported => GetString("JsonEntityWithTableSplittingIsNotSupported"); + /// + /// An error occurred while reading a JSON value for property '{entityType}.{propertyName}'. See the inner exception for more information. + /// + public static string JsonErrorExtractingJsonProperty(object? entityType, object? propertyName) + => string.Format( + GetString("JsonErrorExtractingJsonProperty", nameof(entityType), nameof(propertyName)), + entityType, propertyName); + + /// + /// This node should be handled by provider-specific sql generator. + /// + public static string JsonNodeMustBeHandledByProviderSpecificVisitor + => GetString("JsonNodeMustBeHandledByProviderSpecificVisitor"); + /// /// The JSON property name should only be configured on nested owned navigations. /// public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation"); + /// + /// Entity {entity} is required but the JSON element containing it is null. + /// + public static string JsonRequiredEntityWithNullJson(object? entity) + => string.Format( + GetString("JsonRequiredEntityWithNullJson", nameof(entity)), + entity); + /// /// The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 70af619f01d..3eb627796b8 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -481,6 +481,9 @@ The following lambda argument to 'SetProperty' does not represent a valid property to be set: '{propertyExpression}'. + + Can't navigate from JSON-mapped entity '{jsonEntity}' to its parent entity '{parentEntity}' using navigation '{navigation}'. Entities mapped to JSON can only navigate to their children. + Entity '{jsonType}' is mapped to JSON and also to a view '{viewName}', but its owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as its owner. @@ -517,9 +520,18 @@ Table splitting is not supported for entities containing entities mapped to JSON. + + An error occurred while reading a JSON value for property '{entityType}.{propertyName}'. See the inner exception for more information. + + + This node should be handled by provider-specific sql generator. + The JSON property name should only be configured on nested owned navigations. + + Entity {entity} is required but the JSON element containing it is null. + The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. diff --git a/src/EFCore.Relational/Query/EntityProjectionExpression.cs b/src/EFCore.Relational/Query/EntityProjectionExpression.cs index 302e7c5b0a9..6d54e0b473b 100644 --- a/src/EFCore.Relational/Query/EntityProjectionExpression.cs +++ b/src/EFCore.Relational/Query/EntityProjectionExpression.cs @@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Query; public class EntityProjectionExpression : Expression { private readonly IReadOnlyDictionary _propertyExpressionMap; - private readonly Dictionary _ownedNavigationMap = new(); + private readonly Dictionary _ownedNavigationMap; /// /// Creates a new instance of the class. @@ -29,9 +29,23 @@ public EntityProjectionExpression( IEntityType entityType, IReadOnlyDictionary propertyExpressionMap, SqlExpression? discriminatorExpression = null) + : this( + entityType, + propertyExpressionMap, + new Dictionary(), + discriminatorExpression) + { + } + + private EntityProjectionExpression( + IEntityType entityType, + IReadOnlyDictionary propertyExpressionMap, + Dictionary ownedNavigationMap, + SqlExpression? discriminatorExpression = null) { EntityType = entityType; _propertyExpressionMap = propertyExpressionMap; + _ownedNavigationMap = ownedNavigationMap; DiscriminatorExpression = discriminatorExpression; } @@ -69,8 +83,16 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) var discriminatorExpression = (SqlExpression?)visitor.Visit(DiscriminatorExpression); changed |= discriminatorExpression != DiscriminatorExpression; + var ownedNavigationMap = new Dictionary(); + foreach (var (navigation, entityShaperExpression) in _ownedNavigationMap) + { + var newExpression = (EntityShaperExpression)visitor.Visit(entityShaperExpression); + changed |= newExpression != entityShaperExpression; + ownedNavigationMap[navigation] = newExpression; + } + return changed - ? new EntityProjectionExpression(EntityType, propertyExpressionMap, discriminatorExpression) + ? new EntityProjectionExpression(EntityType, propertyExpressionMap, ownedNavigationMap, discriminatorExpression) : this; } @@ -92,7 +114,65 @@ public virtual EntityProjectionExpression MakeNullable() // if discriminator is column then we need to make it nullable discriminatorExpression = ce.MakeNullable(); } - return new EntityProjectionExpression(EntityType, propertyExpressionMap, discriminatorExpression); + + var primaryKeyProperties = GetMappedKeyProperties(EntityType.FindPrimaryKey()!); + var ownedNavigationMap = new Dictionary(); + foreach (var (navigation, shaper) in _ownedNavigationMap) + { + if (shaper.EntityType.IsMappedToJson()) + { + // even if shaper is nullable, we need to make sure key property map contains nullable keys, + // if json entity itself is optional, the shaper would be null, but the PK of the owner entity would be non-nullable intially + Debug.Assert(primaryKeyProperties != null, "Json entity type can't be keyless"); + + var jsonQueryExpression = (JsonQueryExpression)shaper.ValueBufferExpression; + var ownedPrimaryKeyProperties = GetMappedKeyProperties(shaper.EntityType.FindPrimaryKey()!)!; + var nullableKeyPropertyMap = new Dictionary(); + for (var i = 0; i < primaryKeyProperties.Count; i++) + { + nullableKeyPropertyMap[ownedPrimaryKeyProperties[i]] = propertyExpressionMap[primaryKeyProperties[i]]; + } + + // reuse key columns from owner (that we just made nullable), so that the references are the same + var newJsonQueryExpression = jsonQueryExpression.MakeNullable(nullableKeyPropertyMap); + var newShaper = shaper.Update(newJsonQueryExpression).MakeNullable(); + ownedNavigationMap[navigation] = newShaper; + } + } + + return new EntityProjectionExpression( + EntityType, + propertyExpressionMap, + ownedNavigationMap, + discriminatorExpression); + + static IReadOnlyList? GetMappedKeyProperties(IKey? key) + { + if (key == null) + { + return null; + } + + if (!key.DeclaringEntityType.IsMappedToJson()) + { + return key.Properties; + } + + // TODO: fix this once we enable json entity being owned by another owned non-json entity (issue #28441) + + // for json collections we need to filter out the ordinal key as it's not mapped to any column + // there could be multiple of these in deeply nested structures, + // so we traverse to the outermost owner to see how many mapped keys there are + var currentEntity = key.DeclaringEntityType; + while (currentEntity.IsMappedToJson()) + { + currentEntity = currentEntity.FindOwnership()!.PrincipalEntityType; + } + + var count = currentEntity.FindPrimaryKey()!.Properties.Count; + + return key.Properties.Take(count).ToList(); + } } /// @@ -119,6 +199,16 @@ public virtual EntityProjectionExpression UpdateEntityType(IEntityType derivedTy } } + var ownedNavigationMap = new Dictionary(); + foreach (var (navigation, entityShaperExpression) in _ownedNavigationMap) + { + if (derivedType.IsAssignableFrom(navigation.DeclaringEntityType) + || navigation.DeclaringEntityType.IsAssignableFrom(derivedType)) + { + ownedNavigationMap[navigation] = entityShaperExpression; + } + } + var discriminatorExpression = DiscriminatorExpression; if (DiscriminatorExpression is CaseExpression caseExpression) { @@ -130,7 +220,7 @@ public virtual EntityProjectionExpression UpdateEntityType(IEntityType derivedTy discriminatorExpression = caseExpression.Update(operand: null, whenClauses, elseResult: null); } - return new EntityProjectionExpression(derivedType, propertyExpressionMap, discriminatorExpression); + return new EntityProjectionExpression(derivedType, propertyExpressionMap, ownedNavigationMap, discriminatorExpression); } /// diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index 5022ca16d4b..46bbe345acd 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -25,6 +25,7 @@ private static readonly MethodInfo GetParameterValueMethodInfo private bool _indexBasedBinding; private Dictionary? _entityProjectionCache; + private Dictionary? _jsonQueryCache; private List? _clientProjections; private readonly Dictionary _projectionMapping = new(); @@ -64,7 +65,8 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio if (result == QueryCompilationContext.NotTranslatedExpression) { _indexBasedBinding = true; - _entityProjectionCache = new Dictionary(); + _entityProjectionCache = new(); + _jsonQueryCache = new(); _projectionMapping.Clear(); _clientProjections = new List(); @@ -138,9 +140,44 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio throw new InvalidOperationException(CoreStrings.TranslationFailed(projectionBindingExpression.Print())); case MaterializeCollectionNavigationExpression materializeCollectionNavigationExpression: + if (materializeCollectionNavigationExpression.Navigation.TargetEntityType.IsMappedToJson()) + { + var subquery = materializeCollectionNavigationExpression.Subquery; + if (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) + { + methodCallSubquery = selectSourceMethod; + } + + if (methodCallSubquery.Method.IsGenericMethod + && methodCallSubquery.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable) + { + subquery = methodCallSubquery.Arguments[0]; + } + } + + if (subquery is JsonQueryExpression jsonQueryExpression) + { + Debug.Assert( + jsonQueryExpression.IsCollection, + "JsonQueryExpression inside materialize collection should always be a collection."); + + _clientProjections!.Add(jsonQueryExpression); + + return new CollectionResultExpression( + new ProjectionBindingExpression(_selectExpression, _clientProjections!.Count - 1, jsonQueryExpression.Type), + materializeCollectionNavigationExpression.Navigation, + materializeCollectionNavigationExpression.Navigation.ClrType.GetSequenceType()); + } + } + _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), @@ -267,6 +304,28 @@ protected override Expression VisitExtension(Expression extensionExpression) { // TODO: Make this easier to understand some day. EntityProjectionExpression entityProjectionExpression; + + if (entityShaperExpression.ValueBufferExpression is JsonQueryExpression jsonQueryExpression) + { + if (_indexBasedBinding) + { + if (!_jsonQueryCache!.TryGetValue(jsonQueryExpression, out var jsonProjectionBinding)) + { + jsonProjectionBinding = AddClientProjection(jsonQueryExpression, typeof(ValueBuffer)); + _jsonQueryCache[jsonQueryExpression] = jsonProjectionBinding; + } + + return entityShaperExpression.Update(jsonProjectionBinding); + } + else + { + _projectionMapping[_projectionMembers.Peek()] = jsonQueryExpression; + + return entityShaperExpression.Update( + new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer))); + } + } + if (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression) { if (projectionBindingExpression.ProjectionMember == null @@ -277,9 +336,25 @@ protected override Expression VisitExtension(Expression extensionExpression) return QueryCompilationContext.NotTranslatedExpression; } - entityProjectionExpression = - (EntityProjectionExpression)((SelectExpression)projectionBindingExpression.QueryExpression) - .GetProjection(projectionBindingExpression); + var projection = ((SelectExpression)projectionBindingExpression.QueryExpression).GetProjection(projectionBindingExpression); + if (projection is JsonQueryExpression jsonQuery) + { + if (_indexBasedBinding) + { + var projectionBinding = AddClientProjection(jsonQuery, typeof(ValueBuffer)); + + return entityShaperExpression.Update(projectionBinding); + } + else + { + _projectionMapping[_projectionMembers.Peek()] = jsonQuery; + + return entityShaperExpression.Update( + new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer))); + } + } + + entityProjectionExpression = (EntityProjectionExpression)projection; } else { @@ -303,8 +378,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.IsMappedToJson() + ? Visit(includeExpression.EntityExpression) + : base.VisitExtension(extensionExpression); + } + + return QueryCompilationContext.NotTranslatedExpression; + } case CollectionResultExpression collectionResultExpression: { diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs new file mode 100644 index 00000000000..d6410acb54a --- /dev/null +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -0,0 +1,278 @@ +// 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 +{ + /// + /// Expression representing an entity or a collection of entities mapped to a JSON column and the path to access it. + /// + public class JsonQueryExpression : Expression, IPrintableExpression + { + private readonly IReadOnlyDictionary _keyPropertyMap; + private readonly bool _nullable; + + /// + /// Creates a new instance of the class. + /// + /// An entity type being represented by this expression. + /// A column containing JSON. + /// A value indicating whether this expression represents a collection. + /// A map of key properties and columns they map to in the database. + /// A type of the element represented by this expression. + public JsonQueryExpression( + IEntityType entityType, + ColumnExpression jsonColumn, + bool collection, + IReadOnlyDictionary keyPropertyMap, + Type type) + : this( + entityType, + jsonColumn, + collection, + keyPropertyMap, + type, + jsonPath: new SqlConstantExpression(Constant("$"), typeMapping: null), + jsonColumn.IsNullable) + { + } + + private JsonQueryExpression( + IEntityType entityType, + ColumnExpression jsonColumn, + bool collection, + IReadOnlyDictionary keyPropertyMap, + Type type, + SqlExpression jsonPath, + bool nullable) + { + Check.DebugAssert(entityType.FindPrimaryKey() != null, "primary key is null."); + + EntityType = entityType; + JsonColumn = jsonColumn; + IsCollection = collection; + _keyPropertyMap = keyPropertyMap; + Type = type; + JsonPath = jsonPath; + _nullable = nullable; + } + + /// + /// The entity type being projected out. + /// + public virtual IEntityType EntityType { get; } + + /// + /// The column containg JSON value on which the path is applied. + /// + public virtual ColumnExpression JsonColumn { get; } + + /// + /// The value indicating whether this expression represents a collection. + /// + public virtual bool IsCollection { get; } + + /// + /// The JSON path leading to the entity from the root of the JSON stored in the column. + /// + public virtual SqlExpression JsonPath { get; } + + /// + /// The value indicating whether this expression is nullable. + /// + public virtual bool IsNullable => _nullable; + + /// + public override ExpressionType NodeType => ExpressionType.Extension; + + /// + public override Type Type { get; } + + /// + /// Binds a property with this JSON query expression to get the SQL representation. + /// + public virtual SqlExpression BindProperty(IProperty property) + { + if (!EntityType.IsAssignableFrom(property.DeclaringEntityType) + && !property.DeclaringEntityType.IsAssignableFrom(EntityType)) + { + throw new InvalidOperationException( + RelationalStrings.UnableToBindMemberToEntityProjection("property", property.Name, EntityType.DisplayName())); + } + + if (_keyPropertyMap.TryGetValue(property, out var match)) + { + return match; + } + + var pathSegment = new SqlConstantExpression( + Constant(property.GetJsonPropertyName()), + typeMapping: null); + + var newPath = new SqlBinaryExpression( + ExpressionType.Add, + JsonPath, + pathSegment, + typeof(string), + typeMapping: null); + + return new JsonScalarExpression( + JsonColumn, + property, + newPath, + _nullable || property.IsNullable); + } + + /// + /// Binds a navigation with this JSON query expression to get the SQL representation. + /// + /// The navigation to bind. + /// An JSON query expression for the target entity type of the navigation. + public virtual JsonQueryExpression BindNavigation(INavigation navigation) + { + if (navigation.ForeignKey.DependentToPrincipal == navigation) + { + // issue #28645 + throw new InvalidOperationException( + RelationalStrings.JsonCantNavigateToParentEntity( + navigation.ForeignKey.DeclaringEntityType.DisplayName(), + navigation.ForeignKey.PrincipalEntityType.DisplayName(), + navigation.Name)); + } + + var targetEntityType = navigation.TargetEntityType; + var pathSegment = new SqlConstantExpression( + Constant(navigation.TargetEntityType.GetJsonPropertyName()), + typeMapping: null); + + var newJsonPath = new SqlBinaryExpression( + ExpressionType.Add, + JsonPath, + pathSegment, + typeof(string), + typeMapping: null); + + var newKeyPropertyMap = new Dictionary(); + var targetPrimaryKeyProperties = targetEntityType.FindPrimaryKey()!.Properties.Take(_keyPropertyMap.Count); + var sourcePrimaryKeyProperties = EntityType.FindPrimaryKey()!.Properties.Take(_keyPropertyMap.Count); + foreach (var (target, source) in targetPrimaryKeyProperties.Zip(sourcePrimaryKeyProperties, (t, s) => (t, s))) + { + newKeyPropertyMap[target] = _keyPropertyMap[source]; + } + + return new JsonQueryExpression( + targetEntityType, + JsonColumn, + navigation.IsCollection, + newKeyPropertyMap, + navigation.ClrType, + newJsonPath, + _nullable || !navigation.ForeignKey.IsRequiredDependent); + } + + /// + /// Makes this JSON query expression nullable. + /// + /// A new expression which has property set to true. + public virtual JsonQueryExpression MakeNullable() + { + var keyPropertyMap = new Dictionary(); + foreach (var (property, columnExpression) in _keyPropertyMap) + { + keyPropertyMap[property] = columnExpression.MakeNullable(); + } + + return MakeNullable(keyPropertyMap); + } + + /// + /// Makes this JSON query expression nullable re-using existing nullable key properties + /// + /// A new expression which has property set to true. + [EntityFrameworkInternal] + public virtual JsonQueryExpression MakeNullable(IReadOnlyDictionary nullableKeyPropertyMap) + => Update( + JsonColumn.MakeNullable(), + nullableKeyPropertyMap, + JsonPath, + nullable: true); + + /// + public virtual void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("JsonQueryExpression("); + expressionPrinter.Visit(JsonColumn); + expressionPrinter.Append($", \"{string.Join(".", JsonPath)}\")"); + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn); + var jsonPath = (SqlExpression)visitor.Visit(JsonPath); + + // TODO: also visit columns in the _keyPropertyMap? + return Update(jsonColumn, _keyPropertyMap, jsonPath, IsNullable); + } + + /// + /// 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. + /// The map of key properties and columns they map to. + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual JsonQueryExpression Update( + ColumnExpression jsonColumn, + IReadOnlyDictionary keyPropertyMap, + SqlExpression jsonPath, + bool nullable) + => jsonColumn != JsonColumn + || keyPropertyMap.Count != _keyPropertyMap.Count + || keyPropertyMap.Zip(_keyPropertyMap, (n, o) => n.Value != o.Value).Any(x => x) + || jsonPath != JsonPath + ? new JsonQueryExpression(EntityType, jsonColumn, IsCollection, keyPropertyMap, Type, jsonPath, nullable) + : this; + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is JsonQueryExpression jsonQueryExpression + && Equals(jsonQueryExpression)); + + private bool Equals(JsonQueryExpression jsonQueryExpression) + => EntityType.Equals(jsonQueryExpression.EntityType) + && JsonColumn.Equals(jsonQueryExpression.JsonColumn) + && IsCollection.Equals(jsonQueryExpression.IsCollection) + && JsonPath.Equals(jsonQueryExpression.JsonPath) + && IsNullable == jsonQueryExpression.IsNullable + && KeyPropertyMapEquals(jsonQueryExpression._keyPropertyMap); + + private bool KeyPropertyMapEquals(IReadOnlyDictionary other) + { + if (_keyPropertyMap.Count != other.Count) + { + return false; + } + + foreach (var (key, value) in _keyPropertyMap) + { + if (!other.TryGetValue(key, out var column) || !value.Equals(column)) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() + // not incorporating _keyPropertyMap into the hash, too much work + => HashCode.Combine(EntityType, JsonColumn, IsCollection, JsonPath, IsNullable); + } +} diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 6453c48fe7d..b25242c4926 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -1313,4 +1313,9 @@ void LiftPredicate(TableExpressionBase joinTable) throw new InvalidOperationException( RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); } + + /// + protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + => throw new InvalidOperationException( + RelationalStrings.JsonNodeMustBeHandledByProviderSpecificVisitor); } diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs index 41f068a0c48..b60fdfefe40 100644 --- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs @@ -89,7 +89,8 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType if (containsDiscriminatorProperty || entityType.FindPrimaryKey() == null || entityType.GetRootType() != entityType - || entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) + || entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + || entityType.IsMappedToJson()) { return baseCondition; } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index e4460c53a14..be972081c08 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; @@ -633,7 +634,7 @@ private SqlExpression CreateJoinPredicate( } private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerKey) - => TranslateExpression(EntityFrameworkCore.Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey))!; + => TranslateExpression(Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey))!; /// protected override ShapedQueryExpression? TranslateLastOrDefault( @@ -1527,158 +1528,184 @@ protected override Expression VisitExtension(Expression extensionExpression) return null; } - var entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression); - var foreignKey = navigation.ForeignKey; - if (navigation.IsCollection) + if (TryGetJsonQueryExpression(entityShaperExpression, out var jsonQueryExpression)) { - var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable( - entityProjectionExpression, - targetEntityType.GetViewOrTableMappings().Single().Table, - navigation); - - var innerShapedQuery = CreateShapedQueryExpression( - targetEntityType, innerSelectExpression); - - var makeNullable = foreignKey.PrincipalKey.Properties - .Concat(foreignKey.Properties) - .Select(p => p.ClrType) - .Any(t => t.IsNullableType()); - - var innerSequenceType = innerShapedQuery.Type.GetSequenceType(); - var correlationPredicateParameter = Expression.Parameter(innerSequenceType); - - var outerKey = entityShaperExpression.CreateKeyValuesExpression( - navigation.IsOnDependent - ? foreignKey.Properties - : foreignKey.PrincipalKey.Properties, - makeNullable); - var innerKey = correlationPredicateParameter.CreateKeyValuesExpression( - navigation.IsOnDependent - ? foreignKey.PrincipalKey.Properties - : foreignKey.Properties, - makeNullable); - - var keyComparison = Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey); - - var predicate = makeNullable - ? Expression.AndAlso( - outerKey is NewArrayExpression newArrayExpression - ? newArrayExpression.Expressions - .Select( - e => - { - var left = (e as UnaryExpression)?.Operand ?? e; - - return Expression.NotEqual(left, Expression.Constant(null, left.Type)); - }) - .Aggregate((l, r) => Expression.AndAlso(l, r)) - : Expression.NotEqual(outerKey, Expression.Constant(null, outerKey.Type)), - keyComparison) - : (Expression)keyComparison; - - var correlationPredicate = Expression.Lambda(predicate, correlationPredicateParameter); - - return Expression.Call( - QueryableMethods.Where.MakeGenericMethod(innerSequenceType), - innerShapedQuery, - Expression.Quote(correlationPredicate)); + var newJsonQueryExpression = jsonQueryExpression.BindNavigation(navigation); + + return navigation.IsCollection + ? newJsonQueryExpression + : new RelationalEntityShaperExpression( + navigation.TargetEntityType, + newJsonQueryExpression, + nullable: entityShaperExpression.IsNullable || !navigation.ForeignKey.IsRequired); } - var innerShaper = entityProjectionExpression.BindNavigation(navigation); - if (innerShaper == null) + var entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression); + var foreignKey = navigation.ForeignKey; + + if (targetEntityType.IsMappedToJson()) { - // 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(); - if (table.GetReferencingRowInternalForeignKeys(foreignKey.PrincipalEntityType).Contains(foreignKey) == true) + var innerShaper = entityProjectionExpression.BindNavigation(navigation); + if (innerShaper != null) { - // Mapped to same table - // We get identifying column to figure out tableExpression to pull columns from and nullability of most principal side - var identifyingColumn = entityProjectionExpression.BindProperty(entityType.FindPrimaryKey()!.Properties.First()); - var principalNullable = identifyingColumn.IsNullable - // Also make nullable if navigation is on derived type and and principal is TPT - // Since identifying PK would be non-nullable but principal can still be null - // Derived owned navigation does not de-dupe the PK column which for principal is from base table - // and for dependent on derived table - || (entityType.FindDiscriminatorProperty() == null - && navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType)); - - var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression( - targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable); - - if (entityProjection != null) - { - innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable); - } + return navigation.IsCollection + ? (JsonQueryExpression)innerShaper.ValueBufferExpression + : innerShaper; } - - if (innerShaper == null) + } + else + { + if (navigation.IsCollection) { - // InnerShaper is still null if either it is not table sharing or we failed to find table to pick data from - // So we find the table it is mapped to and generate join with it. - // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 - // So there is no handling for dependent having TPT - table = targetEntityType.GetViewOrTableMappings().Single().Table; var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable( entityProjectionExpression, - table, + targetEntityType.GetViewOrTableMappings().Single().Table, navigation); - var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); + var innerShapedQuery = CreateShapedQueryExpression( + targetEntityType, innerSelectExpression); var makeNullable = foreignKey.PrincipalKey.Properties .Concat(foreignKey.Properties) .Select(p => p.ClrType) .Any(t => t.IsNullableType()); + var innerSequenceType = innerShapedQuery.Type.GetSequenceType(); + var correlationPredicateParameter = Expression.Parameter(innerSequenceType); + var outerKey = entityShaperExpression.CreateKeyValuesExpression( navigation.IsOnDependent ? foreignKey.Properties : foreignKey.PrincipalKey.Properties, makeNullable); - var innerKey = innerShapedQuery.ShaperExpression.CreateKeyValuesExpression( + var innerKey = correlationPredicateParameter.CreateKeyValuesExpression( navigation.IsOnDependent ? foreignKey.PrincipalKey.Properties : foreignKey.Properties, makeNullable); - var joinPredicate = _sqlTranslator.Translate( - EntityFrameworkCore.Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey))!; - // Following conditions should match conditions for pushdown on outer during SelectExpression.AddJoin method - var pushdownRequired = _selectExpression.Limit != null - || _selectExpression.Offset != null - || _selectExpression.IsDistinct - || _selectExpression.GroupBy.Count > 0; - _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); - - // If pushdown was required on SelectExpression then we need to fetch the updated entity projection - if (pushdownRequired) + var keyComparison = Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey); + + var predicate = makeNullable + ? Expression.AndAlso( + outerKey is NewArrayExpression newArrayExpression + ? newArrayExpression.Expressions + .Select( + e => + { + var left = (e as UnaryExpression)?.Operand ?? e; + + return Expression.NotEqual(left, Expression.Constant(null, left.Type)); + }) + .Aggregate((l, r) => Expression.AndAlso(l, r)) + : Expression.NotEqual(outerKey, Expression.Constant(null, outerKey.Type)), + keyComparison) + : (Expression)keyComparison; + + var correlationPredicate = Expression.Lambda(predicate, correlationPredicateParameter); + + return Expression.Call( + QueryableMethods.Where.MakeGenericMethod(innerSequenceType), + innerShapedQuery, + Expression.Quote(correlationPredicate)); + } + + 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(); + if (table.GetReferencingRowInternalForeignKeys(foreignKey.PrincipalEntityType).Contains(foreignKey) == true) + { + // Mapped to same table + // We get identifying column to figure out tableExpression to pull columns from and nullability of most principal side + var identifyingColumn = entityProjectionExpression.BindProperty(entityType.FindPrimaryKey()!.Properties.First()); + var principalNullable = identifyingColumn.IsNullable + // Also make nullable if navigation is on derived type and and principal is TPT + // Since identifying PK would be non-nullable but principal can still be null + // Derived owned navigation does not de-dupe the PK column which for principal is from base table + // and for dependent on derived table + || (entityType.FindDiscriminatorProperty() == null + && navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType)); + + var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable); + + if (entityProjection != null) + { + innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable); + } + } + + if (innerShaper == null) { - if (doee is not null) + // InnerShaper is still null if either it is not table sharing or we failed to find table to pick data from + // So we find the table it is mapped to and generate join with it. + // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 + // So there is no handling for dependent having TPT + table = targetEntityType.GetViewOrTableMappings().Single().Table; + var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable( + entityProjectionExpression, + table, + navigation); + + var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); + + var makeNullable = foreignKey.PrincipalKey.Properties + .Concat(foreignKey.Properties) + .Select(p => p.ClrType) + .Any(t => t.IsNullableType()); + + var outerKey = entityShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.Properties + : foreignKey.PrincipalKey.Properties, + makeNullable); + var innerKey = innerShapedQuery.ShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.PrincipalKey.Properties + : foreignKey.Properties, + makeNullable); + + var joinPredicate = _sqlTranslator.Translate( + Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey))!; + // Following conditions should match conditions for pushdown on outer during SelectExpression.AddJoin method + var pushdownRequired = _selectExpression.Limit != null + || _selectExpression.Offset != null + || _selectExpression.IsDistinct + || _selectExpression.GroupBy.Count > 0; + _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); + + // If pushdown was required on SelectExpression then we need to fetch the updated entity projection + if (pushdownRequired) { - entityShaperExpression = _deferredOwnedExpansionRemover.UnwrapDeferredEntityProjectionExpression(doee); + if (doee is not null) + { + entityShaperExpression = _deferredOwnedExpansionRemover.UnwrapDeferredEntityProjectionExpression(doee); + } + + entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression); } - entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression); - } + var leftJoinTable = _selectExpression.Tables.Last(); - var leftJoinTable = _selectExpression.Tables.Last(); + innerShaper = new RelationalEntityShaperExpression( + targetEntityType, + _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, null, leftJoinTable, nullable: true)!, + nullable: true); + } - innerShaper = new RelationalEntityShaperExpression( - targetEntityType, - _selectExpression.GenerateWeakEntityProjectionExpression( - targetEntityType, table, null, leftJoinTable, nullable: true)!, - nullable: true); + entityProjectionExpression.AddNavigationBinding(navigation, innerShaper); } - - entityProjectionExpression.AddNavigationBinding(navigation, innerShaper); } return doee is not null @@ -1734,6 +1761,26 @@ static TableExpressionBase FindRootTableExpressionForColumn(ColumnExpression col } } + private bool TryGetJsonQueryExpression( + EntityShaperExpression entityShaperExpression, + [NotNullWhen(true)] out JsonQueryExpression? jsonQueryExpression) + { + switch (entityShaperExpression.ValueBufferExpression) + { + case ProjectionBindingExpression projectionBindingExpression: + jsonQueryExpression = _selectExpression.GetProjection(projectionBindingExpression) as JsonQueryExpression; + return jsonQueryExpression != null; + + case JsonQueryExpression jqe: + jsonQueryExpression = jqe; + return true; + + default: + jsonQueryExpression = null; + return false; + } + } + private EntityProjectionExpression GetEntityProjectionExpression(EntityShaperExpression entityShaperExpression) => entityShaperExpression.ValueBufferExpression switch { @@ -1804,7 +1851,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 JsonQueryExpression => expression, _ => base.Visit(expression) }; diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index e4809d7726d..863d86e70c6 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; @@ -23,6 +24,9 @@ private sealed class ShaperProcessingExpressionVisitor : ExpressionVisitor private static readonly MethodInfo ThrowReadValueExceptionMethod = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ThrowReadValueException))!; + private static readonly MethodInfo ThrowExtractJsonPropertyExceptionMethod = + typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ThrowExtractJsonPropertyException))!; + // Coordinating results private static readonly MemberInfo ResultContextValuesMemberInfo = typeof(ResultContext).GetMember(nameof(ResultContext.Values))[0]; @@ -67,9 +71,36 @@ private static readonly MethodInfo PopulateSplitCollectionAsyncMethodInfo private static readonly MethodInfo TaskAwaiterMethodInfo = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(TaskAwaiter))!; + private static readonly MethodInfo IncludeJsonEntityReferenceMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(IncludeJsonEntityReference))!; + + private static readonly MethodInfo IncludeJsonEntityCollectionMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(IncludeJsonEntityCollection))!; + + private static readonly MethodInfo MaterializeJsonEntityMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonEntity))!; + + private static readonly MethodInfo MaterializeJsonEntityCollectionMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonEntityCollection))!; + private static readonly MethodInfo CollectionAccessorAddMethodInfo = typeof(IClrCollectionAccessor).GetTypeInfo().GetDeclaredMethod(nameof(IClrCollectionAccessor.Add))!; + private static readonly MethodInfo ExtractJsonPropertyMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ExtractJsonProperty))!; + + private static readonly MethodInfo JsonElementGetPropertyMethod + = typeof(JsonElement).GetMethod(nameof(JsonElement.GetProperty), new[] { typeof(string) })!; + + private static readonly PropertyInfo _objectArrayIndexerPropertyInfo + = typeof(object[]).GetProperty("Item")!; + + private static readonly PropertyInfo _nullableJsonElementHasValuePropertyInfo + = typeof(JsonElement?).GetProperty(nameof(Nullable.HasValue))!; + + private static readonly PropertyInfo _nullableJsonElementValuePropertyInfo + = typeof(JsonElement?).GetProperty(nameof(Nullable.Value))!; + private readonly RelationalShapedQueryCompilingExpressionVisitor _parentVisitor; private readonly ISet? _tags; private readonly bool _isTracking; @@ -110,13 +141,13 @@ private static readonly MethodInfo CollectionAccessorAddMethodInfo private int _collectionId; // States to convert code to data reader read - private readonly IDictionary> _materializationContextBindings - = new Dictionary>(); - - private readonly IDictionary _entityTypeIdentifyingExpressionInfo - = new Dictionary(); - private readonly IDictionary _singleEntityTypeDiscriminatorValues - = new Dictionary(); + private readonly Dictionary> _materializationContextBindings = new(); + private readonly Dictionary _entityTypeIdentifyingExpressionInfo = new(); + private readonly Dictionary _singleEntityTypeDiscriminatorValues = new(); + private readonly Dictionary _jsonValueBufferParameterMapping = new(); + private readonly Dictionary _jsonMaterializationContextParameterMapping = new(); + private readonly Dictionary<(int, string[]), ParameterExpression> _existingJsonElementMap + = new(new ExisitingJsonElementMapKeyComparer()); public ShaperProcessingExpressionVisitor( RelationalShapedQueryCompilingExpressionVisitor parentVisitor, @@ -358,22 +389,33 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) && parameterExpression.Type == typeof(MaterializationContext)) { var newExpression = (NewExpression)binaryExpression.Right; - var projectionBindingExpression = (ProjectionBindingExpression)newExpression.Arguments[0]; - - var propertyMap = (IDictionary)GetProjectionIndex(projectionBindingExpression); - _materializationContextBindings[parameterExpression] = propertyMap; - _entityTypeIdentifyingExpressionInfo[parameterExpression] = - // If single entity type is being selected in hierarchy then we use the value directly else we store the offset to - // read discriminator value. - _singleEntityTypeDiscriminatorValues.TryGetValue(projectionBindingExpression, out var value) - ? value - : propertyMap.Values.Max() + 1; + if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression) + { + var propertyMap = (IDictionary)GetProjectionIndex(projectionBindingExpression); + _materializationContextBindings[parameterExpression] = propertyMap; + _entityTypeIdentifyingExpressionInfo[parameterExpression] = + // If single entity type is being selected in hierarchy then we use the value directly else we store the offset to + // read discriminator value. + _singleEntityTypeDiscriminatorValues.TryGetValue(projectionBindingExpression, out var value) + ? value + : propertyMap.Values.Max() + 1; + + var updatedExpression = newExpression.Update( + new[] { Expression.Constant(ValueBuffer.Empty), newExpression.Arguments[1] }); + + return Expression.Assign(binaryExpression.Left, updatedExpression); + } + else if (newExpression.Arguments[0] is ParameterExpression valueBufferParameter + && _jsonValueBufferParameterMapping.ContainsKey(valueBufferParameter)) + { + _jsonMaterializationContextParameterMapping[parameterExpression] = _jsonValueBufferParameterMapping[valueBufferParameter]; - var updatedExpression = newExpression.Update( - new[] { Expression.Constant(ValueBuffer.Empty), newExpression.Arguments[1] }); + var updatedExpression = newExpression.Update( + new[] { Expression.Constant(ValueBuffer.Empty), newExpression.Arguments[1] }); - return Expression.Assign(binaryExpression.Left, updatedExpression); + return Expression.Assign(binaryExpression.Left, updatedExpression); + } } if (binaryExpression.NodeType == ExpressionType.Assign @@ -391,39 +433,66 @@ protected override Expression VisitExtension(Expression extensionExpression) { switch (extensionExpression) { - case RelationalEntityShaperExpression entityShaperExpression: + case RelationalEntityShaperExpression entityShaperExpression + when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression: { if (!_variableShaperMapping.TryGetValue(entityShaperExpression.ValueBufferExpression, out var accessor)) { - var entityParameter = Expression.Parameter(entityShaperExpression.Type); - _variables.Add(entityParameter); - if (entityShaperExpression.EntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) - { - var concreteTypes = entityShaperExpression.EntityType.GetDerivedTypesInclusive().Where(e => !e.IsAbstract()).ToArray(); - // Single concrete TPC entity type won't have discriminator column. - // We store the value here and inject it directly rather than reading from server. - if (concreteTypes.Length == 1) - _singleEntityTypeDiscriminatorValues[(ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression] - = concreteTypes[0].ShortName(); - } - - var entityMaterializationExpression = _parentVisitor.InjectEntityMaterializers(entityShaperExpression); - entityMaterializationExpression = Visit(entityMaterializationExpression); - - _expressions.Add(Expression.Assign(entityParameter, entityMaterializationExpression)); - - if (_containsCollectionMaterialization) + if (GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[]> jsonProjectionIndex) { - _valuesArrayInitializers!.Add(entityParameter); - accessor = Expression.Convert( - Expression.ArrayIndex( - _valuesArrayExpression!, - Expression.Constant(_valuesArrayInitializers.Count - 1)), - entityShaperExpression.Type); + // json entity at the root + var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( + jsonProjectionIndex, + entityShaperExpression.EntityType, + isCollection: false); + + var shaperResult = CreateJsonShapers( + entityShaperExpression.EntityType, + entityShaperExpression.IsNullable, + collection: false, + jsonElementParameter, + keyValuesParameter, + outerEntityInstanceParameter: null, + navigation: null); + + var visitedShaperResult = Visit(shaperResult); + var visitedShaperResultParameter = Expression.Parameter(visitedShaperResult.Type); + _variables.Add(visitedShaperResultParameter); + _expressions.Add(Expression.Assign(visitedShaperResultParameter, visitedShaperResult)); + accessor = visitedShaperResultParameter; } else { - accessor = entityParameter; + var entityParameter = Expression.Parameter(entityShaperExpression.Type); + _variables.Add(entityParameter); + if (entityShaperExpression.EntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) + { + var concreteTypes = entityShaperExpression.EntityType.GetDerivedTypesInclusive().Where(e => !e.IsAbstract()).ToArray(); + // Single concrete TPC entity type won't have discriminator column. + // We store the value here and inject it directly rather than reading from server. + if (concreteTypes.Length == 1) + _singleEntityTypeDiscriminatorValues[(ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression] + = concreteTypes[0].ShortName(); + } + + var entityMaterializationExpression = _parentVisitor.InjectEntityMaterializers(entityShaperExpression); + entityMaterializationExpression = Visit(entityMaterializationExpression); + + _expressions.Add(Expression.Assign(entityParameter, entityMaterializationExpression)); + + if (_containsCollectionMaterialization) + { + _valuesArrayInitializers!.Add(entityParameter); + accessor = Expression.Convert( + Expression.ArrayIndex( + _valuesArrayExpression!, + Expression.Constant(_valuesArrayInitializers.Count - 1)), + entityShaperExpression.Type); + } + else + { + accessor = entityParameter; + } } _variableShaperMapping[entityShaperExpression.ValueBufferExpression] = accessor; @@ -432,6 +501,31 @@ protected override Expression VisitExtension(Expression extensionExpression) return accessor; } + case CollectionResultExpression collectionResultExpression + when collectionResultExpression.Navigation is INavigation navigation + && GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) + is ValueTuple, string[]> jsonProjectionIndex: + { + // json entity collection at the root + var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( + jsonProjectionIndex, + navigation.TargetEntityType, + isCollection: true); + + var shaperResult = CreateJsonShapers( + navigation.TargetEntityType, + nullable: true, + collection: true, + jsonElementParameter, + keyValuesParameter, + outerEntityInstanceParameter: null, + navigation); + + var visitedShaperResult = Visit(shaperResult); + + return visitedShaperResult; + } + case ProjectionBindingExpression projectionBindingExpression when _inline: { @@ -674,6 +768,34 @@ protected override Expression VisitExtension(Expression extensionExpression) } else { + var projectionBindingExpression = (includeExpression.NavigationExpression as CollectionResultExpression)?.ProjectionBindingExpression + ?? (includeExpression.NavigationExpression as RelationalEntityShaperExpression)?.ValueBufferExpression as ProjectionBindingExpression; + + // json include case + if (projectionBindingExpression != null + && GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[]> jsonProjectionIndex) + { + var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( + jsonProjectionIndex, + includeExpression.Navigation.TargetEntityType, + includeExpression.Navigation.IsCollection); + + var shaperResult = CreateJsonShapers( + includeExpression.Navigation.TargetEntityType, + nullable: true, + collection: includeExpression.NavigationExpression is CollectionResultExpression, + jsonElementParameter, + keyValuesParameter, + outerEntityInstanceParameter: (ParameterExpression)entity, + navigation: (INavigation)includeExpression.Navigation); + + var visitedShaperResult = Visit(shaperResult); + + _expressions.Add(visitedShaperResult); + + return entity; + } + var navigationExpression = Visit(includeExpression.NavigationExpression); var entityType = entity.Type; var navigation = includeExpression.Navigation; @@ -890,45 +1012,316 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && 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!; - int projectionIndex; - if (property == null) + + if (_jsonMaterializationContextParameterMapping.ContainsKey(mappingParameter)) + { + var (jsonElementParameter, keyPropertyValuesParameter) = _jsonMaterializationContextParameterMapping[mappingParameter]; + + return property!.IsPrimaryKey() + ? Expression.MakeIndex( + keyPropertyValuesParameter, + _objectArrayIndexerPropertyInfo, + new[] { Expression.Constant(index) }) + : CreateExtractJsonPropertyExpression(jsonElementParameter, property); + } + else { - // This is trying to read the computed discriminator value - var storedInfo = _entityTypeIdentifyingExpressionInfo[mappingParameter]; - if (storedInfo is string s) + int projectionIndex; + if (property == null) { - // 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); + // 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] + index; + } + else + { + projectionIndex = _materializationContextBindings[mappingParameter][property]; } - projectionIndex = (int)_entityTypeIdentifyingExpressionInfo[mappingParameter] - + methodCallExpression.Arguments[1].GetConstantValue(); + 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."); + + return CreateGetValueExpression( + _dataReaderParameter, + projectionIndex, + nullable, + projection.Expression.TypeMapping!, + methodCallExpression.Type, + property); } - else + } + + return base.VisitMethodCall(methodCallExpression); + } + + private Expression CreateJsonShapers( + IEntityType entityType, + bool nullable, + bool collection, + ParameterExpression jsonElementParameter, + ParameterExpression keyValuesParameter, + ParameterExpression? outerEntityInstanceParameter, + INavigation? navigation) + { + var jsonElementShaperLambdaParameter = Expression.Parameter(typeof(JsonElement)); + var keyValuesShaperLambdaParameter = Expression.Parameter(typeof(object[])); + var shaperBlockVariables = new List(); + var shaperBlockExpressions = new List(); + + var valueBufferParameter = Expression.Parameter(typeof(ValueBuffer)); + + _jsonValueBufferParameterMapping[valueBufferParameter] = (jsonElementShaperLambdaParameter, keyValuesShaperLambdaParameter); + + var entityShaperExpression = new RelationalEntityShaperExpression( + entityType, + valueBufferParameter, + nullable); + + var entityShaperMaterializer = (BlockExpression)_parentVisitor.InjectEntityMaterializers(entityShaperExpression); + var entityShaperMaterializerVariable = Expression.Variable(entityShaperMaterializer.Type); + shaperBlockVariables.Add(entityShaperMaterializerVariable); + shaperBlockExpressions.Add(Expression.Assign(entityShaperMaterializerVariable, entityShaperMaterializer)); + + foreach (var ownedNavigation in entityType.GetNavigations().Where( + n => n.TargetEntityType.IsMappedToJson() && n.ForeignKey.IsOwnership && n == n.ForeignKey.PrincipalToDependent)) + { + // TODO: use caching like we do in pre-process, there's chance we already have this json element + var innerJsonElementParameter = Expression.Variable( + typeof(JsonElement?)); + + shaperBlockVariables.Add(innerJsonElementParameter); + + // TODO: do TryGetProperty and short circuit if failed instead + var innerJsonElementAssignment = Expression.Assign( + innerJsonElementParameter, + Expression.Convert( + Expression.Call( + jsonElementShaperLambdaParameter, + JsonElementGetPropertyMethod, + Expression.Constant(ownedNavigation.TargetEntityType.GetJsonPropertyName())), + typeof(JsonElement?))); + + shaperBlockExpressions.Add(innerJsonElementAssignment); + + var innerShaperResult = CreateJsonShapers( + ownedNavigation.TargetEntityType, + nullable || !ownedNavigation.ForeignKey.IsRequired, + ownedNavigation.IsCollection, + innerJsonElementParameter, + keyValuesShaperLambdaParameter, + entityShaperMaterializerVariable, + ownedNavigation); + + shaperBlockExpressions.Add(innerShaperResult); + } + + shaperBlockExpressions.Add(entityShaperMaterializerVariable); + + var shaperBlock = Expression.Block( + shaperBlockVariables, + shaperBlockExpressions); + + var shaperLambda = Expression.Lambda( + shaperBlock, + QueryCompilationContext.QueryContextParameter, + keyValuesShaperLambdaParameter, + jsonElementShaperLambdaParameter); + + if (outerEntityInstanceParameter != null) + { + Debug.Assert(navigation != null, "Navigation shouldn't be null when including."); + + var fixup = GenerateFixup( + navigation.DeclaringEntityType.ClrType, + navigation.TargetEntityType.ClrType, + navigation, + navigation.Inverse); + + // inheritance scenario - navigation defined on derived + var outerEntityInstanceExpression = outerEntityInstanceParameter.Type != navigation.DeclaringEntityType.ClrType + ? Expression.Convert(outerEntityInstanceParameter, navigation.DeclaringEntityType.ClrType) + : (Expression)outerEntityInstanceParameter; + + if (navigation.IsCollection) { - projectionIndex = _materializationContextBindings[mappingParameter][property]; + var includeJsonEntityCollectionMethodCall = + Expression.Call( + IncludeJsonEntityCollectionMethodInfo.MakeGenericMethod( + navigation.DeclaringEntityType.ClrType, + navigation.TargetEntityType.ClrType), + QueryCompilationContext.QueryContextParameter, + jsonElementParameter, + keyValuesParameter, + outerEntityInstanceExpression, + shaperLambda, + fixup); + + return navigation.DeclaringEntityType.ClrType.IsAssignableFrom(outerEntityInstanceParameter.Type) + ? includeJsonEntityCollectionMethodCall + : Expression.IfThen( + Expression.TypeIs( + outerEntityInstanceParameter, + navigation.DeclaringEntityType.ClrType), + includeJsonEntityCollectionMethodCall); } - var projection = _selectExpression.Projection[projectionIndex]; - var nullable = IsNullableProjection(projection); + var includeJsonEntityReferenceMethodCall = + Expression.Call( + IncludeJsonEntityReferenceMethodInfo.MakeGenericMethod( + navigation.DeclaringEntityType.ClrType, + navigation.TargetEntityType.ClrType), + QueryCompilationContext.QueryContextParameter, + jsonElementParameter, + keyValuesParameter, + outerEntityInstanceExpression, + shaperLambda, + fixup); + + return navigation.DeclaringEntityType.ClrType.IsAssignableFrom(outerEntityInstanceParameter.Type) + ? includeJsonEntityReferenceMethodCall + : Expression.IfThen( + Expression.TypeIs( + outerEntityInstanceParameter, + navigation.DeclaringEntityType.ClrType), + includeJsonEntityReferenceMethodCall); + } - Check.DebugAssert( - !nullable || property != null || methodCallExpression.Type.IsNullableType(), - "For nullable reads the return type must be null unless property is specified."); + if (collection) + { + Debug.Assert(navigation != null, "navigation shouldn't be null when materializing collection."); - return CreateGetValueExpression( - _dataReaderParameter, - projectionIndex, - nullable, - projection.Expression.TypeMapping!, - methodCallExpression.Type, - property); + var materializeJsonEntityCollection = Expression.Call( + MaterializeJsonEntityCollectionMethodInfo.MakeGenericMethod( + entityType.ClrType, + navigation.ClrType), + QueryCompilationContext.QueryContextParameter, + jsonElementParameter, + keyValuesParameter, + Expression.Constant(navigation), + shaperLambda); + + return materializeJsonEntityCollection; } - return base.VisitMethodCall(methodCallExpression); + var materializedRootJsonEntity = Expression.Call( + MaterializeJsonEntityMethodInfo.MakeGenericMethod(entityType.ClrType), + QueryCompilationContext.QueryContextParameter, + jsonElementParameter, + keyValuesParameter, + Expression.Constant(nullable), + shaperLambda); + + return materializedRootJsonEntity; + } + + private (ParameterExpression, ParameterExpression) JsonShapingPreProcess( + ValueTuple, string[]> projectionIndex, + IEntityType entityType, + bool isCollection) + { + var jsonColumnProjectionIndex = projectionIndex.Item1; + var keyInfo = projectionIndex.Item2; + var additionalPath = projectionIndex.Item3; + + var keyValuesParameter = Expression.Parameter(typeof(object[])); + var keyValues = new Expression[keyInfo.Count]; + + for (var i = 0; i < keyInfo.Count; i++) + { + var projection = _selectExpression.Projection[keyInfo[i].Item2]; + + keyValues[i] = Expression.Convert( + CreateGetValueExpression( + _dataReaderParameter, + keyInfo[i].Item2, + IsNullableProjection(projection), + projection.Expression.TypeMapping!, + keyInfo[i].Item1.ClrType, + keyInfo[i].Item1), + typeof(object)); + } + + var keyValuesInitialize = Expression.NewArrayInit(typeof(object), keyValues); + var keyValuesAssignment = Expression.Assign(keyValuesParameter, keyValuesInitialize); + + _variables.Add(keyValuesParameter); + _expressions.Add(keyValuesAssignment); + + var jsonColumnTypeMapping = entityType.GetContainerColumnTypeMapping()!; + if (_existingJsonElementMap.TryGetValue((jsonColumnProjectionIndex, additionalPath), out var exisitingJsonElementVariable)) + { + return (exisitingJsonElementVariable, keyValuesParameter); + } + + // TODO: this logic could/should be improved (later) + var currentJsonElementVariable = default(ParameterExpression); + var index = 0; + do + { + // try to find JsonElement variable for this json column and path if we encountered (and cached it) before + // otherwise either create new JsonElement from the data reader if we are at root level + // or build on top of previous variable withing the navigation chain (e.g. when we encountered the root before, but not this entire path) + if (!_existingJsonElementMap.TryGetValue((jsonColumnProjectionIndex, additionalPath[..index]), out var exisitingJsonElementVariable2)) + { + var jsonElementVariable = Expression.Variable( + typeof(JsonElement?)); + + var jsonElementValueExpression = index == 0 + ? CreateGetValueExpression( + _dataReaderParameter, + jsonColumnProjectionIndex, + nullable: true, + jsonColumnTypeMapping, + typeof(JsonElement?), + property: null) + : Expression.Condition( + Expression.MakeMemberAccess( + currentJsonElementVariable!, + _nullableJsonElementHasValuePropertyInfo), + Expression.Convert( + Expression.Call( + Expression.MakeMemberAccess( + currentJsonElementVariable!, + _nullableJsonElementValuePropertyInfo), + JsonElementGetPropertyMethod, + Expression.Constant(additionalPath[index - 1])), + currentJsonElementVariable!.Type), + Expression.Default(currentJsonElementVariable!.Type)); + + var jsonElementAssignment = Expression.Assign( + jsonElementVariable, + jsonElementValueExpression); + + _variables.Add(jsonElementVariable); + _expressions.Add(jsonElementAssignment); + _existingJsonElementMap[(jsonColumnProjectionIndex, additionalPath[..index])] = jsonElementVariable; + + currentJsonElementVariable = jsonElementVariable; + } + else + { + currentJsonElementVariable = exisitingJsonElementVariable2; + } + + index++; + } + while (index <= additionalPath.Length); + + return (currentJsonElementVariable!, keyValuesParameter); } private static LambdaExpression GenerateFixup( @@ -1159,6 +1552,76 @@ private static TValue ThrowReadValueException( throw new InvalidOperationException(message, exception); } + private Expression CreateExtractJsonPropertyExpression( + ParameterExpression jsonElementParameter, + IProperty property) + { + Expression resultExpression; + if (property.GetTypeMapping().Converter is ValueConverter converter) + { + resultExpression = Expression.Call( + ExtractJsonPropertyMethodInfo, + jsonElementParameter, + Expression.Constant(property.GetJsonPropertyName()), + Expression.Constant(converter.ProviderClrType)); + + if (resultExpression.Type != converter.ProviderClrType) + { + resultExpression = Expression.Convert(resultExpression, converter.ProviderClrType); + } + + resultExpression = ReplacingExpressionVisitor.Replace( + converter.ConvertFromProviderExpression.Parameters.Single(), + resultExpression, + converter.ConvertFromProviderExpression.Body); + } + else + { + resultExpression = Expression.Convert( + Expression.Call( + ExtractJsonPropertyMethodInfo, + jsonElementParameter, + Expression.Constant(property.GetJsonPropertyName()), + Expression.Constant(property.ClrType)), + property.ClrType); + } + + if (_detailedErrorsEnabled) + { + var exceptionParameter = Expression.Parameter(typeof(Exception), name: "e"); + var catchBlock = Expression.Catch( + exceptionParameter, + Expression.Call( + ThrowExtractJsonPropertyExceptionMethod.MakeGenericMethod(resultExpression.Type), + exceptionParameter, + Expression.Constant(property, typeof(IProperty)))); + + resultExpression = Expression.TryCatch(resultExpression, catchBlock); + } + + return resultExpression; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TValue ThrowExtractJsonPropertyException( + Exception exception, + IProperty property) + { + var entityType = property.DeclaringType.DisplayName(); + var propertyName = property.Name; + + throw new InvalidOperationException( + RelationalStrings.JsonErrorExtractingJsonProperty(entityType, propertyName), + exception); + } + + private static object? ExtractJsonProperty(JsonElement element, string propertyName, Type returnType) + { + var jsonElementProperty = element.GetProperty(propertyName); + + return jsonElementProperty.Deserialize(returnType); + } + private static void IncludeReference( QueryContext queryContext, TEntity entity, @@ -1895,6 +2358,107 @@ static async Task InitializeReaderAsync( dataReaderContext.HasNext = false; } + private static void IncludeJsonEntityReference( + QueryContext queryContext, + JsonElement? jsonElement, + object[] keyPropertyValues, + TIncludingEntity entity, + Func innerShaper, + Action fixup) + where TIncludingEntity : class + where TIncludedEntity : class + { + if (jsonElement.HasValue && jsonElement.Value.ValueKind != JsonValueKind.Null) + { + var included = innerShaper(queryContext, keyPropertyValues, jsonElement.Value); + fixup(entity, included); + } + } + + private static void IncludeJsonEntityCollection( + QueryContext queryContext, + JsonElement? jsonElement, + object[] keyPropertyValues, + TIncludingEntity entity, + Func innerShaper, + Action fixup) + where TIncludingEntity : class + where TIncludedCollectionElement : class + { + if (jsonElement.HasValue && jsonElement.Value.ValueKind != JsonValueKind.Null) + { + var newKeyPropertyValues = new object[keyPropertyValues.Length + 1]; + Array.Copy(keyPropertyValues, newKeyPropertyValues, keyPropertyValues.Length); + + var i = 0; + foreach (var jsonArrayElement in jsonElement.Value.EnumerateArray()) + { + newKeyPropertyValues[^1] = ++i; + + var resultElement = innerShaper(queryContext, newKeyPropertyValues, jsonArrayElement); + + fixup(entity, resultElement); + } + } + } + + private static TEntity? MaterializeJsonEntity( + QueryContext queryContext, + JsonElement? jsonElement, + object[] keyPropertyValues, + bool nullable, + Func shaper) + where TEntity : class + { + if (jsonElement.HasValue && jsonElement.Value.ValueKind != JsonValueKind.Null) + { + var result = shaper(queryContext, keyPropertyValues, jsonElement.Value); + + return result; + } + + if (nullable) + { + return default(TEntity); + } + + throw new InvalidOperationException( + RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TEntity).Name)); + } + + private static TResult? MaterializeJsonEntityCollection( + QueryContext queryContext, + JsonElement? jsonElement, + object[] keyPropertyValues, + INavigationBase navigation, + Func innerShaper) + where TEntity : class + where TResult : ICollection + { + if (jsonElement.HasValue && jsonElement.Value.ValueKind != JsonValueKind.Null) + { + 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.Value.EnumerateArray()) + { + newKeyPropertyValues[^1] = ++i; + + var resultElement = innerShaper(queryContext, newKeyPropertyValues, jsonArrayElement); + + result.Add(resultElement); + } + + return result; + } + + return default(TResult); + } + private static async Task TaskAwaiter(Func[] taskFactories) { for (var i = 0; i < taskFactories.Length; i++) @@ -1949,5 +2513,14 @@ public bool ContainsCollectionMaterialization(Expression expression) return base.Visit(expression); } } + + private sealed class ExisitingJsonElementMapKeyComparer : IEqualityComparer<(int, string[])> + { + public bool Equals((int, string[]) x, (int, string[]) y) + => x.Item1 == y.Item1 && x.Item2.Length == y.Item2.Length && x.Item2.SequenceEqual(y.Item2); + + public int GetHashCode([DisallowNull] (int, string[]) obj) + => HashCode.Combine(obj.Item1, obj.Item2?.Length); + } } } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index e6c37bd1a48..733eab08943 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -233,9 +233,11 @@ 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( 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 0603974a5e3..83f40952443 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -596,6 +596,7 @@ protected override Expression VisitExtension(Expression extensionExpression) case EntityReferenceExpression: case SqlExpression: case EnumerableExpression: + case JsonQueryExpression: return extensionExpression; case RelationalGroupByShaperExpression relationalGroupByShaperExpression: @@ -1304,6 +1305,10 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) if (entityReferenceExpression.ParameterEntity != null) { var valueBufferExpression = Visit(entityReferenceExpression.ParameterEntity.ValueBufferExpression); + if (valueBufferExpression is JsonQueryExpression jsonQueryExpression) + { + return jsonQueryExpression.BindProperty(property); + } var entityProjectionExpression = (EntityProjectionExpression)valueBufferExpression; var propertyAccess = entityProjectionExpression.BindProperty(property); diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index e337001b143..d37cc923a5e 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -54,6 +54,7 @@ ShapedQueryExpression shapedQueryExpression TableExpression tableExpression => VisitTable(tableExpression), UnionExpression unionExpression => VisitUnion(unionExpression), UpdateExpression updateExpression => VisitUpdate(updateExpression), + JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), _ => base.VisitExtension(extensionExpression), }; @@ -281,4 +282,11 @@ ShapedQueryExpression shapedQueryExpression /// The expression to visit. /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitUpdate(UpdateExpression updateExpression); + + /// + /// Visits the children of the JSON scalar expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs new file mode 100644 index 00000000000..afc8845b253 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs @@ -0,0 +1,109 @@ +// 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 +{ + /// + /// Expression representing a scalar extracted from a JSON column with the given path. + /// + public class JsonScalarExpression : SqlExpression + { + /// + /// Creates a new instance of the class. + /// + /// A column containg JSON. + /// A property representing the result of this expression. + /// A JSON path leading to the scalar from the root of the JSON stored in the column. + /// A value indicating whether the expression is nullable. + public JsonScalarExpression( + ColumnExpression jsonColumn, + IProperty property, + SqlExpression jsonPath, + bool nullable) + : this(jsonColumn, property.ClrType, property.FindRelationalTypeMapping()!, jsonPath, nullable) + { + } + + internal JsonScalarExpression( + ColumnExpression jsonColumn, + Type type, + RelationalTypeMapping typeMapping, + SqlExpression jsonPath, + bool nullable) + : base(type, typeMapping) + { + JsonColumn = jsonColumn; + JsonPath = jsonPath; + IsNullable = nullable; + } + + /// + /// The column containg JSON. + /// + public virtual ColumnExpression JsonColumn { get; } + + /// + /// The JSON path leading to the scalar from the root of the JSON stored in the column. + /// + public virtual SqlExpression JsonPath { get; } + + /// + /// The value indicating whether the expression is nullable. + /// + public virtual bool IsNullable { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn); + var jsonColumnMadeNullable = jsonColumn.IsNullable && !JsonColumn.IsNullable; + + return jsonColumn != JsonColumn + ? new JsonScalarExpression( + jsonColumn, + Type, + TypeMapping!, + JsonPath, + IsNullable || jsonColumnMadeNullable) + : this; + } + + /// + /// 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. + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual JsonScalarExpression Update( + ColumnExpression jsonColumn, + SqlExpression jsonPath, + bool nullable) + => jsonColumn != JsonColumn + || jsonPath != JsonPath + || nullable != IsNullable + ? new JsonScalarExpression(jsonColumn, Type, TypeMapping!, jsonPath, nullable) + : this; + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("JsonScalarExpression(column: "); + expressionPrinter.Visit(JsonColumn); + expressionPrinter.Append(" Path: "); + expressionPrinter.Visit(JsonPath); + expressionPrinter.Append(")"); + } + + /// + public override bool Equals(object? obj) + => obj is JsonScalarExpression jsonScalarExpression + && JsonColumn.Equals(jsonScalarExpression.JsonColumn) + && JsonPath.Equals(jsonScalarExpression.JsonPath); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), JsonColumn, JsonPath); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 57624e48133..957f0070119 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -608,7 +608,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) => this; public override ConcreteColumnExpression MakeNullable() - => new(Name, _table, Type, TypeMapping!, true); + => IsNullable ? this : new(Name, _table, Type, TypeMapping!, true); public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect) => _table.UpdateTableReference(oldSelect, newSelect); @@ -750,6 +750,13 @@ public ClientProjectionRemappingExpressionVisitor(List clientProjectionI collectionResultExpression.ElementType); } + if (value is int) + { + var binding = (ProjectionBindingExpression)Visit(collectionResultExpression.ProjectionBindingExpression); + + return collectionResultExpression.Update(binding); + } + throw new InvalidOperationException(); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index cc07e0c98df..18c1087f7ae 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; @@ -427,6 +428,49 @@ void GenerateNonHierarchyNonSplittingEntityType(ITableBase table, TableExpressio } var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions); + + foreach (var ownedJsonNavigation in GetAllNavigationsInHierarchy(entityType) + .Where(n => n.ForeignKey.IsOwnership && n.TargetEntityType.IsMappedToJson() && n.ForeignKey.PrincipalToDependent == n)) + { + var targetEntityType = ownedJsonNavigation.TargetEntityType; + var jsonColumnName = targetEntityType.GetContainerColumnName()!; + var jsonColumnTypeMapping = targetEntityType.GetContainerColumnTypeMapping()!; + + var jsonColumn = new ConcreteColumnExpression( + jsonColumnName, + tableReferenceExpression, + jsonColumnTypeMapping.ClrType, + jsonColumnTypeMapping, + nullable: !ownedJsonNavigation.ForeignKey.IsRequiredDependent || ownedJsonNavigation.IsCollection); + + // for json collections we need to skip ordinal key (which is always the last one) + // simple copy from parent is safe here, because we only do it at top level + // so there is no danger of multiple keys being synthesized (like we have in multi-level nav chains) + var keyPropertiesMap = new Dictionary(); + var keyProperties = targetEntityType.FindPrimaryKey()!.Properties; + var keyPropertiesCount = ownedJsonNavigation.IsCollection + ? keyProperties.Count - 1 + : keyProperties.Count; + + for (var i = 0; i < keyPropertiesCount; i++) + { + var correspondingParentKeyProperty = ownedJsonNavigation.ForeignKey.PrincipalKey.Properties[i]; + keyPropertiesMap[keyProperties[i]] = propertyExpressions[correspondingParentKeyProperty]; + } + + var entityShaperExpression = new RelationalEntityShaperExpression( + targetEntityType, + new JsonQueryExpression( + targetEntityType, + jsonColumn, + ownedJsonNavigation.IsCollection, + keyPropertiesMap, + ownedJsonNavigation.ClrType), + !ownedJsonNavigation.ForeignKey.IsRequiredDependent); + + entityProjection.AddNavigationBinding(ownedJsonNavigation, entityShaperExpression); + } + _projectionMapping[new ProjectionMember()] = entityProjection; var primaryKey = entityType.FindPrimaryKey(); @@ -577,6 +621,25 @@ public void ApplyDistinct() entityProjectionValueComparers.Add(property.GetKeyValueComparer()); } } + else if (projection is JsonQueryExpression jsonQueryExpression) + { + if (jsonQueryExpression.IsCollection) + { + throw new InvalidOperationException(RelationalStrings.DistinctOnCollectionNotSupported); + } + + var primaryKeyProperties = jsonQueryExpression.EntityType.FindPrimaryKey()!.Properties; + var primaryKeyPropertiesCount = jsonQueryExpression.IsCollection + ? primaryKeyProperties.Count - 1 + : primaryKeyProperties.Count; + + for (var i = 0; i < primaryKeyPropertiesCount; i++) + { + var keyProperty = primaryKeyProperties[i]; + entityProjectionIdentifiers.Add((ColumnExpression)jsonQueryExpression.BindProperty(keyProperty)); + entityProjectionValueComparers.Add(keyProperty.GetKeyValueComparer()); + } + } else if (projection is SqlExpression sqlExpression) { otherExpressions.Add(sqlExpression); @@ -706,6 +769,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) @@ -721,6 +786,11 @@ public Expression ApplyProjection( containsSingleResult = true; } } + + if (projection is JsonQueryExpression) + { + jsonClientProjectionsCount++; + } } if (containsSingleResult @@ -775,6 +845,7 @@ static void UpdateLimit(SelectExpression selectExpression) } } + var jsonClientProjectionDeduplicationMap = BuildJsonProjectionDeduplicationMap(_clientProjections.OfType()); var earlierClientProjectionCount = _clientProjections.Count; var newClientProjections = new List(); var clientProjectionIndexMap = new List(); @@ -789,6 +860,10 @@ static void UpdateLimit(SelectExpression selectExpression) baseSelectExpression = (SelectExpression)cloningExpressionVisitor!.Visit(this); baseSelectExpression._mutable = true; baseSelectExpression._projection.Clear(); + + //since we updated the client projections, we also need updated deduplication map + jsonClientProjectionDeduplicationMap = BuildJsonProjectionDeduplicationMap( + _clientProjections.Skip(i).OfType()); } var value = _clientProjections[i]; @@ -803,6 +878,18 @@ static void UpdateLimit(SelectExpression selectExpression) break; } + case JsonQueryExpression jsonQueryExpression: + { + var jsonProjectionResult = AddJsonProjection( + jsonQueryExpression, + jsonScalarToAdd: jsonClientProjectionDeduplicationMap[jsonQueryExpression]); + + newClientProjections.Add(jsonProjectionResult); + clientProjectionIndexMap.Add(newClientProjections.Count - 1); + + break; + } + case SqlExpression sqlExpression: { var result = Constant(AddToProjection(sqlExpression, _aliasForClientProjections[i])); @@ -863,8 +950,10 @@ static void UpdateLimit(SelectExpression selectExpression) AddJoin(JoinType.OuterApply, ref innerSelectExpression, out _); var offset = _clientProjections.Count; var count = innerSelectExpression._clientProjections.Count; + _clientProjections.AddRange( innerSelectExpression._clientProjections.Select(e => MakeNullable(e, nullable: true))); + _aliasForClientProjections.AddRange(innerSelectExpression._aliasForClientProjections); innerShaperExpression = new ProjectionIndexRemappingExpressionVisitor( innerSelectExpression, @@ -1262,14 +1351,21 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express return innerShaperExpression; } } - + else { + var jsonProjectionDeduplicationMap = BuildJsonProjectionDeduplicationMap( + _projectionMapping.Select(x => x.Value).OfType()); + var result = new Dictionary(_projectionMapping.Count); + foreach (var (projectionMember, expression) in _projectionMapping) { - result[projectionMember] = expression is EntityProjectionExpression entityProjection - ? AddEntityProjection(entityProjection) - : Constant(AddToProjection((SqlExpression)expression, projectionMember.Last?.Name)); + result[projectionMember] = expression switch + { + EntityProjectionExpression entityProjection => AddEntityProjection(entityProjection), + JsonQueryExpression jsonQueryExpression => AddJsonProjection(jsonQueryExpression, jsonProjectionDeduplicationMap[jsonQueryExpression]), + _ => Constant(AddToProjection((SqlExpression)expression, projectionMember.Last?.Name)) + }; } _projectionMapping.Clear(); @@ -1278,6 +1374,44 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express return shaperExpression; } + static Dictionary BuildJsonProjectionDeduplicationMap(IEnumerable projections) + { + // force reference comparison for this one, even if we implement custom equality for JsonQueryExpression in the future + var deduplicationMap = new Dictionary(ReferenceEqualityComparer.Instance); + if (projections.Count() > 0) + { + var ordered = projections + .OrderBy(x => $"{x.JsonColumn.TableAlias}.{x.JsonColumn.Name}") + .ThenBy(x => BreakJsonPathIntoComponents(x.JsonPath).Count); + + var needed = new List(); + foreach (var orderedElement in ordered) + { + var match = needed.FirstOrDefault(x => JsonEntityContainedIn(x, orderedElement)); + JsonScalarExpression jsonScalarExpression; + if (match == null) + { + jsonScalarExpression = new JsonScalarExpression( + orderedElement.JsonColumn, + orderedElement.JsonColumn.Type, + orderedElement.JsonColumn.TypeMapping!, + orderedElement.JsonPath, + orderedElement.IsNullable); + + needed.Add(jsonScalarExpression); + } + else + { + jsonScalarExpression = match; + } + + deduplicationMap[orderedElement] = jsonScalarExpression; + } + } + + return deduplicationMap; + } + ConstantExpression AddEntityProjection(EntityProjectionExpression entityProjectionExpression) { var dictionary = new Dictionary(); @@ -1293,6 +1427,85 @@ ConstantExpression AddEntityProjection(EntityProjectionExpression entityProjecti return Constant(dictionary); } + + ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression, JsonScalarExpression jsonScalarToAdd) + { + var additionalPath = new string[0]; + + // this will be more tricky once we support more complicated json path options + additionalPath = BreakJsonPathIntoComponents(jsonQueryExpression.JsonPath) + .Skip(BreakJsonPathIntoComponents(jsonScalarToAdd.JsonPath).Count) + .Select(x => (string)((SqlConstantExpression)x).Value!) + .ToArray(); + + var jsonColumnIndex = AddToProjection(jsonScalarToAdd); + + var keyInfo = new List<(IProperty, int)>(); + var keyProperties = GetMappedKeyProperties(jsonQueryExpression.EntityType.FindPrimaryKey()!); + foreach (var keyProperty in keyProperties) + { + var keyColumn = jsonQueryExpression.BindProperty(keyProperty); + keyInfo.Add((keyProperty, AddToProjection(keyColumn))); + } + + return Constant((jsonColumnIndex, keyInfo, additionalPath)); + } + + static IReadOnlyList GetMappedKeyProperties(IKey key) + { + if (!key.DeclaringEntityType.IsMappedToJson()) + { + return key.Properties; + } + + // TODO: fix this once we enable json entity being owned by another owned non-json entity (issue #28441) + + // for json collections we need to filter out the ordinal key as it's not mapped to any column + // there could be multiple of these in deeply nested structures, + // so we traverse to the outermost owner to see how many mapped keys there are + var currentEntity = key.DeclaringEntityType; + while (currentEntity.IsMappedToJson()) + { + currentEntity = currentEntity.FindOwnership()!.PrincipalEntityType; + } + + var count = currentEntity.FindPrimaryKey()!.Properties.Count; + + return key.Properties.Take(count).ToList(); + } + + static bool JsonEntityContainedIn(JsonScalarExpression sourceExpression, JsonQueryExpression targetExpression) + { + if (sourceExpression.JsonColumn != targetExpression.JsonColumn) + { + return false; + } + + var sourcePath = BreakJsonPathIntoComponents(sourceExpression.JsonPath); + var targetPath = BreakJsonPathIntoComponents(targetExpression.JsonPath); + + if (targetPath.Count < sourcePath.Count) + { + return false; + } + + return sourcePath.SequenceEqual(targetPath.Take(sourcePath.Count)); + } + + static List BreakJsonPathIntoComponents(SqlExpression jsonPath) + { + var result = new List(); + var currentPath = jsonPath; + while (currentPath is SqlBinaryExpression sqlBinary && sqlBinary.OperatorType == ExpressionType.Add) + { + result.Insert(0, sqlBinary.Right); + currentPath = sqlBinary.Left; + } + + result.Insert(0, currentPath); + + return result; + } } /// @@ -1306,7 +1519,8 @@ public void ReplaceProjection(IReadOnlyDictionary { Check.DebugAssert( expression is SqlExpression - || expression is EntityProjectionExpression, + || expression is EntityProjectionExpression + || expression is JsonQueryExpression, "Invalid operation in the projection."); _projectionMapping[projectionMember] = expression; } @@ -1326,7 +1540,8 @@ public void ReplaceProjection(IReadOnlyList clientProjections) Check.DebugAssert( expression is SqlExpression || expression is EntityProjectionExpression - || expression is ShapedQueryExpression, + || expression is ShapedQueryExpression + || expression is JsonQueryExpression, "Invalid operation in the projection."); _clientProjections.Add(expression); _aliasForClientProjections.Add(null); @@ -3038,6 +3253,10 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal() { _clientProjections[i] = LiftEntityProjectionFromSubquery(entityProjection); } + else if (item is JsonQueryExpression jsonQueryExpression) + { + _clientProjections[i] = LiftJsonQueryFromSubquery(jsonQueryExpression); + } else if (item is SqlExpression sqlExpression) { var alias = _aliasForClientProjections[i]; @@ -3066,6 +3285,10 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal() { _projectionMapping[projectionMember] = LiftEntityProjectionFromSubquery(entityProjection); } + else if (expression is JsonQueryExpression jsonQueryExpression) + { + _projectionMapping[projectionMember] = LiftJsonQueryFromSubquery(jsonQueryExpression); + } else { var innerColumn = (SqlExpression)expression; @@ -3195,15 +3418,52 @@ EntityProjectionExpression LiftEntityProjectionFromSubquery(EntityProjectionExpr var boundEntityShaperExpression = entityProjection.BindNavigation(navigation); if (boundEntityShaperExpression != null) { - var innerEntityProjection = (EntityProjectionExpression)boundEntityShaperExpression.ValueBufferExpression; - var newInnerEntityProjection = LiftEntityProjectionFromSubquery(innerEntityProjection); - boundEntityShaperExpression = boundEntityShaperExpression.Update(newInnerEntityProjection); + var newValueBufferExpression = boundEntityShaperExpression.ValueBufferExpression is EntityProjectionExpression innerEntityProjection + ? (Expression)LiftEntityProjectionFromSubquery(innerEntityProjection) + : LiftJsonQueryFromSubquery((JsonQueryExpression)boundEntityShaperExpression.ValueBufferExpression); + + boundEntityShaperExpression = boundEntityShaperExpression.Update(newValueBufferExpression); newEntityProjection.AddNavigationBinding(navigation, boundEntityShaperExpression); } } return newEntityProjection; } + + JsonQueryExpression LiftJsonQueryFromSubquery(JsonQueryExpression jsonQueryExpression) + { + var jsonScalarExpression = new JsonScalarExpression( + jsonQueryExpression.JsonColumn, + jsonQueryExpression.JsonColumn.TypeMapping!.ClrType, + jsonQueryExpression.JsonColumn.TypeMapping, + jsonQueryExpression.JsonPath, + jsonQueryExpression.IsNullable); + + var newJsonColumn = subquery.GenerateOuterColumn(subqueryTableReferenceExpression, jsonScalarExpression); + + var newKeyPropertyMap = new Dictionary(); + + var keyProperties = jsonQueryExpression.EntityType.FindPrimaryKey()!.Properties; + var keyPropertyCount = jsonQueryExpression.IsCollection + ? keyProperties.Count - 1 + : keyProperties.Count; + + for (var i = 0; i < keyPropertyCount; i++) + { + var keyProperty = keyProperties[i]; + var innerColumn = jsonQueryExpression.BindProperty(keyProperty); + var outerColumn = subquery.GenerateOuterColumn(subqueryTableReferenceExpression, innerColumn); + projectionMap[innerColumn] = outerColumn; + newKeyPropertyMap[keyProperty] = outerColumn; + } + + // clear up the json path - we start from empty path after pushdown + return jsonQueryExpression.Update( + newJsonColumn, + newKeyPropertyMap, + jsonPath: new SqlConstantExpression(Expression.Constant("$"), typeMapping: null), + newJsonColumn.IsNullable); + } } /// @@ -3406,6 +3666,11 @@ private static Expression MakeNullable(Expression expression, bool nullable) { return column.MakeNullable(); } + + if (expression is JsonQueryExpression jsonQueryExpression) + { + return jsonQueryExpression.MakeNullable(); + } } return expression; @@ -3422,6 +3687,10 @@ private static IEnumerable GetAllPropertiesInHierarchy(IEntityType en => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) .SelectMany(t => t.GetDeclaredProperties()); + private static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityType) + => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) + .SelectMany(t => t.GetDeclaredNavigations()); + private static ConcreteColumnExpression CreateColumnExpression( IProperty property, ITableBase table, diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 41d004349f1..1eaf3487b90 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -416,6 +416,8 @@ SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression, allowOptimizedExpansion, out nullable), SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression, allowOptimizedExpansion, out nullable), + JsonScalarExpression jsonScalarExpression + => VisitJsonScalar(jsonScalarExpression, allowOptimizedExpansion, out nullable), _ => VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) }; @@ -1151,6 +1153,23 @@ protected virtual SqlExpression VisitSqlUnary( : updated; } + /// + /// Visits a and computes its nullability. + /// + /// A json scalar 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 VisitJsonScalar( + JsonScalarExpression jsonScalarExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + nullable = jsonScalarExpression.IsNullable; + + return jsonScalarExpression; + } + private static bool? TryGetBoolConstantValue(SqlExpression? expression) => expression is SqlConstantExpression constantExpression && constantExpression.Value is bool boolValue diff --git a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs index fc723149ac7..8eb2e9c60a5 100644 --- a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs +++ b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs @@ -110,8 +110,8 @@ private void AddAllDependentsInclusive(IUpdateEntry entry, List en foreach (var foreignKey in foreignKeys) { - var dependentEntry = _updateAdapter.GetDependents(entry, foreignKey).SingleOrDefault(); - if (dependentEntry != null) + var dependentEntries = _updateAdapter.GetDependents(entry, foreignKey); + foreach (var dependentEntry in dependentEntries) { AddAllDependentsInclusive(dependentEntry, entries); } diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 2af9621b351..69744ad6b91 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -321,6 +321,13 @@ private List GenerateColumnModifications() } processedJsonNavigations.Add(navigation); + + // parent entity got deleted, no need to do any json-specific processing + if (currentEntry.EntityState == EntityState.Deleted) + { + continue; + } + var navigationValue = currentEntry.GetCurrentValue(navigation)!; var json = CreateJson( @@ -333,7 +340,7 @@ private List GenerateColumnModifications() var columnModificationParameters = new ColumnModificationParameters( jsonColumnName, originalValue: null, - value: json.ToJsonString(), + value: json?.ToJsonString(), property: null, columnType: jsonColumnTypeMapping.StoreType, jsonColumnTypeMapping, @@ -521,20 +528,22 @@ entry.EntityState is EntityState.Modified or EntityState.Added return columnModifications; } - private JsonNode CreateJson(object? navigationValue, IUpdateEntry parentEntry, IEntityType entityType, int? ordinal, bool isCollection) + private JsonNode? CreateJson(object? navigationValue, IUpdateEntry parentEntry, IEntityType entityType, int? ordinal, bool isCollection) { if (navigationValue == null) { - return new JsonObject(); + return isCollection ? new JsonArray() : null; } if (isCollection) { var i = 1; - var jsonNodes = new List(); + var jsonNodes = new List(); foreach (var collectionElement in (IEnumerable)navigationValue) { - jsonNodes.Add(CreateJson(collectionElement, parentEntry, entityType, i++, isCollection: false)); + // TODO: should we ever expect null entities inside a collection? + var collectionElementJson = CreateJson(collectionElement, parentEntry, entityType, i++, isCollection: false); + jsonNodes.Add(collectionElementJson); } return new JsonArray(jsonNodes.ToArray()); @@ -565,6 +574,12 @@ private JsonNode CreateJson(object? navigationValue, IUpdateEntry parentEntry, I foreach (var navigation in entityType.GetNavigations()) { + // skip back-references to the parent + if (navigation.IsOnDependent) + { + continue; + } + var jsonPropertyName = navigation.TargetEntityType.GetJsonPropertyName()!; var ownedNavigationValue = entry.GetCurrentValue(navigation)!; var navigationJson = CreateJson( diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs index 15f5ffc05ee..52728c8041a 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.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 Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -96,6 +97,16 @@ public override void ProcessEntityTypeAnnotationChanged( /// The store value generation strategy to set for the given property. protected override ValueGenerated? GetValueGenerated(IConventionProperty property) { + // TODO: move to relational? + if (property.DeclaringEntityType.IsMappedToJson() + && !property.DeclaringEntityType.FindOwnership()!.IsUnique +#pragma warning disable EF1001 // Internal EF Core API usage. + && property.IsOrdinalKeyProperty()) +#pragma warning restore EF1001 // Internal EF Core API usage. + { + return ValueGenerated.OnAdd; + } + var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); if (declaringTable.Name == null) { diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index f5ec4af665a..9cdb7842578 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -754,4 +754,20 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) _isSearchCondition = parentSearchCondition; return updateExpression.Update(selectExpression, setColumnValues ?? updateExpression.SetColumnValues); } + + /// + /// 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 VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var jsonPath = (SqlExpression)Visit(jsonScalarExpression.JsonPath); + _isSearchCondition = parentSearchCondition; + + return jsonScalarExpression.Update(jsonScalarExpression.JsonColumn, jsonPath, jsonScalarExpression.IsNullable); + } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 95b67577259..03ef76e406e 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -1,8 +1,10 @@ // 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; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; @@ -291,6 +293,46 @@ when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationTy return base.VisitExtension(extensionExpression); } + /// + protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + { + if (jsonScalarExpression.TypeMapping is SqlServerJsonTypeMapping) + { + Sql.Append("JSON_QUERY("); + } + else + { + Sql.Append("CAST(JSON_VALUE("); + } + + Visit(jsonScalarExpression.JsonColumn); + + var jsonPathStrings = new List(); + + if (jsonScalarExpression.JsonPath != null) + { + var currentPath = jsonScalarExpression.JsonPath; + while (currentPath is SqlBinaryExpression sqlBinary && sqlBinary.OperatorType == ExpressionType.Add) + { + currentPath = sqlBinary.Left; + jsonPathStrings.Insert(0, (string)((SqlConstantExpression)sqlBinary.Right).Value!); + } + + jsonPathStrings.Insert(0, (string)((SqlConstantExpression)currentPath).Value!); + } + + Sql.Append($",'{string.Join(".", jsonPathStrings)}')"); + + if (jsonScalarExpression.Type != typeof(JsonElement)) + { + Sql.Append(" AS "); + Sql.Append(jsonScalarExpression.TypeMapping!.StoreType); + Sql.Append(")"); + } + + return jsonScalarExpression; + } + /// protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) { diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs index ff01293fae2..14bb75babf3 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerJsonTypeMapping.cs @@ -49,22 +49,12 @@ public override MethodInfo GetDataReaderMethod() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Expression CustomizeDataReaderExpression(Expression expression) - { - if (expression is UnaryExpression unary - && unary.NodeType == ExpressionType.Convert) - { - var parse = Expression.Call( + => Expression.MakeMemberAccess( + Expression.Call( _jsonDocumentParseMethod, - Expression.Convert( - unary.Operand, - typeof(string)), - Expression.Default(typeof(JsonDocumentOptions))); - - return Expression.MakeMemberAccess(parse, _jsonDocumentRootElementMember); - } - - return base.CustomizeDataReaderExpression(expression); - } + expression, + Expression.Default(typeof(JsonDocumentOptions))), + _jsonDocumentRootElementMember); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs b/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs index 887b60cb503..ac0a5d43f00 100644 --- a/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs +++ b/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs @@ -68,7 +68,6 @@ public ValueGenerationManager( public virtual void Generate(InternalEntityEntry entry, bool includePrimaryKey = true) { var entityEntry = new EntityEntry(entry); - foreach (var property in entry.EntityType.GetValueGeneratingProperties()) { if (!entry.HasDefaultValue(property) 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/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs new file mode 100644 index 00000000000..d2f70697373 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs @@ -0,0 +1,390 @@ +// 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.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class JsonQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase +{ + private JsonQueryData _expectedData; + + public Func GetContextCreator() + => () => CreateContext(); + + public virtual ISetSource GetExpectedData() + { + if (_expectedData == null) + { + _expectedData = new JsonQueryData(); + } + + return _expectedData; + } + + public IReadOnlyDictionary EntitySorters { get; } = new Dictionary> + { + { typeof(JsonEntityBasic), e => ((JsonEntityBasic)e)?.Id }, + { typeof(JsonEntityCustomNaming), e => ((JsonEntityCustomNaming)e)?.Id }, + { typeof(JsonEntitySingleOwned), e => ((JsonEntitySingleOwned)e)?.Id }, + { typeof(JsonEntityInheritanceBase), e => ((JsonEntityInheritanceBase)e)?.Id }, + { typeof(JsonEntityInheritanceDerived), e => ((JsonEntityInheritanceDerived)e)?.Id }, + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary EntityAsserters { get; } = new Dictionary> + { + { + typeof(JsonEntityBasic), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntityBasic)e; + var aa = (JsonEntityBasic)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + + AssertOwnedRoot(ee.OwnedReferenceRoot, aa.OwnedReferenceRoot); + + Assert.Equal(ee.OwnedCollectionRoot.Count, aa.OwnedCollectionRoot.Count); + for (var i = 0; i < ee.OwnedCollectionRoot.Count; i++) + { + AssertOwnedRoot(ee.OwnedCollectionRoot[i], aa.OwnedCollectionRoot[i]); + } + } + } + }, + { + typeof(JsonOwnedRoot), (e, a) => + { + if (a != null) + { + var ee = (JsonOwnedRoot)e; + var aa = (JsonOwnedRoot)a; + + AssertOwnedRoot(ee, aa); + } + } + }, + { + typeof(JsonOwnedBranch), (e, a) => + { + if (a != null) + { + var ee = (JsonOwnedBranch)e; + var aa = (JsonOwnedBranch)a; + + AssertOwnedBranch(ee, aa); + } + } + }, + { + typeof(JsonOwnedLeaf), (e, a) => + { + if (a != null) + { + var ee = (JsonOwnedLeaf)e; + var aa = (JsonOwnedLeaf)a; + + AssertOwnedLeaf(ee, aa); + } + } + }, + { + typeof(JsonEntityCustomNaming), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntityCustomNaming)e; + var aa = (JsonEntityCustomNaming)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Title, aa.Title); + + AssertCustomNameRoot(ee.OwnedReferenceRoot, aa.OwnedReferenceRoot); + + Assert.Equal(ee.OwnedCollectionRoot.Count, aa.OwnedCollectionRoot.Count); + for (var i = 0; i < ee.OwnedCollectionRoot.Count; i++) + { + AssertCustomNameRoot(ee.OwnedCollectionRoot[i], aa.OwnedCollectionRoot[i]); + } + } + } + }, + { + typeof( JsonOwnedCustomNameRoot), (e, a) => + { + if (a != null) + { + var ee = (JsonOwnedCustomNameRoot)e; + var aa = (JsonOwnedCustomNameRoot)a; + + AssertCustomNameRoot(ee, aa); + } + } + }, + { + typeof(JsonOwnedCustomNameBranch), (e, a) => + { + if (a != null) + { + var ee = (JsonOwnedCustomNameBranch)e; + var aa = (JsonOwnedCustomNameBranch)a; + + AssertCustomNameBranch(ee, aa); + } + } + }, + { + typeof(JsonEntitySingleOwned), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntitySingleOwned)e; + var aa = (JsonEntitySingleOwned)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + + Assert.Equal(ee.OwnedCollection?.Count ?? 0, aa.OwnedCollection?.Count ?? 0); + for (var i = 0; i < ee.OwnedCollection.Count; i++) + { + AssertOwnedLeaf(ee.OwnedCollection[i], aa.OwnedCollection[i]); + } + } + } + }, + { + typeof(JsonEntityInheritanceBase), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntityInheritanceBase)e; + var aa = (JsonEntityInheritanceBase)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + + AssertOwnedBranch(ee.ReferenceOnBase, aa.ReferenceOnBase); + Assert.Equal(ee.CollectionOnBase?.Count ?? 0, aa.CollectionOnBase?.Count ?? 0); + for (var i = 0; i < ee.CollectionOnBase.Count; i++) + { + AssertOwnedBranch(ee.CollectionOnBase[i], aa.CollectionOnBase[i]); + } + } + } + }, + { + typeof(JsonEntityInheritanceDerived), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntityInheritanceDerived)e; + var aa = (JsonEntityInheritanceDerived)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.Fraction, aa.Fraction); + + AssertOwnedBranch(ee.ReferenceOnBase, aa.ReferenceOnBase); + AssertOwnedBranch(ee.ReferenceOnDerived, aa.ReferenceOnDerived); + + Assert.Equal(ee.CollectionOnBase?.Count ?? 0, aa.CollectionOnBase?.Count ?? 0); + for (var i = 0; i < ee.CollectionOnBase.Count; i++) + { + AssertOwnedBranch(ee.CollectionOnBase[i], aa.CollectionOnBase[i]); + } + + Assert.Equal(ee.CollectionOnDerived?.Count ?? 0, aa.CollectionOnDerived?.Count ?? 0); + for (var i = 0; i < ee.CollectionOnDerived.Count; i++) + { + AssertOwnedBranch(ee.CollectionOnDerived[i], aa.CollectionOnDerived[i]); + } + } + } + }, + }.ToDictionary(e => e.Key, e => (object)e.Value); + + private static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual) + { + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.Number, actual.Number); + + AssertOwnedBranch(expected.OwnedReferenceBranch, actual.OwnedReferenceBranch); + Assert.Equal(expected.OwnedCollectionBranch.Count, actual.OwnedCollectionBranch.Count); + for (var i = 0; i < expected.OwnedCollectionBranch.Count; i++) + { + AssertOwnedBranch(expected.OwnedCollectionBranch[i], actual.OwnedCollectionBranch[i]); + } + } + + private static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch actual) + { + Assert.Equal(expected.Date, actual.Date); + Assert.Equal(expected.Fraction, actual.Fraction); + Assert.Equal(expected.Enum, actual.Enum); + + AssertOwnedLeaf(expected.OwnedReferenceLeaf, actual.OwnedReferenceLeaf); + Assert.Equal(expected.OwnedCollectionLeaf.Count, actual.OwnedCollectionLeaf.Count); + for (var i = 0; i < expected.OwnedCollectionLeaf.Count; i++) + { + AssertOwnedLeaf(expected.OwnedCollectionLeaf[i], actual.OwnedCollectionLeaf[i]); + } + } + + private static void AssertOwnedLeaf(JsonOwnedLeaf expected, JsonOwnedLeaf actual) + { + Assert.Equal(expected.SomethingSomething, actual.SomethingSomething); + } + + public static void AssertCustomNameRoot(JsonOwnedCustomNameRoot expected, JsonOwnedCustomNameRoot actual) + { + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.Number, actual.Number); + Assert.Equal(expected.Enum, actual.Enum); + AssertCustomNameBranch(expected.OwnedReferenceBranch, actual.OwnedReferenceBranch); + Assert.Equal(expected.OwnedCollectionBranch.Count, actual.OwnedCollectionBranch.Count); + for (var i = 0; i < expected.OwnedCollectionBranch.Count; i++) + { + AssertCustomNameBranch(expected.OwnedCollectionBranch[i], actual.OwnedCollectionBranch[i]); + } + } + + public static void AssertCustomNameBranch(JsonOwnedCustomNameBranch expected, JsonOwnedCustomNameBranch actual) + { + Assert.Equal(expected.Date, actual.Date); + Assert.Equal(expected.Fraction, actual.Fraction); + } + + protected override string StoreName { get; } = "JsonQueryTest"; + + public new RelationalTestStore TestStore + => (RelationalTestStore)base.TestStore; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public override JsonQueryContext CreateContext() + { + var context = base.CreateContext(); + + return context; + } + + protected override void Seed(JsonQueryContext context) + => JsonQueryContext.Seed(context); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.OwnedReferenceRoot, b => + { + b.ToJson(); + b.WithOwner(x => x.Owner); + + b.OwnsOne(x => x.OwnedReferenceBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); + bb.Navigation(x => x.OwnedReferenceLeaf).IsRequired(); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + }); + + b.OwnsMany(x => x.OwnedCollectionBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + }); + }); + + modelBuilder.Entity().Navigation(x => x.OwnedReferenceRoot).IsRequired(); + + modelBuilder.Entity().OwnsMany(x => x.OwnedCollectionRoot, b => + { + b.OwnsOne(x => x.OwnedReferenceBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + }); + + b.OwnsMany(x => x.OwnedCollectionBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + }); + b.ToJson(); + }); + + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.OwnedReferenceRoot, b => + { + b.Property(x => x.Enum).HasConversion(); + b.OwnsOne(x => x.OwnedReferenceBranch); + b.OwnsMany(x => x.OwnedCollectionBranch); + b.ToJson("json_reference_custom_naming"); + }); + + modelBuilder.Entity().OwnsMany(x => x.OwnedCollectionRoot, b => + { + b.ToJson("json_collection_custom_naming"); + b.Property(x => x.Enum).HasConversion(); + b.OwnsOne(x => x.OwnedReferenceBranch); + b.OwnsMany(x => x.OwnedCollectionBranch); + }); + + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsMany(x => x.OwnedCollection, b => + { + b.ToJson(); + b.Ignore(x => x.Parent); + }); + + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity(b => + { + b.OwnsOne(x => x.ReferenceOnBase, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + + b.OwnsMany(x => x.CollectionOnBase, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + }); + + modelBuilder.Entity(b => + { + b.HasBaseType(); + b.OwnsOne(x => x.ReferenceOnDerived, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + + b.OwnsMany(x => x.CollectionOnDerived, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + }); + } +} 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..0bac3fb5370 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs @@ -0,0 +1,638 @@ +// 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.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class JsonQueryTestBase : QueryTestBase + where TFixture : JsonQueryFixtureBase, new() +{ + protected JsonQueryTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owner_entity(bool async) + => AssertQuery( + async, + ss => ss.Set(), + entryCount: 40); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_root(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_duplicated(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => new + { + Root1 = x.OwnedReferenceRoot, + Branch1 = x.OwnedReferenceRoot.OwnedReferenceBranch, + Root2 = x.OwnedReferenceRoot, + Branch2 = x.OwnedReferenceRoot.OwnedReferenceBranch, + }).AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertEqual(e.Root1, a.Root1); + AssertEqual(e.Root2, a.Root2); + AssertEqual(e.Branch1, a.Branch1); + AssertEqual(e.Branch2, a.Branch2); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_collection_root(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot).AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_branch(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_collection_branch(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedCollectionBranch).AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_leaf(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_collection_leaf(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf).AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_scalar(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.Name)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_scalar_length(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.OwnedReferenceRoot.Name.Length > 2).Select(x => x.Name)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_enum_inside_json_entity(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.Id, + Enum = x.OwnedReferenceRoot.OwnedReferenceBranch.Enum, + }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.Enum, a.Enum); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_enum_with_custom_conversion(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.Id, + Enum = x.OwnedReferenceRoot.Enum, + }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.Enum, a.Enum); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_with_deduplication(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x, + x.OwnedReferenceRoot.OwnedReferenceBranch, + x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf, + x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf, + x.OwnedReferenceRoot.OwnedCollectionBranch, + x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething + }), + elementAsserter: (e, a) => + { + AssertEqual(e.OwnedReferenceBranch, a.OwnedReferenceBranch); + AssertEqual(e.OwnedReferenceLeaf, a.OwnedReferenceLeaf); + AssertCollection(e.OwnedCollectionLeaf, a.OwnedCollectionLeaf, ordered: true); + AssertCollection(e.OwnedCollectionBranch, a.OwnedCollectionBranch, ordered: true); + Assert.Equal(e.SomethingSomething, a.SomethingSomething); + }, + entryCount: 40); + + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_with_deduplication_reverse_order(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf, + x.OwnedReferenceRoot, + x + }).AsNoTracking(), + elementAsserter: (e, a) => + { + AssertEqual(e.OwnedReferenceLeaf, a.OwnedReferenceLeaf); + AssertEqual(e.OwnedReferenceRoot, a.OwnedReferenceRoot); + AssertEqual(e.x, a.x); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_property_in_predicate(bool async) + => AssertQueryScalar( + async, + ss => ss.Set() + .Where(x => x.OwnedReferenceRoot.OwnedReferenceBranch.Fraction < 20.5M).Select(x => x.Id)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_property_pushdown_length(bool async) + => AssertQueryScalar( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething) + .Take(3) + .Distinct() + .Select(x => x.Length)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_reference_pushdown_reference(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => x.OwnedReferenceRoot) + .Take(10) + .Distinct() + .Select(x => x.OwnedReferenceBranch).AsNoTracking()); + + [ConditionalTheory(Skip = "issue #24263")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_reference_pushdown_reference_anonymous_projection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => new { Entity = x.OwnedReferenceRoot, Scalar = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething }) + .Take(10) + .Distinct() + .Select(x => new { x.Entity.OwnedReferenceBranch, x.Scalar.Length }).AsNoTracking(), + elementSorter: e => (e.OwnedReferenceBranch.Date, e.OwnedReferenceBranch.Fraction, e.Length), + elementAsserter: (e, a) => + { + AssertEqual(e.OwnedReferenceBranch, a.OwnedReferenceBranch); + Assert.Equal(e.Length, a.Length); + }); + + [ConditionalTheory(Skip = "issue #24263")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => new { Root = x.OwnedReferenceRoot, Scalar = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething }) + .Take(10) + .Distinct() + .Select(x => new { Branch = x.Root.OwnedReferenceBranch, x.Scalar.Length }) + .OrderBy(x => x.Length) + .Take(10) + .Distinct() + .Select(x => new { x.Branch.OwnedReferenceLeaf, x.Branch.OwnedCollectionLeaf, x.Length }) + .AsNoTracking(), + elementSorter: e => (e.OwnedReferenceLeaf.SomethingSomething, e.OwnedCollectionLeaf.Count, e.Length), + elementAsserter: (e, a) => + { + AssertEqual(e.OwnedReferenceLeaf, a.OwnedReferenceLeaf); + AssertCollection(e.OwnedCollectionLeaf, e.OwnedCollectionLeaf, ordered: true); + Assert.Equal(e.Length, a.Length); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_reference_pushdown_reference_pushdown_reference(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => x.OwnedReferenceRoot) + .Take(10) + .Distinct() + .OrderBy(x => x.Name) + .Select(x => x.OwnedReferenceBranch) + .Take(10) + .Distinct() + .Select(x => x.OwnedReferenceLeaf).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_reference_pushdown_reference_pushdown_collection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => x.OwnedReferenceRoot) + .Take(10) + .Distinct() + .OrderBy(x => x.Name) + .Select(x => x.OwnedReferenceBranch) + .Take(10) + .Distinct() + .Select(x => x.OwnedCollectionLeaf).AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_subquery_reference_pushdown_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf) + .Take(10) + .Distinct() + .Select(x => x.SomethingSomething)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Custom_naming_projection_owner_entity(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x), + entryCount: 13); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Custom_naming_projection_owned_reference(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Custom_naming_projection_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.OwnedCollectionRoot).AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Custom_naming_projection_owned_scalar(bool async) + => AssertQueryScalar( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch.Fraction)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Custom_naming_projection_everything(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + root = x, + referece = x.OwnedReferenceRoot, + nested_reference = x.OwnedReferenceRoot.OwnedReferenceBranch, + collection = x.OwnedCollectionRoot, + nested_collection = x.OwnedReferenceRoot.OwnedCollectionBranch, + scalar = x.OwnedReferenceRoot.Name, + nested_scalar = x.OwnedReferenceRoot.OwnedReferenceBranch.Fraction, + }), + elementSorter: e => e.root.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.root, a.root); + AssertEqual(e.referece, a.referece); + AssertEqual(e.nested_reference, a.nested_reference); + AssertCollection(e.collection, a.collection, ordered: true); + AssertCollection(e.nested_collection, a.nested_collection, ordered: true); + Assert.Equal(e.scalar, a.scalar); + Assert.Equal(e.nested_scalar, a.nested_scalar); + }, + entryCount: 13); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_entity_with_single_owned(bool async) + => AssertQuery( + async, + ss => ss.Set(), + entryCount: 8); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Left_join_json_entities(bool async) + => AssertQuery( + async, + ss => from e1 in ss.Set() + join e2 in ss.Set() on e1.Id equals e2.Id into g + from e2 in g.DefaultIfEmpty() + select new { e1, e2 }, + elementSorter: e => (e.e1.Id, e.e2?.Id), + elementAsserter: (e, a) => + { + AssertEqual(e.e1, a.e1); + AssertEqual(e.e2, a.e2); + }, + entryCount: 48); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Left_join_json_entities_complex_projection(bool async) + => AssertQuery( + async, + ss => (from e1 in ss.Set() + join e2 in ss.Set() on e1.Id equals e2.Id into g + from e2 in g.DefaultIfEmpty() + select new + { + Id1 = e1.Id, + Id2 = (int?)e2.Id, + e2, + e2.OwnedReferenceRoot, + e2.OwnedReferenceRoot.OwnedReferenceBranch, + e2.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf, + e2.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf + }), + elementSorter: e => (e.Id1, e?.Id2), + elementAsserter: (e, a) => + { + Assert.Equal(e.Id1, a.Id1); + Assert.Equal(e.Id2, a.Id2); + AssertEqual(e.e2, a.e2); + AssertEqual(e.OwnedReferenceRoot, a.OwnedReferenceRoot); + AssertEqual(e.OwnedReferenceBranch, a.OwnedReferenceBranch); + AssertEqual(e.OwnedReferenceLeaf, a.OwnedReferenceLeaf); + AssertCollection(e.OwnedCollectionLeaf, a.OwnedCollectionLeaf, ordered: true); + }, + entryCount: 40); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_json_entity_FirstOrDefault_subquery(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => xx.OwnedReferenceRoot) + .FirstOrDefault().OwnedReferenceBranch) + .AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(bool async) + => AssertQueryScalar( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => xx.OwnedReferenceRoot) + .FirstOrDefault().OwnedReferenceBranch.Date)); + + [ConditionalTheory(Skip = "issue #28733")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(bool async) + => AssertQueryScalar( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => xx.OwnedReferenceRoot) + .FirstOrDefault().OwnedReferenceBranch == ss.Set() + .OrderByDescending(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => xx.OwnedReferenceRoot) + .FirstOrDefault().OwnedReferenceBranch))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_json_entity_FirstOrDefault_subquery_deduplication(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => new + { + x.OwnedReferenceRoot.OwnedCollectionBranch, + xx.OwnedReferenceRoot, + xx.OwnedReferenceRoot.OwnedReferenceBranch, + xx.OwnedReferenceRoot.Name, + x.OwnedReferenceRoot.OwnedReferenceBranch.Enum + }).FirstOrDefault()).AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertEqual(e.OwnedReferenceRoot, a.OwnedReferenceRoot); + AssertEqual(e.OwnedReferenceBranch, a.OwnedReferenceBranch); + AssertEqual(e.Name, a.Name); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_json_entity_FirstOrDefault_subquery_deduplication_and_outer_reference(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => new + { + x.OwnedReferenceRoot.OwnedCollectionBranch, + xx.OwnedReferenceRoot, + xx.OwnedReferenceRoot.OwnedReferenceBranch, + xx.OwnedReferenceRoot.Name, + x.OwnedReferenceRoot.OwnedReferenceBranch.Enum + }).FirstOrDefault()).AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertCollection(e.OwnedCollectionBranch, a.OwnedCollectionBranch, ordered: true); + AssertEqual(e.OwnedReferenceRoot, a.OwnedReferenceRoot); + AssertEqual(e.OwnedReferenceBranch, a.OwnedReferenceBranch); + AssertEqual(e.Name, a.Name); + AssertEqual(e.Enum, a.Enum); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_json_entity_FirstOrDefault_subquery_deduplication_outer_reference_and_pruning(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => ss.Set() + .OrderBy(xx => xx.Id) + .Select(xx => new + { + x.OwnedReferenceRoot.OwnedCollectionBranch, + xx.OwnedReferenceRoot, + xx.OwnedReferenceRoot.OwnedReferenceBranch, + xx.OwnedReferenceRoot.Name, + x.OwnedReferenceRoot.OwnedReferenceBranch.Enum + }).FirstOrDefault().OwnedCollectionBranch).AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_entity_with_inheritance_basic_projection(bool async) + => AssertQuery( + async, + ss => ss.Set(), + entryCount: 38); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_entity_with_inheritance_project_derived(bool async) + => AssertQuery( + async, + ss => ss.Set().OfType(), + entryCount: 25); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_entity_with_inheritance_project_navigations(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.Id, + x.ReferenceOnBase, + x.CollectionOnBase + }).AsNoTracking(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertEqual(e.ReferenceOnBase, a.ReferenceOnBase); + AssertCollection(e.CollectionOnBase, a.CollectionOnBase, ordered: true); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_entity_with_inheritance_project_navigations_on_derived(bool async) + => AssertQuery( + async, + ss => ss.Set().OfType().Select(x => new + { + x, + x.ReferenceOnBase, + x.ReferenceOnDerived, + x.CollectionOnBase, + x.CollectionOnDerived + }), + elementSorter: e => e.x.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.x, a.x); + AssertEqual(e.ReferenceOnBase, a.ReferenceOnBase); + AssertEqual(e.ReferenceOnDerived, a.ReferenceOnDerived); + AssertCollection(e.CollectionOnBase, a.CollectionOnBase, ordered: true); + AssertCollection(e.CollectionOnDerived, a.CollectionOnDerived, ordered: true); + }, + entryCount: 25); + + [ConditionalTheory(Skip = "issue #28645")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_entity_backtracking(bool async) + => AssertQueryScalar( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.Parent.Date)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_basic(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[0]).AsNoTracking()); + + [ConditionalTheory(Skip = "issue #28648")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate(bool async) + => AssertQueryScalar( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot[0].Name != "Foo").Select(x => x.Id)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_scalar_required_null_semantics(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => x.OwnedReferenceRoot.Number != x.OwnedReferenceRoot.Name.Length) + .Select(x => x.Name)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_scalar_optional_null_semantics(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => x.OwnedReferenceRoot.Name == x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething) + .Select(x => x.Name)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Group_by_on_json_scalar(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(x => x.OwnedReferenceRoot.Name).Select(x => new { x.Key, Count = x.Count() })); + + +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityBasic.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityBasic.cs new file mode 100644 index 00000000000..1035a285d25 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityBasic.cs @@ -0,0 +1,14 @@ +// 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.JsonQuery +{ + public class JsonEntityBasic + { + public int Id { get; set; } + public string Name { get; set; } + + public JsonOwnedRoot OwnedReferenceRoot { get; set; } + public List OwnedCollectionRoot { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityCustomNaming.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityCustomNaming.cs new file mode 100644 index 00000000000..9cc28b9a183 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityCustomNaming.cs @@ -0,0 +1,20 @@ +// 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.Serialization; + +namespace Microsoft.EntityFrameworkCore.TestModels.JsonQuery +{ + public class JsonEntityCustomNaming + { + public int Id { get; set; } + + public string Title { get; set; } + + [JsonPropertyName("CustomOwnedReferenceRoot")] + public JsonOwnedCustomNameRoot OwnedReferenceRoot { get; set; } + + [JsonPropertyName("CustomOwnedCollectionRoot")] + public List OwnedCollectionRoot { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityInheritanceBase.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityInheritanceBase.cs new file mode 100644 index 00000000000..a5a43432c40 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityInheritanceBase.cs @@ -0,0 +1,14 @@ +// 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.JsonQuery +{ + public class JsonEntityInheritanceBase + { + public int Id { get; set; } + public string Name { get; set; } + + public JsonOwnedBranch ReferenceOnBase { get; set; } + public List CollectionOnBase { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityInheritanceDerived.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityInheritanceDerived.cs new file mode 100644 index 00000000000..a377486e95f --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntityInheritanceDerived.cs @@ -0,0 +1,12 @@ +// 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.JsonQuery +{ + public class JsonEntityInheritanceDerived : JsonEntityInheritanceBase + { + public double Fraction { get; set; } + public JsonOwnedBranch ReferenceOnDerived { get; set; } + public List CollectionOnDerived { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntitySingleOwned.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntitySingleOwned.cs new file mode 100644 index 00000000000..ec266b4498d --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEntitySingleOwned.cs @@ -0,0 +1,13 @@ +// 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.JsonQuery +{ + public class JsonEntitySingleOwned + { + public int Id { get; set; } + public string Name { get; set; } + + public List OwnedCollection { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEnum.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEnum.cs new file mode 100644 index 00000000000..ebca48e86ed --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonEnum.cs @@ -0,0 +1,12 @@ +// 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.JsonQuery +{ + public enum JsonEnum + { + One, + Two, + Three + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedBranch.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedBranch.cs new file mode 100644 index 00000000000..93880c8a1e7 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedBranch.cs @@ -0,0 +1,16 @@ +// 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.JsonQuery +{ + public class JsonOwnedBranch + { + public DateTime Date { get; set; } + public decimal Fraction { get; set; } + + public JsonEnum Enum { get; set; } + + public JsonOwnedLeaf OwnedReferenceLeaf { get; set; } + public List OwnedCollectionLeaf { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedCustomNameBranch.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedCustomNameBranch.cs new file mode 100644 index 00000000000..5cffa045bda --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedCustomNameBranch.cs @@ -0,0 +1,16 @@ +// 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.Serialization; + +namespace Microsoft.EntityFrameworkCore.TestModels.JsonQuery +{ + public class JsonOwnedCustomNameBranch + { + [JsonPropertyName("CustomDate")] + public DateTime Date { get; set; } + + [JsonPropertyName("CustomFraction")] + public double Fraction { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedCustomNameRoot.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedCustomNameRoot.cs new file mode 100644 index 00000000000..ba294c7f4c0 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedCustomNameRoot.cs @@ -0,0 +1,25 @@ +// 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.Serialization; + +namespace Microsoft.EntityFrameworkCore.TestModels.JsonQuery +{ + public class JsonOwnedCustomNameRoot + { + [JsonPropertyName("CustomName")] + public string Name { get; set; } + + [JsonPropertyName("CustomNumber")] + public int Number { get; set; } + + [JsonPropertyName("CustomEnum")] + public JsonEnum Enum { get; set; } + + [JsonPropertyName("CustomOwnedReferenceBranch")] + public JsonOwnedCustomNameBranch OwnedReferenceBranch { get; set; } + + [JsonPropertyName("CustomOwnedCollectionBranch")] + public List OwnedCollectionBranch { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedLeaf.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedLeaf.cs new file mode 100644 index 00000000000..5d973b25fec --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedLeaf.cs @@ -0,0 +1,12 @@ +// 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.JsonQuery +{ + public class JsonOwnedLeaf + { + public string SomethingSomething { get; set; } + + public JsonOwnedBranch Parent { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedRoot.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedRoot.cs new file mode 100644 index 00000000000..2c3f2ab257d --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonOwnedRoot.cs @@ -0,0 +1,16 @@ +// 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.JsonQuery +{ + public class JsonOwnedRoot + { + public string Name { get; set; } + public int Number { get; set; } + + public JsonOwnedBranch OwnedReferenceBranch { get; set; } + public List OwnedCollectionBranch { get; set; } + + public JsonEntityBasic Owner { get; set; } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs new file mode 100644 index 00000000000..fdaa0fdbe62 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs @@ -0,0 +1,32 @@ +// 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.JsonQuery +{ + public class JsonQueryContext : DbContext + { + public JsonQueryContext(DbContextOptions options) + : base(options) + { + } + + public DbSet JsonEntitiesBasic { get; set; } + public DbSet JsonEntitiesCustomNaming { get; set; } + public DbSet JsonEntitiesSingleOwned { get; set; } + public DbSet JsonEntitiesInheritance { get; set; } + + public static void Seed(JsonQueryContext context) + { + var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic(); + var jsonEntitiesCustomNaming = JsonQueryData.CreateJsonEntitiesCustomNaming(); + var jsonEntitiesSingleOwned = JsonQueryData.CreateJsonEntitiesSingleOwned(); + var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); + + context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic); + context.JsonEntitiesCustomNaming.AddRange(jsonEntitiesCustomNaming); + context.JsonEntitiesSingleOwned.AddRange(jsonEntitiesSingleOwned); + context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); + context.SaveChanges(); + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs new file mode 100644 index 00000000000..423081e7b7b --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs @@ -0,0 +1,691 @@ +// 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.JsonQuery; + +public class JsonQueryData : ISetSource +{ + public JsonQueryData() + { + JsonEntitiesBasic = CreateJsonEntitiesBasic(); + JsonEntitiesCustomNaming = CreateJsonEntitiesCustomNaming(); + JsonEntitiesSingleOwned = CreateJsonEntitiesSingleOwned(); + JsonEntitiesInheritance = CreateJsonEntitiesInheritance(); + } + + public IReadOnlyList JsonEntitiesBasic { get; } + public IReadOnlyList JsonEntitiesCustomNaming { get; set; } + public IReadOnlyList JsonEntitiesSingleOwned { get; set; } + public IReadOnlyList JsonEntitiesInheritance { get; set; } + + public static IReadOnlyList CreateJsonEntitiesBasic() + { + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_r_r_r = new JsonOwnedLeaf { SomethingSomething = "e1_r_r_r" }; + var e1_r_r_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_r_r_c1" }; + var e1_r_r_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_r_r_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_r_r = new JsonOwnedBranch + { + Date = new DateTime(2100, 1, 1), + Fraction = 10.0M, + Enum = JsonEnum.One, + OwnedReferenceLeaf = e1_r_r_r, + OwnedCollectionLeaf = new List { e1_r_r_c1, e1_r_r_c2 } + }; + + e1_r_r_r.Parent = e1_r_r; + e1_r_r_c1.Parent = e1_r_r; + e1_r_r_c2.Parent = e1_r_r; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_r_c1_r = new JsonOwnedLeaf { SomethingSomething = "e1_r_c1_r" }; + var e1_r_c1_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_r_c1_c1" }; + var e1_r_c1_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_r_c1_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_r_c1 = new JsonOwnedBranch + { + Date = new DateTime(2101, 1, 1), + Fraction = 10.1M, + Enum = JsonEnum.Two, + OwnedReferenceLeaf = e1_r_c1_r, + OwnedCollectionLeaf = new List { e1_r_c1_c1, e1_r_c1_c2 } + }; + + e1_r_c1_r.Parent = e1_r_c1; + e1_r_c1_c1.Parent = e1_r_c1; + e1_r_c1_c2.Parent = e1_r_c1; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_r_c2_r = new JsonOwnedLeaf { SomethingSomething = "e1_r_c2_r" }; + var e1_r_c2_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_r_c2_c1" }; + var e1_r_c2_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_r_c2_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_r_c2 = new JsonOwnedBranch + { + Date = new DateTime(2102, 1, 1), + Fraction = 10.2M, + Enum = JsonEnum.Three, + OwnedReferenceLeaf = e1_r_c2_r, + OwnedCollectionLeaf = new List { e1_r_c2_c1, e1_r_c2_c2 } + }; + + e1_r_c2_r.Parent = e1_r_c2; + e1_r_c2_c1.Parent = e1_r_c2; + e1_r_c2_c2.Parent = e1_r_c2; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_r = new JsonOwnedRoot + { + Name = "e1_r", + Number = 10, + OwnedReferenceBranch = e1_r_r, + OwnedCollectionBranch = new List { e1_r_c1, e1_r_c2 } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c1_r_r = new JsonOwnedLeaf { SomethingSomething = "e1_c1_r_r" }; + var e1_c1_r_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_c1_r_c1" }; + var e1_c1_r_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_c1_r_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_c1_r = new JsonOwnedBranch + { + Date = new DateTime(2110, 1, 1), + Fraction = 11.0M, + Enum = JsonEnum.One, + OwnedReferenceLeaf = e1_c1_r_r, + OwnedCollectionLeaf = new List { e1_c1_r_c1, e1_c1_r_c2 } + }; + + e1_c1_r_r.Parent = e1_c1_r; + e1_c1_r_c1.Parent = e1_c1_r; + e1_c1_r_c2.Parent = e1_c1_r; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c1_c1_r = new JsonOwnedLeaf { SomethingSomething = "e1_c1_c1_r" }; + var e1_c1_c1_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_c1_c1_c1" }; + var e1_c1_c1_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_c1_c1_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_c1_c1 = new JsonOwnedBranch + { + Date = new DateTime(2111, 1, 1), + Fraction = 11.1M, + Enum = JsonEnum.Two, + OwnedReferenceLeaf = e1_c1_c1_r, + OwnedCollectionLeaf = new List { e1_c1_c1_c1, e1_c1_c1_c2 } + }; + + e1_c1_c1_r.Parent = e1_c1_c1; + e1_c1_c1_c1.Parent = e1_c1_c1; + e1_c1_c1_c2.Parent = e1_c1_c1; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c1_c2_r = new JsonOwnedLeaf { SomethingSomething = "e1_c1_c2_r" }; + var e1_c1_c2_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_c1_c2_c1" }; + var e1_c1_c2_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_c1_c2_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_c1_c2 = new JsonOwnedBranch + { + Date = new DateTime(2112, 1, 1), + Fraction = 11.2M, + Enum = JsonEnum.Three, + OwnedReferenceLeaf = e1_c1_c2_r, + OwnedCollectionLeaf = new List { e1_c1_c2_c1, e1_c1_c2_c2 } + }; + + e1_c1_c2_r.Parent = e1_c1_c2; + e1_c1_c2_c1.Parent = e1_c1_c2; + e1_c1_c2_c2.Parent = e1_c1_c2; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c1 = new JsonOwnedRoot + { + Name = "e1_c1", + Number = 11, + OwnedReferenceBranch = e1_c1_r, + OwnedCollectionBranch = new List { e1_c1_c1, e1_c1_c2 } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + + var e1_c2_r_r = new JsonOwnedLeaf { SomethingSomething = "e1_c2_r_r" }; + var e1_c2_r_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_c2_r_c1" }; + var e1_c2_r_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_c2_r_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_c2_r = new JsonOwnedBranch + { + Date = new DateTime(2120, 1, 1), + Fraction = 12.0M, + Enum = JsonEnum.Three, + OwnedReferenceLeaf = e1_c2_r_r, + OwnedCollectionLeaf = new List { e1_c2_r_c1, e1_c2_r_c2 } + }; + + e1_c2_r_r.Parent = e1_c2_r; + e1_c2_r_c1.Parent = e1_c2_r; + e1_c2_r_c2.Parent = e1_c2_r; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c2_c1_r = new JsonOwnedLeaf { SomethingSomething = "e1_c2_c1_r" }; + var e1_c2_c1_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_c2_c1_c1" }; + var e1_c2_c1_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_c2_c1_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_c2_c1 = new JsonOwnedBranch + { + Date = new DateTime(2121, 1, 1), + Fraction = 12.1M, + Enum = JsonEnum.Two, + OwnedReferenceLeaf = e1_c2_c1_r, + OwnedCollectionLeaf = new List { e1_c2_c1_c1, e1_c2_c1_c2 } + }; + + e1_c2_c1_r.Parent = e1_c2_c1; + e1_c2_c1_c1.Parent = e1_c2_c1; + e1_c2_c1_c2.Parent = e1_c2_c1; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c2_c2_r = new JsonOwnedLeaf { SomethingSomething = "e1_c2_c2_r" }; + var e1_c2_c2_c1 = new JsonOwnedLeaf { SomethingSomething = "e1_c2_c2_c1" }; + var e1_c2_c2_c2 = new JsonOwnedLeaf { SomethingSomething = "e1_c2_c2_c2" }; + + //------------------------------------------------------------------------------------------- + + var e1_c2_c2 = new JsonOwnedBranch + { + Date = new DateTime(2122, 1, 1), + Fraction = 12.2M, + Enum = JsonEnum.One, + OwnedReferenceLeaf = e1_c2_c2_r, + OwnedCollectionLeaf = new List { e1_c2_c2_c1, e1_c2_c2_c2 } + }; + + e1_c2_c2_r.Parent = e1_c2_c2; + e1_c2_c2_c1.Parent = e1_c2_c2; + e1_c2_c2_c2.Parent = e1_c2_c2; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var e1_c2 = new JsonOwnedRoot + { + Name = "e1_c2", + Number = 12, + OwnedReferenceBranch = e1_c2_r, + OwnedCollectionBranch = new List { e1_c2_c1, e1_c2_c2 } + }; + + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------- + + var entity1 = new JsonEntityBasic + { + Id = 1, + Name = "JsonEntityBasic1", + OwnedReferenceRoot = e1_r, + OwnedCollectionRoot = new List { e1_c1, e1_c2 } + }; + + e1_r.Owner = entity1; + e1_c1.Owner = entity1; + e1_c2.Owner = entity1; + + return new List { entity1 }; + } + + public static IReadOnlyList CreateJsonEntitiesCustomNaming() + { + var e1_r_r = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2100, 1, 1), + Fraction = 10.0, + }; + + var e1_r_c1 = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2101, 1, 1), + Fraction = 10.1, + }; + + var e1_r_c2 = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2102, 1, 1), + Fraction = 10.2, + }; + + var e1_r = new JsonOwnedCustomNameRoot + { + Name = "e1_r", + Number = 10, + Enum = JsonEnum.One, + OwnedReferenceBranch = e1_r_r, + OwnedCollectionBranch = new List { e1_r_c1, e1_r_c2 } + }; + + var e1_c1_r = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2110, 1, 1), + Fraction = 11.0, + }; + + var e1_c1_c1 = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2111, 1, 1), + Fraction = 11.1, + }; + + var e1_c1_c2 = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2112, 1, 1), + Fraction = 11.2, + }; + + var e1_c1 = new JsonOwnedCustomNameRoot + { + Name = "e1_c1", + Number = 11, + Enum = JsonEnum.Two, + OwnedReferenceBranch = e1_c1_r, + OwnedCollectionBranch = new List { e1_c1_c1, e1_c1_c2 } + }; + + var e1_c2_r = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2120, 1, 1), + Fraction = 12.0, + }; + + var e1_c2_c1 = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2121, 1, 1), + Fraction = 12.1, + }; + + var e1_c2_c2 = new JsonOwnedCustomNameBranch + { + Date = new DateTime(2122, 1, 1), + Fraction = 12.2, + }; + + var e1_c2 = new JsonOwnedCustomNameRoot + { + Name = "e1_c2", + Number = 12, + Enum = JsonEnum.Three, + OwnedReferenceBranch = e1_c2_r, + OwnedCollectionBranch = new List { e1_c2_c1, e1_c2_c2 } + }; + + var entity1 = new JsonEntityCustomNaming + { + Id = 1, + Title = "JsonEntityCustomNaming1", + OwnedReferenceRoot = e1_r, + OwnedCollectionRoot = new List { e1_c1, e1_c2 } + }; + + return new List { entity1 }; + } + + public static IReadOnlyList CreateJsonEntitiesSingleOwned() + { + var e1 = new JsonEntitySingleOwned + { + Id = 1, + Name = "JsonEntitySingleOwned1", + OwnedCollection = new List + { + new JsonOwnedLeaf { SomethingSomething = "owned_1_1" }, + new JsonOwnedLeaf { SomethingSomething = "owned_1_2" }, + new JsonOwnedLeaf { SomethingSomething = "owned_1_3" }, + } + }; + + var e2 = new JsonEntitySingleOwned + { + Id = 2, + Name = "JsonEntitySingleOwned2", + OwnedCollection = new List + { + } + }; + + var e3 = new JsonEntitySingleOwned + { + Id = 3, + Name = "JsonEntitySingleOwned3", + OwnedCollection = new List + { + new JsonOwnedLeaf { SomethingSomething = "owned_3_1" }, + new JsonOwnedLeaf { SomethingSomething = "owned_3_2" }, + } + }; + + return new List { e1, e2, e3 }; + } + + public static IReadOnlyList CreateJsonEntitiesInheritance() + { + var b1_r_r = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_r", + }; + + var b1_r_c1 = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_c1", + }; + + var b1_r_c2 = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_c2", + }; + + + var b1_r = new JsonOwnedBranch + { + Date = new DateTime(2010, 1, 1), + Fraction = 1.0M, + Enum = JsonEnum.One, + + OwnedReferenceLeaf = b1_r_r, + OwnedCollectionLeaf = new List { b1_r_c1, b1_r_c2 } + }; + + var b1_c1_r = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_r", + }; + + var b1_c1_c1 = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_c1", + }; + + var b1_c1_c2 = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_c2", + }; + + var b1_c1 = new JsonOwnedBranch + { + Date = new DateTime(2011, 1, 1), + Fraction = 11.1M, + Enum = JsonEnum.Three, + + OwnedReferenceLeaf = b1_c1_r, + OwnedCollectionLeaf = new List { b1_c1_c1, b1_c1_c2 } + }; + + var b1_c2_r = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_r", + }; + + var b1_c2_c1 = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_c1", + }; + + var b1_c2_c2 = new JsonOwnedLeaf + { + SomethingSomething = "b1_r_c2", + }; + + var b1_c2 = new JsonOwnedBranch + { + Date = new DateTime(2012, 1, 1), + Fraction = 12.1M, + Enum = JsonEnum.Two, + + OwnedReferenceLeaf = b1_c2_r, + OwnedCollectionLeaf = new List { b1_c2_c1, b1_c2_c2 } + }; + + var b2_r_r = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_r", + }; + + var b2_r_c1 = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_c1", + }; + + var b2_r_c2 = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_c2", + }; + + var b2_r = new JsonOwnedBranch + { + Date = new DateTime(2020, 1, 1), + Fraction = 2.0M, + Enum = JsonEnum.Two, + + OwnedReferenceLeaf = b2_r_r, + OwnedCollectionLeaf = new List { b2_r_c1, b2_r_c2 } + }; + + var b2_c1_r = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_r", + }; + + var b2_c1_c1 = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_c1", + }; + + var b2_c1_c2 = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_c2", + }; + + var b2_c1 = new JsonOwnedBranch + { + Date = new DateTime(2021, 1, 1), + Fraction = 21.1M, + Enum = JsonEnum.Three, + + OwnedReferenceLeaf = b2_c1_r, + OwnedCollectionLeaf = new List { b2_c1_c1, b2_c1_c2 } + }; + + var b2_c2_r = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_r", + }; + + var b2_c2_c1 = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_c1", + }; + + var b2_c2_c2 = new JsonOwnedLeaf + { + SomethingSomething = "b2_r_c2", + }; + + var b2_c2 = new JsonOwnedBranch + { + Date = new DateTime(2022, 1, 1), + Fraction = 22.1M, + Enum = JsonEnum.One, + + OwnedReferenceLeaf = b2_c2_r, + OwnedCollectionLeaf = new List { b2_c2_c1, b2_c2_c2 } + }; + + var d2_r_r = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_r", + }; + + var d2_r_c1 = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_c1", + }; + + var d2_r_c2 = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_c2", + }; + + var d2_r = new JsonOwnedBranch + { + Date = new DateTime(2220, 1, 1), + Fraction = 22.0M, + Enum = JsonEnum.One, + + OwnedReferenceLeaf = d2_r_r, + OwnedCollectionLeaf = new List { d2_r_c1, d2_r_c2 } + }; + + var d2_c1_r = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_r", + }; + + var d2_c1_c1 = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_c1", + }; + + var d2_c1_c2 = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_c2", + }; + + var d2_c1 = new JsonOwnedBranch + { + Date = new DateTime(2221, 1, 1), + Fraction = 221.1M, + Enum = JsonEnum.Two, + + OwnedReferenceLeaf = d2_c1_r, + OwnedCollectionLeaf = new List { d2_c1_c1, d2_c1_c2 } + }; + + var d2_c2_r = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_r", + }; + + var d2_c2_c1 = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_c1", + }; + + var d2_c2_c2 = new JsonOwnedLeaf + { + SomethingSomething = "d2_r_c2", + }; + + var d2_c2 = new JsonOwnedBranch + { + Date = new DateTime(2222, 1, 1), + Fraction = 222.1M, + Enum = JsonEnum.Three, + + OwnedReferenceLeaf = d2_c2_r, + OwnedCollectionLeaf = new List { d2_c2_c1, d2_c2_c2 } + }; + + var baseEntity = new JsonEntityInheritanceBase + { + Id = 1, + Name = "JsonEntityInheritanceBase1", + ReferenceOnBase = b1_r, + CollectionOnBase = new List { b1_c1, b1_c2 } + }; + + var derivedEntity = new JsonEntityInheritanceDerived + { + Id = 2, + Name = "JsonEntityInheritanceDerived2", + ReferenceOnBase = b2_r, + CollectionOnBase = new List { b2_c1, b2_c2 }, + + ReferenceOnDerived = d2_r, + CollectionOnDerived = new List { d2_c1, d2_c2 }, + }; + + return new List { baseEntity, derivedEntity }; + } + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(JsonEntityBasic)) + { + return (IQueryable)JsonEntitiesBasic.AsQueryable(); + } + + if (typeof(TEntity) == typeof(JsonEntityCustomNaming)) + { + return (IQueryable)JsonEntitiesCustomNaming.AsQueryable(); + } + + if (typeof(TEntity) == typeof(JsonEntitySingleOwned)) + { + return (IQueryable)JsonEntitiesSingleOwned.AsQueryable(); + } + + if (typeof(TEntity) == typeof(JsonEntityInheritanceBase)) + { + return (IQueryable)JsonEntitiesInheritance.AsQueryable(); + } + + if (typeof(TEntity) == typeof(JsonEntityInheritanceDerived)) + { + return (IQueryable)JsonEntitiesInheritance.OfType().AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs new file mode 100644 index 00000000000..b55f4a8c988 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs @@ -0,0 +1,564 @@ +// 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.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Update; + +public abstract class JsonUpdateTestBase: SharedStoreFixtureBase +{ + protected override string StoreName => "JsonUpdateTest"; + + [ConditionalFact] + public virtual Task Add_entity_with_json() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var newEntity = new JsonEntityBasic + { + Id = 2, + Name = "NewEntity", + OwnedCollectionRoot = new List(), + OwnedReferenceRoot = new JsonOwnedRoot + { + Name = "RootName", + Number = 42, + OwnedCollectionBranch = new List(), + OwnedReferenceBranch = new JsonOwnedBranch + { + Date = new DateTime(2010, 10, 10), + Enum = JsonEnum.Three, + Fraction = 42.42m, + OwnedCollectionLeaf = new List + { + new JsonOwnedLeaf { SomethingSomething = "ss1" }, + new JsonOwnedLeaf { SomethingSomething = "ss2" }, + }, + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" } + } + }, + }; + + context.Set().Add(newEntity); + await context.SaveChangesAsync(); + }, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + Assert.Equal(2, query.Count); + + var newEntity = query.Where(e => e.Id == 2).Single(); + Assert.Equal("NewEntity", newEntity.Name); + Assert.Null(newEntity.OwnedCollectionRoot); + Assert.Equal("RootName", newEntity.OwnedReferenceRoot.Name); + Assert.Equal(42, newEntity.OwnedReferenceRoot.Number); + Assert.Null(newEntity.OwnedReferenceRoot.OwnedCollectionBranch); + Assert.Equal(new DateTime(2010, 10, 10), newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Date); + Assert.Equal(JsonEnum.Three, newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Enum); + Assert.Equal(42.42m, newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Fraction); + + Assert.Equal(42.42m, newEntity.OwnedReferenceRoot.OwnedReferenceBranch.Fraction); + Assert.Equal("ss3", newEntity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); + + var collectionLeaf = newEntity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf; + Assert.Equal(2, collectionLeaf.Count); + Assert.Equal("ss1", collectionLeaf[0].SomethingSomething); + Assert.Equal("ss2", collectionLeaf[1].SomethingSomething); + }); + + [ConditionalFact] + public virtual Task Add_json_reference_root() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot = null; + await context.SaveChangesAsync(); + }, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + + Assert.Null(entity.OwnedReferenceRoot); + entity.OwnedReferenceRoot = new JsonOwnedRoot + { + Name = "RootName", + Number = 42, + OwnedCollectionBranch = new List(), + OwnedReferenceBranch = new JsonOwnedBranch + { + Date = new DateTime(2010, 10, 10), + Enum = JsonEnum.Three, + Fraction = 42.42m, + OwnedCollectionLeaf = new List + { + new JsonOwnedLeaf { SomethingSomething = "ss1" }, + new JsonOwnedLeaf { SomethingSomething = "ss2" }, + }, + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" } + } + }; + await context.SaveChangesAsync(); + }, + + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + var updatedReference = updatedEntity.OwnedReferenceRoot; + Assert.Equal("RootName", updatedReference.Name); + Assert.Equal(42, updatedReference.Number); + Assert.Null(updatedReference.OwnedCollectionBranch); + Assert.Equal(new DateTime(2010, 10, 10), updatedReference.OwnedReferenceBranch.Date); + Assert.Equal(JsonEnum.Three, updatedReference.OwnedReferenceBranch.Enum); + Assert.Equal(42.42m, updatedReference.OwnedReferenceBranch.Fraction); + Assert.Equal("ss3", updatedReference.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); + var collectionLeaf = updatedReference.OwnedReferenceBranch.OwnedCollectionLeaf; + Assert.Equal(2, collectionLeaf.Count); + Assert.Equal("ss1", collectionLeaf[0].SomethingSomething); + Assert.Equal("ss2", collectionLeaf[1].SomethingSomething); + }); + + [ConditionalFact] + public virtual Task Add_json_reference_leaf() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf = null; + await context.SaveChangesAsync(); + }, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + + Assert.Null(entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf); + var newLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" }; + entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf = newLeaf; + + await context.SaveChangesAsync(); + }, + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + var updatedReference = updatedEntity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf; + Assert.Equal("ss3", updatedReference.SomethingSomething); + }); + + [ConditionalFact] + public virtual Task Add_element_to_json_collection_root() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + + var newRoot = new JsonOwnedRoot + { + Name = "new Name", + Number = 142, + OwnedCollectionBranch = new List(), + OwnedReferenceBranch = new JsonOwnedBranch + { + Date = new DateTime(2010, 10, 10), + Enum = JsonEnum.Three, + Fraction = 42.42m, + OwnedCollectionLeaf = new List + { + new JsonOwnedLeaf { SomethingSomething = "ss1" }, + new JsonOwnedLeaf { SomethingSomething = "ss2" }, + }, + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" } + } + }; + + entity.OwnedCollectionRoot.Add(newRoot); + await context.SaveChangesAsync(); + }, + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + var updatedCollection = updatedEntity.OwnedCollectionRoot; + Assert.Equal(3, updatedCollection.Count); + Assert.Equal("new Name", updatedCollection[2].Name); + Assert.Equal(142, updatedCollection[2].Number); + Assert.Null(updatedCollection[2].OwnedCollectionBranch); + Assert.Equal(new DateTime(2010, 10, 10), updatedCollection[2].OwnedReferenceBranch.Date); + Assert.Equal(JsonEnum.Three, updatedCollection[2].OwnedReferenceBranch.Enum); + Assert.Equal(42.42m, updatedCollection[2].OwnedReferenceBranch.Fraction); + Assert.Equal("ss3", updatedCollection[2].OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); + var collectionLeaf = updatedCollection[2].OwnedReferenceBranch.OwnedCollectionLeaf; + Assert.Equal(2, collectionLeaf.Count); + Assert.Equal("ss1", collectionLeaf[0].SomethingSomething); + Assert.Equal("ss2", collectionLeaf[1].SomethingSomething); + }); + + [ConditionalFact] + public virtual Task Add_element_to_json_collection_branch() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + var newBranch = new JsonOwnedBranch + { + Date = new DateTime(2010, 10, 10), + Enum = JsonEnum.Three, + Fraction = 42.42m, + OwnedCollectionLeaf = new List + { + new JsonOwnedLeaf { SomethingSomething = "ss1" }, + new JsonOwnedLeaf { SomethingSomething = "ss2" }, + }, + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" } + }; + + entity.OwnedReferenceRoot.OwnedCollectionBranch.Add(newBranch); + await context.SaveChangesAsync(); + }, + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + var updatedCollection = updatedEntity.OwnedReferenceRoot.OwnedCollectionBranch; + Assert.Equal(3, updatedCollection.Count); + Assert.Equal(new DateTime(2010, 10, 10), updatedCollection[2].Date); + Assert.Equal(JsonEnum.Three, updatedCollection[2].Enum); + Assert.Equal(42.42m, updatedCollection[2].Fraction); + Assert.Equal("ss3", updatedCollection[2].OwnedReferenceLeaf.SomethingSomething); + var collectionLeaf = updatedCollection[2].OwnedCollectionLeaf; + Assert.Equal(2, collectionLeaf.Count); + Assert.Equal("ss1", collectionLeaf[0].SomethingSomething); + Assert.Equal("ss2", collectionLeaf[1].SomethingSomething); + }); + + [ConditionalFact] + public virtual Task Add_element_to_json_collection_leaf() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + var newLeaf = new JsonOwnedLeaf { SomethingSomething = "ss1" }; + entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf.Add(newLeaf); + await context.SaveChangesAsync(); + }, + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + var updatedCollection = updatedEntity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf; + Assert.Equal(3, updatedCollection.Count); + Assert.Equal("ss1", updatedCollection[2].SomethingSomething); + }); + + [ConditionalFact] + public virtual Task Delete_entity_with_json() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + + context.Set().Remove(entity); + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().CountAsync(); + + Assert.Equal(0, result); + }); + + [ConditionalFact] + public virtual Task Delete_json_reference_root() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot = null; + await context.SaveChangesAsync(); + }, + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + Assert.Null(updatedEntity.OwnedReferenceRoot); + }); + + [ConditionalFact] + public virtual Task Delete_json_reference_leaf() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf = null; + await context.SaveChangesAsync(); + }, + async context => + { + var updatedEntity = await context.JsonEntitiesBasic.SingleAsync(); + Assert.Null(updatedEntity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf); + }); + + [ConditionalFact] + public virtual Task Delete_json_collection_root() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedCollectionRoot = null; + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(); + Assert.Null(result.OwnedCollectionRoot); + }); + + [ConditionalFact] + public virtual Task Delete_json_collection_branch() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedReferenceRoot.OwnedCollectionBranch = null; + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(); + Assert.Null(result.OwnedReferenceRoot.OwnedCollectionBranch); + }); + + [ConditionalFact] + public virtual Task Edit_element_in_json_collection_root1() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedCollectionRoot[0].Name = "Modified"; + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(); + var resultCollection = result.OwnedCollectionRoot; + Assert.Equal(2, resultCollection.Count); + Assert.Equal("Modified", resultCollection[0].Name); + }); + + [ConditionalFact] + public virtual Task Edit_element_in_json_collection_root2() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedCollectionRoot[1].Name = "Modified"; + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(); + var resultCollection = result.OwnedCollectionRoot; + Assert.Equal(2, resultCollection.Count); + Assert.Equal("Modified", resultCollection[1].Name); + }); + + [ConditionalFact] + public virtual Task Edit_element_in_json_collection_branch() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + entity.OwnedCollectionRoot[0].OwnedCollectionBranch[0].Date = new DateTime(2111, 11, 11); + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(); + Assert.Equal(new DateTime(2111, 11, 11), result.OwnedCollectionRoot[0].OwnedCollectionBranch[0].Date); + }); + + [ConditionalFact] + public virtual Task Add_element_to_json_collection_on_derived() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesInheritance.OfType().ToListAsync(); + var entity = query.Single(); + + var newBranch = new JsonOwnedBranch + { + Date = new DateTime(2010, 10, 10), + Enum = JsonEnum.Three, + Fraction = 42.42m, + OwnedCollectionLeaf = new List + { + new JsonOwnedLeaf { SomethingSomething = "ss1" }, + new JsonOwnedLeaf { SomethingSomething = "ss2" }, + }, + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "ss3" } + }; + + entity.CollectionOnDerived.Add(newBranch); + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.JsonEntitiesInheritance.OfType().SingleAsync(); + var updatedCollection = result.CollectionOnDerived; + + Assert.Equal(new DateTime(2010, 10, 10), updatedCollection[2].Date); + Assert.Equal(JsonEnum.Three, updatedCollection[2].Enum); + Assert.Equal(42.42m, updatedCollection[2].Fraction); + Assert.Equal("ss3", updatedCollection[2].OwnedReferenceLeaf.SomethingSomething); + var collectionLeaf = updatedCollection[2].OwnedCollectionLeaf; + Assert.Equal(2, collectionLeaf.Count); + Assert.Equal("ss1", collectionLeaf[0].SomethingSomething); + Assert.Equal("ss2", collectionLeaf[1].SomethingSomething); + }); + + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + protected override void Seed(JsonQueryContext context) + { + var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic(); + var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); + + context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic); + context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); + context.SaveChanges(); + } + + protected override void Clean(DbContext context) + { + base.Clean(context); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.OwnedReferenceRoot, b => + { + b.ToJson(); + b.WithOwner(x => x.Owner); + b.OwnsOne(x => x.OwnedReferenceBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + }); + b.OwnsMany(x => x.OwnedCollectionBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.Navigation(x => x.OwnedReferenceLeaf).IsRequired(false); + bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + }); + }); + + modelBuilder.Entity().Navigation(x => x.OwnedReferenceRoot).IsRequired(false); + + modelBuilder.Entity().OwnsMany(x => x.OwnedCollectionRoot, b => + { + b.OwnsOne(x => x.OwnedReferenceBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf).WithOwner(x => x.Parent); + }); + + b.OwnsMany(x => x.OwnedCollectionBranch, bb => + { + bb.Property(x => x.Fraction).HasPrecision(18, 2); + bb.OwnsOne(x => x.OwnedReferenceLeaf).WithOwner(x => x.Parent); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + }); + b.ToJson(); + }); + + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity(b => + { + b.OwnsOne(x => x.ReferenceOnBase, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + + b.OwnsMany(x => x.CollectionOnBase, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + }); + + modelBuilder.Entity(b => + { + b.HasBaseType(); + b.OwnsOne(x => x.ReferenceOnDerived, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + + b.OwnsMany(x => x.CollectionOnDerived, bb => + { + bb.ToJson(); + bb.OwnsOne(x => x.OwnedReferenceLeaf); + bb.OwnsMany(x => x.OwnedCollectionLeaf); + bb.Property(x => x.Fraction).HasPrecision(18, 2); + }); + }); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + base.OnModelCreating(modelBuilder, context); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 5764028c12e..678204e07f1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -6992,6 +6992,14 @@ await Test( o.Property("Date2"); o.ToJson(); }); + + e.OwnsOne("Owned", "OwnedRequiredReference", o => + { + o.Property("Date"); + o.ToJson(); + }); + + e.Navigation("OwnedRequiredReference").IsRequired(); }); }, model => @@ -7012,6 +7020,13 @@ await Test( { Assert.Equal("OwnedReference", c.Name); Assert.Equal("nvarchar(max)", c.StoreType); + Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("OwnedRequiredReference", c.Name); + Assert.Equal("nvarchar(max)", c.StoreType); + Assert.False(c.IsNullable); }); Assert.Same( table.Columns.Single(c => c.Name == "Id"), @@ -7023,7 +7038,8 @@ await Test( [Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NULL, [OwnedCollection] nvarchar(max) NULL, - [OwnedReference] nvarchar(max) NOT NULL, + [OwnedReference] nvarchar(max) NULL, + [OwnedRequiredReference] nvarchar(max) NOT NULL, CONSTRAINT [PK_Entity] PRIMARY KEY ([Id]) );"); } @@ -7098,7 +7114,7 @@ await Test( [Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NULL, [json_collection] nvarchar(max) NULL, - [json_reference] nvarchar(max) NOT NULL, + [json_reference] nvarchar(max) NULL, CONSTRAINT [PK_Entity] PRIMARY KEY ([Id]) );"); } @@ -7135,6 +7151,14 @@ await Test( o.ToJson(); }); + e.OwnsOne("Owned", "OwnedRequiredReference", o => + { + o.Property("Date"); + o.ToJson(); + }); + + e.Navigation("OwnedRequiredReference").IsRequired(); + e.OwnsMany("Owned2", "OwnedCollection", o => { o.OwnsOne("Nested3", "NestedReference2", n => @@ -7168,6 +7192,13 @@ await Test( { Assert.Equal("OwnedReference", c.Name); Assert.Equal("nvarchar(max)", c.StoreType); + Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("OwnedRequiredReference", c.Name); + Assert.Equal("nvarchar(max)", c.StoreType); + Assert.False(c.IsNullable); }); Assert.Same( table.Columns.Single(c => c.Name == "Id"), @@ -7177,7 +7208,9 @@ await Test( AssertSql( @"ALTER TABLE [Entity] ADD [OwnedCollection] nvarchar(max) NULL;", // - @"ALTER TABLE [Entity] ADD [OwnedReference] nvarchar(max) NOT NULL DEFAULT N'';"); + @"ALTER TABLE [Entity] ADD [OwnedReference] nvarchar(max) NULL;", + // + @"ALTER TABLE [Entity] ADD [OwnedRequiredReference] nvarchar(max) NOT NULL DEFAULT N'';"); } [ConditionalFact] @@ -7603,7 +7636,7 @@ FROM [sys].[default_constraints] [d] // @"ALTER TABLE [Entity] ADD [OwnedCollection] nvarchar(max) NULL;", // - @"ALTER TABLE [Entity] ADD [OwnedReference] nvarchar(max) NOT NULL DEFAULT N'';"); + @"ALTER TABLE [Entity] ADD [OwnedReference] nvarchar(max) NULL;"); } [ConditionalFact] @@ -7779,6 +7812,55 @@ await Test( c => Assert.Equal("Name", c.Name)); }); + AssertSql(); + } + + [ConditionalFact] + public virtual async Task Convert_string_column_to_a_json_column_containing_required_reference() + { + await Test( + builder => + { + builder.Entity("Entity", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + }); + }, + builder => + { + builder.Entity("Entity", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + + e.OwnsOne("Owned", "OwnedReference", o => + { + o.ToJson("Name"); + o.OwnsOne("Nested", "NestedReference", n => + { + n.Property("Number"); + }); + o.OwnsMany("Nested2", "NestedCollection", n => + { + n.Property("Number2"); + }); + o.Property("Date"); + }); + + e.Navigation("OwnedReference").IsRequired(); + }); + }, + model => + { + var table = model.Tables.Single(); + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + }); + AssertSql( @"DECLARE @var0 sysname; SELECT @var0 = [d].[name] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs new file mode 100644 index 00000000000..ee81ce182db --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs @@ -0,0 +1,12 @@ +// 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.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class JsonQuerySqlServerFixture : JsonQueryFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs new file mode 100644 index 00000000000..944df10ce60 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -0,0 +1,558 @@ +// 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 JsonQuerySqlServerTest : JsonQueryTestBase +{ + public JsonQuerySqlServerTest(JsonQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Basic_json_projection_owner_entity(bool async) + { + await base.Basic_json_projection_owner_entity(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_owned_reference_root(bool async) + { + await base.Basic_json_projection_owned_reference_root(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_owned_reference_duplicated(bool async) + { + await base.Basic_json_projection_owned_reference_duplicated(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +ORDER BY [j].[Id]"); + } + + public override async Task Basic_json_projection_owned_collection_root(bool async) + { + await base.Basic_json_projection_owned_collection_root(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_owned_reference_branch(bool async) + { + await base.Basic_json_projection_owned_reference_branch(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_owned_collection_branch(bool async) + { + await base.Basic_json_projection_owned_collection_branch(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_owned_reference_leaf(bool async) + { + await base.Basic_json_projection_owned_reference_leaf(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedReferenceLeaf'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_owned_collection_leaf(bool async) + { + await base.Basic_json_projection_owned_collection_leaf(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedCollectionLeaf'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Basic_json_projection_scalar(bool async) + { + await base.Basic_json_projection_scalar(async); + + AssertSql( + @"SELECT CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Json_scalar_length(bool async) + { + await base.Json_scalar_length(async); + + AssertSql( + @"SELECT [j].[Name] +FROM [JsonEntitiesBasic] AS [j] +WHERE CAST(LEN(CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max))) AS int) > 2"); + } + + + public override async Task Basic_json_projection_enum_inside_json_entity(bool async) + { + await base.Basic_json_projection_enum_inside_json_entity(async); + + AssertSql( + @"SELECT [j].[Id], CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.Enum') AS nvarchar(max)) AS [Enum] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Json_projection_enum_with_custom_conversion(bool async) + { + await base.Json_projection_enum_with_custom_conversion(async); + + AssertSql( + @"SELECT [j].[Id], CAST(JSON_VALUE([j].[json_reference_custom_naming],'$.CustomEnum') AS int) AS [Enum] +FROM [JsonEntitiesCustomNaming] AS [j]"); + } + + public override async Task Json_projection_with_deduplication(bool async) + { + await base.Json_projection_with_deduplication(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$'), JSON_QUERY([j].[OwnedReferenceRoot],'$'), CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething') AS nvarchar(max)) +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Json_projection_with_deduplication_reverse_order(bool async) + { + await base.Json_projection_with_deduplication_reverse_order(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedReferenceRoot],'$'), [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollectionRoot],'$') +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Json_property_in_predicate(bool async) + { + await base.Json_property_in_predicate(async); + + AssertSql( + @"SELECT [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +WHERE CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.Fraction') AS decimal(18,2)) < 20.5"); + } + + public override async Task Json_subquery_property_pushdown_length(bool async) + { + await base.Json_subquery_property_pushdown_length(async); + + AssertSql( + @"@__p_0='3' + +SELECT CAST(LEN([t0].[c]) AS int) +FROM ( + SELECT DISTINCT [t].[c] + FROM ( + SELECT TOP(@__p_0) CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething') AS nvarchar(max)) AS [c] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] +) AS [t0]"); + } + + public override async Task Json_subquery_reference_pushdown_reference(bool async) + { + await base.Json_subquery_reference_pushdown_reference(async); + + AssertSql( + @"@__p_0='10' + +SELECT JSON_QUERY([t0].[c],'$.OwnedReferenceBranch'), [t0].[Id] +FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[OwnedReferenceRoot],'$') AS [c], [j].[Id] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] +) AS [t0]"); + } + + public override async Task Json_subquery_reference_pushdown_reference_anonymous_projection(bool async) + { + await base.Json_subquery_reference_pushdown_reference_anonymous_projection(async); + + AssertSql( + @"@__p_0='10' + +SELECT JSON_QUERY([t0].[c],'$.OwnedReferenceSharedBranch'), [t0].[Id], CAST(LEN([t0].[c0]) AS int) +FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[c0] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[json_reference_shared],'$') AS [c], [j].[Id], CAST(JSON_VALUE([j].[json_reference_shared],'$.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf.SomethingSomething') AS nvarchar(max)) AS [c0] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] +) AS [t0]"); + } + + public override async Task Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(bool async) + { + await base.Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(async); + + AssertSql( + @"@__p_0='10' + +SELECT JSON_QUERY([t2].[c],'$.OwnedReferenceSharedLeaf'), [t2].[Id], JSON_QUERY([t2].[c],'$.OwnedCollectionSharedLeaf'), [t2].[Length] +FROM ( + SELECT DISTINCT JSON_QUERY([t1].[c],'$') AS [c], [t1].[Id], [t1].[Length] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([t0].[c],'$.OwnedReferenceSharedBranch') AS [c], [t0].[Id], CAST(LEN([t0].[Scalar]) AS int) AS [Length] + FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[Scalar] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[json_reference_shared],'$') AS [c], [j].[Id], CAST(JSON_VALUE([j].[json_reference_shared],'$.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf.SomethingSomething') AS nvarchar(max)) AS [Scalar] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] + ) AS [t0] + ORDER BY CAST(LEN([t0].[Scalar]) AS int) + ) AS [t1] +) AS [t2]"); + } + + public override async Task Json_subquery_reference_pushdown_reference_pushdown_reference(bool async) + { + await base.Json_subquery_reference_pushdown_reference_pushdown_reference(async); + + AssertSql( + @"@__p_0='10' + +SELECT JSON_QUERY([t2].[c],'$.OwnedReferenceLeaf'), [t2].[Id] +FROM ( + SELECT DISTINCT JSON_QUERY([t1].[c],'$') AS [c], [t1].[Id] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([t0].[c],'$.OwnedReferenceBranch') AS [c], [t0].[Id] + FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[c] AS [c0] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[OwnedReferenceRoot],'$') AS [c], [j].[Id] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] + ) AS [t0] + ORDER BY CAST(JSON_VALUE([t0].[c0],'$.Name') AS nvarchar(max)) + ) AS [t1] +) AS [t2]"); + } + + public override async Task Json_subquery_reference_pushdown_reference_pushdown_collection(bool async) + { + await base.Json_subquery_reference_pushdown_reference_pushdown_collection(async); + + AssertSql( + @"@__p_0='10' + +SELECT JSON_QUERY([t2].[c],'$.OwnedCollectionLeaf'), [t2].[Id] +FROM ( + SELECT DISTINCT JSON_QUERY([t1].[c],'$') AS [c], [t1].[Id] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([t0].[c],'$.OwnedReferenceBranch') AS [c], [t0].[Id] + FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[c] AS [c0] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[OwnedReferenceRoot],'$') AS [c], [j].[Id] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] + ) AS [t0] + ORDER BY CAST(JSON_VALUE([t0].[c0],'$.Name') AS nvarchar(max)) + ) AS [t1] +) AS [t2]"); + } + + public override async Task Json_subquery_reference_pushdown_property(bool async) + { + await base.Json_subquery_reference_pushdown_property(async); + + AssertSql( + @"@__p_0='10' + +SELECT CAST(JSON_VALUE([t0].[c],'$.SomethingSomething') AS nvarchar(max)) +FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedReferenceLeaf') AS [c], [j].[Id] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] +) AS [t0]"); + } + + public override async Task Custom_naming_projection_owner_entity(bool async) + { + await base.Custom_naming_projection_owner_entity(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Title], JSON_QUERY([j].[json_collection_custom_naming],'$'), JSON_QUERY([j].[json_reference_custom_naming],'$') +FROM [JsonEntitiesCustomNaming] AS [j]"); + } + + public override async Task Custom_naming_projection_owned_reference(bool async) + { + await base.Custom_naming_projection_owned_reference(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[json_reference_custom_naming],'$.CustomOwnedReferenceBranch'), [j].[Id] +FROM [JsonEntitiesCustomNaming] AS [j]"); + } + + public override async Task Custom_naming_projection_owned_collection(bool async) + { + await base.Custom_naming_projection_owned_collection(async); + + AssertSql( + @"SELECT JSON_QUERY([j].[json_collection_custom_naming],'$'), [j].[Id] +FROM [JsonEntitiesCustomNaming] AS [j] +ORDER BY [j].[Id]"); + } + + public override async Task Custom_naming_projection_owned_scalar(bool async) + { + await base.Custom_naming_projection_owned_scalar(async); + + AssertSql( + @"SELECT CAST(JSON_VALUE([j].[json_reference_custom_naming],'$.CustomOwnedReferenceBranch.CustomFraction') AS float) +FROM [JsonEntitiesCustomNaming] AS [j]"); + } + + public override async Task Custom_naming_projection_everything(bool async) + { + await base.Custom_naming_projection_everything(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Title], JSON_QUERY([j].[json_collection_custom_naming],'$'), JSON_QUERY([j].[json_reference_custom_naming],'$'), CAST(JSON_VALUE([j].[json_reference_custom_naming],'$.CustomName') AS nvarchar(max)), CAST(JSON_VALUE([j].[json_reference_custom_naming],'$.CustomOwnedReferenceBranch.CustomFraction') AS float) +FROM [JsonEntitiesCustomNaming] AS [j]"); + } + + public override async Task Project_entity_with_single_owned(bool async) + { + await base.Project_entity_with_single_owned(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollection],'$') +FROM [JsonEntitiesSingleOwned] AS [j]"); + } + + public override async Task Left_join_json_entities(bool async) + { + await base.Left_join_json_entities(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Name], JSON_QUERY([j].[OwnedCollection],'$'), [j0].[Id], [j0].[Name], JSON_QUERY([j0].[OwnedCollectionRoot],'$'), JSON_QUERY([j0].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesSingleOwned] AS [j] +LEFT JOIN [JsonEntitiesBasic] AS [j0] ON [j].[Id] = [j0].[Id]"); + } + + public override async Task Left_join_json_entities_complex_projection(bool async) + { + await base.Left_join_json_entities_complex_projection(async); + + AssertSql( + @"SELECT [j].[Id], [j0].[Id], [j0].[Name], JSON_QUERY([j0].[OwnedCollectionRoot],'$'), JSON_QUERY([j0].[OwnedReferenceRoot],'$') +FROM [JsonEntitiesSingleOwned] AS [j] +LEFT JOIN [JsonEntitiesBasic] AS [j0] ON [j].[Id] = [j0].[Id]"); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery(async); + + AssertSql( + @"SELECT JSON_QUERY([t].[c],'$'), [t].[Id] +FROM [JsonEntitiesBasic] AS [j] +OUTER APPLY ( + SELECT TOP(1) JSON_QUERY([j0].[OwnedReferenceRoot],'$.OwnedReferenceBranch') AS [c], [j0].[Id] + FROM [JsonEntitiesBasic] AS [j0] + ORDER BY [j0].[Id] +) AS [t] +ORDER BY [j].[Id]"); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(async); + + AssertSql( + @"SELECT ( + SELECT TOP(1) CAST(JSON_VALUE([j0].[OwnedReferenceRoot],'$.OwnedReferenceBranch.Date') AS datetime2) + FROM [JsonEntitiesBasic] AS [j0] + ORDER BY [j0].[Id]) +FROM [JsonEntitiesBasic] AS [j] +ORDER BY [j].[Id]"); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(async); + + AssertSql( + @""); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery_deduplication(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_deduplication(async); + + AssertSql( + @"SELECT JSON_QUERY([t].[c],'$'), [t].[Id], JSON_QUERY([t].[c0],'$'), [t].[Id0], JSON_QUERY([t].[c1],'$'), [t].[c2], [t].[c3], [t].[c4] +FROM [JsonEntitiesBasic] AS [j] +OUTER APPLY ( + SELECT TOP(1) JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch') AS [c], [j].[Id], JSON_QUERY([j0].[OwnedReferenceRoot],'$') AS [c0], [j0].[Id] AS [Id0], JSON_QUERY([j0].[OwnedReferenceRoot],'$.OwnedReferenceBranch') AS [c1], CAST(JSON_VALUE([j0].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) AS [c2], CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.Enum') AS nvarchar(max)) AS [c3], 1 AS [c4] + FROM [JsonEntitiesBasic] AS [j0] + ORDER BY [j0].[Id] +) AS [t] +ORDER BY [j].[Id]"); + } + + + public override async Task Project_json_entity_FirstOrDefault_subquery_deduplication_and_outer_reference(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_deduplication_and_outer_reference(async); + + AssertSql( + @"SELECT JSON_QUERY([t].[c],'$'), [t].[Id], JSON_QUERY([t].[c0],'$'), [t].[Id0], JSON_QUERY([t].[c1],'$'), [t].[c2], [t].[c3], [t].[c4] +FROM [JsonEntitiesBasic] AS [j] +OUTER APPLY ( + SELECT TOP(1) JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch') AS [c], [j].[Id], JSON_QUERY([j0].[OwnedReferenceRoot],'$') AS [c0], [j0].[Id] AS [Id0], JSON_QUERY([j0].[OwnedReferenceRoot],'$.OwnedReferenceBranch') AS [c1], CAST(JSON_VALUE([j0].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) AS [c2], CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.Enum') AS nvarchar(max)) AS [c3], 1 AS [c4] + FROM [JsonEntitiesBasic] AS [j0] + ORDER BY [j0].[Id] +) AS [t] +ORDER BY [j].[Id]"); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery_deduplication_outer_reference_and_pruning(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_deduplication_outer_reference_and_pruning(async); + + AssertSql( + @"SELECT JSON_QUERY([t].[c],'$'), [t].[Id], [t].[c0] +FROM [JsonEntitiesBasic] AS [j] +OUTER APPLY ( + SELECT TOP(1) JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch') AS [c], [j].[Id], 1 AS [c0] + FROM [JsonEntitiesBasic] AS [j0] + ORDER BY [j0].[Id] +) AS [t] +ORDER BY [j].[Id]"); + } + + + public override async Task Json_entity_with_inheritance_basic_projection(bool async) + { + await base.Json_entity_with_inheritance_basic_projection(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Discriminator], [j].[Name], [j].[Fraction], JSON_QUERY([j].[CollectionOnBase],'$'), JSON_QUERY([j].[ReferenceOnBase],'$'), JSON_QUERY([j].[CollectionOnDerived],'$'), JSON_QUERY([j].[ReferenceOnDerived],'$') +FROM [JsonEntitiesInheritance] AS [j]"); + } + + public override async Task Json_entity_with_inheritance_project_derived(bool async) + { + await base.Json_entity_with_inheritance_project_derived(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Discriminator], [j].[Name], [j].[Fraction], JSON_QUERY([j].[CollectionOnBase],'$'), JSON_QUERY([j].[ReferenceOnBase],'$'), JSON_QUERY([j].[CollectionOnDerived],'$'), JSON_QUERY([j].[ReferenceOnDerived],'$') +FROM [JsonEntitiesInheritance] AS [j] +WHERE [j].[Discriminator] = N'JsonEntityInheritanceDerived'"); + } + + public override async Task Json_entity_with_inheritance_project_navigations(bool async) + { + await base.Json_entity_with_inheritance_project_navigations(async); + + AssertSql( + @"SELECT [j].[Id], JSON_QUERY([j].[ReferenceOnBase],'$'), JSON_QUERY([j].[CollectionOnBase],'$') +FROM [JsonEntitiesInheritance] AS [j]"); + } + + public override async Task Json_entity_with_inheritance_project_navigations_on_derived(bool async) + { + await base.Json_entity_with_inheritance_project_navigations_on_derived(async); + + AssertSql( + @"SELECT [j].[Id], [j].[Discriminator], [j].[Name], [j].[Fraction], JSON_QUERY([j].[CollectionOnBase],'$'), JSON_QUERY([j].[ReferenceOnBase],'$'), JSON_QUERY([j].[CollectionOnDerived],'$'), JSON_QUERY([j].[ReferenceOnDerived],'$') +FROM [JsonEntitiesInheritance] AS [j] +WHERE [j].[Discriminator] = N'JsonEntityInheritanceDerived'"); + } + + public override async Task Json_entity_backtracking(bool async) + { + await base.Json_entity_backtracking(async); + + AssertSql( + @""); + } + + public override async Task Json_collection_element_access_in_projection_basic(bool async) + { + await base.Json_collection_element_access_in_projection_basic(async); + + // array element access in projection is currently done on the client - issue 28648 + AssertSql( + @"SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j]"); + } + + public override async Task Json_collection_element_access_in_predicate(bool async) + { + await base.Json_collection_element_access_in_predicate(async); + + AssertSql( + @""); + } + + public override async Task Json_scalar_required_null_semantics(bool async) + { + await base.Json_scalar_required_null_semantics(async); + + AssertSql( + @"SELECT [j].[Name] +FROM [JsonEntitiesBasic] AS [j] +WHERE CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Number') AS int) <> CAST(LEN(CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max))) AS int) OR CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) IS NULL"); + } + + public override async Task Json_scalar_optional_null_semantics(bool async) + { + await base.Json_scalar_optional_null_semantics(async); + + AssertSql( + @"SELECT [j].[Name] +FROM [JsonEntitiesBasic] AS [j] +WHERE CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) = CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething') AS nvarchar(max)) OR (CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) IS NULL AND CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething') AS nvarchar(max)) IS NULL)"); + } + + public override async Task Group_by_on_json_scalar(bool async) + { + await base.Group_by_on_json_scalar(async); + + AssertSql( + @"SELECT [t].[Key], COUNT(*) AS [Count] +FROM ( + SELECT CAST(JSON_VALUE([j].[OwnedReferenceRoot],'$.Name') AS nvarchar(max)) AS [Key] + FROM [JsonEntitiesBasic] AS [j] +) AS [t] +GROUP BY [t].[Key]"); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerJsonUpdateTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerJsonUpdateTest.cs new file mode 100644 index 00000000000..849d21649ef --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerJsonUpdateTest.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.Update; + +public class SqlServerJsonUpdateTest : JsonUpdateTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; +} diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs index f47fb74836d..ac248289723 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -1859,6 +1859,78 @@ public virtual void Json_entity_with_custom_property_names() } } + [ConditionalFact] + public virtual void Json_entity_and_normal_owned_can_exist_side_to_side_on_same_entity() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity(b => + { + b.OwnsOne(x => x.OwnedReference1); + b.OwnsOne(x => x.OwnedReference2, bb => bb.ToJson("reference")); + b.OwnsMany(x => x.OwnedCollection1); + b.OwnsMany(x => x.OwnedCollection2, bb => bb.ToJson("collection")); + }); + + var model = modelBuilder.FinalizeModel(); + + var ownedEntities = model.FindEntityTypes(typeof(OwnedEntity)); + Assert.Equal(4, ownedEntities.Count()); + Assert.Equal(2, ownedEntities.Where(e => e.IsMappedToJson()).Count()); + Assert.Equal(2, ownedEntities.Where(e => e.IsOwned() && !e.IsMappedToJson()).Count()); + } + + [ConditionalFact] + public virtual void Json_entity_with_nested_structure_same_property_names_() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity(b => + { + b.OwnsOne(x => x.OwnedReference1, bb => + { + bb.ToJson("ref1"); + bb.OwnsOne(x => x.Reference1); + bb.OwnsOne(x => x.Reference2); + bb.OwnsMany(x => x.Collection1); + bb.OwnsMany(x => x.Collection2); + }); + + b.OwnsOne(x => x.OwnedReference2, bb => + { + bb.ToJson("ref2"); + bb.OwnsOne(x => x.Reference1); + bb.OwnsOne(x => x.Reference2); + bb.OwnsMany(x => x.Collection1); + bb.OwnsMany(x => x.Collection2); + }); + + b.OwnsMany(x => x.OwnedCollection1, bb => + { + bb.ToJson("col1"); + bb.OwnsOne(x => x.Reference1); + bb.OwnsOne(x => x.Reference2); + bb.OwnsMany(x => x.Collection1); + bb.OwnsMany(x => x.Collection2); + }); + + b.OwnsMany(x => x.OwnedCollection2, bb => + { + bb.ToJson("col2"); + bb.OwnsOne(x => x.Reference1); + bb.OwnsOne(x => x.Reference2); + bb.OwnsMany(x => x.Collection1); + bb.OwnsMany(x => x.Collection2); + }); + }); + + var model = modelBuilder.FinalizeModel(); + var outerOwnedEntities = model.FindEntityTypes(typeof(OwnedEntityExtraLevel)); + Assert.Equal(4, outerOwnedEntities.Count()); + + var ownedEntities = model.FindEntityTypes(typeof(OwnedEntity)); + Assert.Equal(16, ownedEntities.Count()); + } + protected override TestModelBuilder CreateModelBuilder(Action? configure = null) => CreateTestModelBuilder(SqlServerTestHelpers.Instance, configure); } diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs index 47e1712f1e6..c3d15497ae7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs @@ -8,6 +8,8 @@ public class SqliteComplianceTest : RelationalComplianceTestBase protected override ICollection IgnoredTestBases { get; } = new HashSet { typeof(FromSqlSprocQueryTestBase<>), + typeof(JsonQueryTestBase<>), + typeof(JsonUpdateTestBase), typeof(SqlExecutorTestBase<>), typeof(UdfDbFunctionTestBase<>), typeof(TPCRelationshipsQueryTestBase<>), // internal class is added