diff --git a/src/EFCore.Relational/Query/EntityProjectionExpression.cs b/src/EFCore.Relational/Query/EntityProjectionExpression.cs index 6d54e0b473b..a1454869810 100644 --- a/src/EFCore.Relational/Query/EntityProjectionExpression.cs +++ b/src/EFCore.Relational/Query/EntityProjectionExpression.cs @@ -22,7 +22,7 @@ public class EntityProjectionExpression : Expression /// /// Creates a new instance of the class. /// - /// The entity type to shape. + /// An entity type to shape. /// A dictionary of column expressions corresponding to properties of the entity type. /// A to generate discriminator for each concrete entity type in hierarchy. public EntityProjectionExpression( @@ -115,7 +115,6 @@ public virtual EntityProjectionExpression MakeNullable() discriminatorExpression = ce.MakeNullable(); } - var primaryKeyProperties = GetMappedKeyProperties(EntityType.FindPrimaryKey()!); var ownedNavigationMap = new Dictionary(); foreach (var (navigation, shaper) in _ownedNavigationMap) { @@ -123,18 +122,8 @@ public virtual EntityProjectionExpression MakeNullable() { // 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 newJsonQueryExpression = jsonQueryExpression.MakeNullable(); var newShaper = shaper.Update(newJsonQueryExpression).MakeNullable(); ownedNavigationMap[navigation] = newShaper; } @@ -145,34 +134,6 @@ public virtual EntityProjectionExpression MakeNullable() 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(); - } } /// diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs index 491ce382206..ee742a7a8c4 100644 --- a/src/EFCore.Relational/Query/JsonQueryExpression.cs +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -3,275 +3,253 @@ using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -namespace Microsoft.EntityFrameworkCore.Query +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// +/// An expression representing an entity or a collection of entities mapped to a JSON column and the path to access it. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class JsonQueryExpression : Expression, IPrintableExpression { + private readonly IReadOnlyDictionary _keyPropertyMap; + + /// + /// Creates a new instance of the class. + /// + /// An entity type being represented by this expression. + /// A column containing JSON value. + /// A map of key properties and columns they map to in the database. + /// A type of the element represented by this expression. + /// A value indicating whether this expression represents a collection or not. + public JsonQueryExpression( + IEntityType entityType, + ColumnExpression jsonColumn, + IReadOnlyDictionary keyPropertyMap, + Type type, + bool collection) + : this( + entityType, + jsonColumn, + keyPropertyMap, + path: new List { new("$") }, + type, + collection, + jsonColumn.IsNullable) + { + } + + private JsonQueryExpression( + IEntityType entityType, + ColumnExpression jsonColumn, + IReadOnlyDictionary keyPropertyMap, + IReadOnlyList path, + Type type, + bool collection, + bool nullable) + { + Check.DebugAssert(entityType.FindPrimaryKey() != null, "primary key is null."); + + EntityType = entityType; + JsonColumn = jsonColumn; + IsCollection = collection; + _keyPropertyMap = keyPropertyMap; + Type = type; + Path = path; + IsNullable = nullable; + } + + /// + /// The entity type being represented by this expression. + /// + public virtual IEntityType EntityType { get; } + + /// + /// The column containg JSON value. + /// + public virtual ColumnExpression JsonColumn { get; } + + /// + /// The value indicating whether this expression represents a collection. + /// + public virtual bool IsCollection { get; } + + /// + /// The list of path segments leading to the entity from the root of the JSON stored in the column. + /// + public virtual IReadOnlyList Path { get; } + + /// + /// The value indicating whether this expression is nullable. + /// + public virtual bool IsNullable { get; } + + /// + public override ExpressionType NodeType => ExpressionType.Extension; + + /// + public override Type Type { get; } + /// - /// Expression representing an entity or a collection of entities mapped to a JSON column and the path to access it. + /// Binds a property with this JSON query expression to get the SQL representation. /// - public class JsonQueryExpression : Expression, IPrintableExpression + public virtual SqlExpression BindProperty(IProperty property) { - 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, - path: new SqlConstantExpression(Constant("$"), typeMapping: null), - jsonColumn.IsNullable) + if (!EntityType.IsAssignableFrom(property.DeclaringEntityType) + && !property.DeclaringEntityType.IsAssignableFrom(EntityType)) { + throw new InvalidOperationException( + RelationalStrings.UnableToBindMemberToEntityProjection("property", property.Name, EntityType.DisplayName())); } - private JsonQueryExpression( - IEntityType entityType, - ColumnExpression jsonColumn, - bool collection, - IReadOnlyDictionary keyPropertyMap, - Type type, - SqlExpression path, - bool nullable) + if (_keyPropertyMap.TryGetValue(property, out var match)) { - Check.DebugAssert(entityType.FindPrimaryKey() != null, "primary key is null."); - - EntityType = entityType; - JsonColumn = jsonColumn; - IsCollection = collection; - _keyPropertyMap = keyPropertyMap; - Type = type; - Path = path; - _nullable = nullable; + return match; } - /// - /// 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 Path { 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())); - } + var newPath = Path.ToList(); + newPath.Add(new PathSegment(property.GetJsonPropertyName()!)); - if (_keyPropertyMap.TryGetValue(property, out var match)) - { - return match; - } + return new JsonScalarExpression( + JsonColumn, + property, + newPath, + IsNullable || property.IsNullable); + } - var pathSegment = new SqlConstantExpression( - Constant(property.GetJsonPropertyName()), - typeMapping: null); - - var newPath = new SqlBinaryExpression( - ExpressionType.Add, - Path, - 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)); } - /// - /// 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) + var targetEntityType = navigation.TargetEntityType; + var newPath = Path.ToList(); + newPath.Add(new PathSegment(targetEntityType.GetJsonPropertyName()!)); + + 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))) { - if (navigation.ForeignKey.DependentToPrincipal == navigation) - { - // issue #28645 - throw new InvalidOperationException( - RelationalStrings.JsonCantNavigateToParentEntity( - navigation.ForeignKey.DeclaringEntityType.DisplayName(), - navigation.ForeignKey.PrincipalEntityType.DisplayName(), - navigation.Name)); - } + newKeyPropertyMap[target] = _keyPropertyMap[source]; + } - var targetEntityType = navigation.TargetEntityType; - var pathSegment = new SqlConstantExpression( - Constant(navigation.TargetEntityType.GetJsonPropertyName()), - typeMapping: null); - - var newJsonPath = new SqlBinaryExpression( - ExpressionType.Add, - Path, - 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, + newKeyPropertyMap, + newPath, + navigation.ClrType, + navigation.IsCollection, + IsNullable || !navigation.ForeignKey.IsRequiredDependent); + } - 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(); } - /// - /// 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 new JsonQueryExpression( + EntityType, + JsonColumn.MakeNullable(), + keyPropertyMap, + Path, + Type, + IsCollection, + nullable: true); + } - return MakeNullable(keyPropertyMap); - } + /// + public virtual void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("JsonQueryExpression("); + expressionPrinter.Visit(JsonColumn); + expressionPrinter.Append($", {string.Join("", Path.Select(e => e.ToString()))})"); + } - /// - /// Makes this JSON query expression nullable re-using existing nullable key properties - /// - /// A new expression which has property set to true. - internal virtual JsonQueryExpression MakeNullable(IReadOnlyDictionary nullableKeyPropertyMap) - => Update( - JsonColumn.MakeNullable(), - nullableKeyPropertyMap, - Path, - nullable: true); - - /// - public virtual void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Append("JsonQueryExpression("); - expressionPrinter.Visit(JsonColumn); - expressionPrinter.Append($", \"{string.Join(".", Path)}\")"); - } + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn); - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn); - var jsonPath = (SqlExpression)visitor.Visit(Path); + // TODO: also visit columns in the _keyPropertyMap? + return Update(jsonColumn, _keyPropertyMap); + } - // 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. + /// This expression if no children changed, or an expression with the updated children. + public virtual JsonQueryExpression Update( + ColumnExpression jsonColumn, + IReadOnlyDictionary keyPropertyMap) + => jsonColumn != JsonColumn + || keyPropertyMap.Count != _keyPropertyMap.Count + || keyPropertyMap.Zip(_keyPropertyMap, (n, o) => n.Value != o.Value).Any(x => x) + ? new JsonQueryExpression(EntityType, jsonColumn, keyPropertyMap, Path, Type, IsCollection, IsNullable) + : 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) + && IsNullable == jsonQueryExpression.IsNullable + && Path.SequenceEqual(jsonQueryExpression.Path) + && KeyPropertyMapEquals(jsonQueryExpression._keyPropertyMap); + + private bool KeyPropertyMapEquals(IReadOnlyDictionary other) + { + if (_keyPropertyMap.Count != other.Count) + { + return false; } - /// - /// 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 path, - bool nullable) - => jsonColumn != JsonColumn - || keyPropertyMap.Count != _keyPropertyMap.Count - || keyPropertyMap.Zip(_keyPropertyMap, (n, o) => n.Value != o.Value).Any(x => x) - || path != Path - ? new JsonQueryExpression(EntityType, jsonColumn, IsCollection, keyPropertyMap, Type, path, 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) - && Path.Equals(jsonQueryExpression.Path) - && IsNullable == jsonQueryExpression.IsNullable - && KeyPropertyMapEquals(jsonQueryExpression._keyPropertyMap); - - private bool KeyPropertyMapEquals(IReadOnlyDictionary other) + foreach (var (key, value) in _keyPropertyMap) { - if (_keyPropertyMap.Count != other.Count) + if (!other.TryGetValue(key, out var column) || !value.Equals(column)) { 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, Path, IsNullable); + return true; } + + /// + public override int GetHashCode() + // not incorporating _keyPropertyMap into the hash, too much work + => HashCode.Combine(EntityType, JsonColumn, IsCollection, Path, IsNullable); } diff --git a/src/EFCore.Relational/Query/PathSegment.cs b/src/EFCore.Relational/Query/PathSegment.cs new file mode 100644 index 00000000000..8c25cd5eacd --- /dev/null +++ b/src/EFCore.Relational/Query/PathSegment.cs @@ -0,0 +1,49 @@ +// 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; + +/// +/// +/// A class representing a component of JSON path used in or . +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class PathSegment +{ + /// + /// Creates a new instance of the class. + /// + /// A key which is being accessed in the JSON. + public PathSegment(string key) + { + Key = key; + } + + /// + /// The key which is being accessed in the JSON. + /// + public virtual string Key { get; } + + /// + public override string ToString() => (Key == "$" ? "" : ".") + Key; + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is PathSegment pathSegment + && Equals(pathSegment)); + + private bool Equals(PathSegment pathSegment) + => Key == pathSegment.Key; + + /// + public override int GetHashCode() + => HashCode.Combine(Key); +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs index e888c5f99eb..1a568204cf9 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/JsonScalarExpression.cs @@ -1,106 +1,106 @@ // 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 +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An expression representing a scalar extracted from a JSON column with the given path in SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class JsonScalarExpression : SqlExpression { /// - /// Expression representing a scalar extracted from a JSON column with the given path. + /// Creates a new instance of the class. /// - public class JsonScalarExpression : SqlExpression + /// A column containg JSON value. + /// A property representing the result of this expression. + /// A list of path segments 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, + IReadOnlyList path, + bool nullable) + : this(jsonColumn, path, property.ClrType, property.FindRelationalTypeMapping()!, nullable) { - /// - /// 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 path, - bool nullable) - : this(jsonColumn, property.ClrType, property.FindRelationalTypeMapping()!, path, nullable) - { - } + } - internal JsonScalarExpression( - ColumnExpression jsonColumn, - Type type, - RelationalTypeMapping typeMapping, - SqlExpression path, - bool nullable) - : base(type, typeMapping) - { - JsonColumn = jsonColumn; - Path = path; - IsNullable = nullable; - } + internal JsonScalarExpression( + ColumnExpression jsonColumn, + IReadOnlyList path, + Type type, + RelationalTypeMapping typeMapping, + bool nullable) + : base(type, typeMapping) + { + JsonColumn = jsonColumn; + Path = path; + IsNullable = nullable; + } - /// - /// The column containg JSON. - /// - public virtual ColumnExpression JsonColumn { get; } + /// + /// The column containg JSON value. + /// + 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 Path { get; } + /// + /// The list of path segments leading to the scalar from the root of the JSON stored in the column. + /// + public virtual IReadOnlyList Path { get; } - /// - /// The value indicating whether the expression is nullable. - /// - public virtual bool IsNullable { 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; + /// + 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!, - Path, - IsNullable || jsonColumnMadeNullable) - : this; - } + // TODO Call update: Issue#28887 + return jsonColumn != JsonColumn + ? new JsonScalarExpression( + jsonColumn, + Path, + Type, + TypeMapping!, + 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. - /// This expression if no children changed, or an expression with the updated children. - public virtual JsonScalarExpression Update( - ColumnExpression jsonColumn, - SqlExpression path) - => jsonColumn != JsonColumn - || path != Path - ? new JsonScalarExpression(jsonColumn, Type, TypeMapping!, path, IsNullable) - : 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. + /// This expression if no children changed, or an expression with the updated children. + public virtual JsonScalarExpression Update(ColumnExpression jsonColumn) + => jsonColumn != JsonColumn + ? new JsonScalarExpression(jsonColumn, Path, Type, TypeMapping!, IsNullable) + : this; - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Append("JsonScalarExpression(column: "); - expressionPrinter.Visit(JsonColumn); - expressionPrinter.Append(" Path: "); - expressionPrinter.Visit(Path); - expressionPrinter.Append(")"); - } + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("JsonScalarExpression(column: "); + expressionPrinter.Visit(JsonColumn); + expressionPrinter.Append($", {string.Join("", Path.Select(e => e.ToString()))})"); + } - /// - public override bool Equals(object? obj) - => obj is JsonScalarExpression jsonScalarExpression - && JsonColumn.Equals(jsonScalarExpression.JsonColumn) - && Path.Equals(jsonScalarExpression.Path); + /// + public override bool Equals(object? obj) + => obj is JsonScalarExpression jsonScalarExpression + && JsonColumn.Equals(jsonScalarExpression.JsonColumn) + && Path.SequenceEqual(jsonScalarExpression.Path); - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), JsonColumn, Path); - } + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), JsonColumn, Path); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 6cb57eeebf1..72e6ff23af4 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -463,9 +463,9 @@ void GenerateNonHierarchyNonSplittingEntityType(ITableBase table, TableExpressio new JsonQueryExpression( targetEntityType, jsonColumn, - ownedJsonNavigation.IsCollection, keyPropertiesMap, - ownedJsonNavigation.ClrType), + ownedJsonNavigation.ClrType, + ownedJsonNavigation.IsCollection), !ownedJsonNavigation.ForeignKey.IsRequiredDependent); entityProjection.AddNavigationBinding(ownedJsonNavigation, entityShaperExpression); @@ -1391,7 +1391,7 @@ static Dictionary BuildJsonProjection { var ordered = projections .OrderBy(x => $"{x.JsonColumn.TableAlias}.{x.JsonColumn.Name}") - .ThenBy(x => BreakJsonPathIntoComponents(x.Path).Count); + .ThenBy(x => x.Path.Count); var needed = new List(); foreach (var orderedElement in ordered) @@ -1402,9 +1402,9 @@ static Dictionary BuildJsonProjection { jsonScalarExpression = new JsonScalarExpression( orderedElement.JsonColumn, + orderedElement.Path, orderedElement.JsonColumn.Type, orderedElement.JsonColumn.TypeMapping!, - orderedElement.Path, orderedElement.IsNullable); needed.Add(jsonScalarExpression); @@ -1442,9 +1442,9 @@ ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression, Js var additionalPath = new string[0]; // this will be more tricky once we support more complicated json path options - additionalPath = BreakJsonPathIntoComponents(jsonQueryExpression.Path) - .Skip(BreakJsonPathIntoComponents(jsonScalarToAdd.Path).Count) - .Select(x => (string)((SqlConstantExpression)x).Value!) + additionalPath = jsonQueryExpression.Path + .Skip(jsonScalarToAdd.Path.Count) + .Select(x => x.Key) .ToArray(); var jsonColumnIndex = AddToProjection(jsonScalarToAdd); @@ -1490,8 +1490,8 @@ static bool JsonEntityContainedIn(JsonScalarExpression sourceExpression, JsonQue return false; } - var sourcePath = BreakJsonPathIntoComponents(sourceExpression.Path); - var targetPath = BreakJsonPathIntoComponents(targetExpression.Path); + var sourcePath = sourceExpression.Path; + var targetPath = targetExpression.Path; if (targetPath.Count < sourcePath.Count) { @@ -1500,21 +1500,6 @@ static bool JsonEntityContainedIn(JsonScalarExpression sourceExpression, JsonQue 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; - } } /// @@ -3443,9 +3428,9 @@ JsonQueryExpression LiftJsonQueryFromSubquery(JsonQueryExpression jsonQueryExpre { var jsonScalarExpression = new JsonScalarExpression( jsonQueryExpression.JsonColumn, + jsonQueryExpression.Path, jsonQueryExpression.JsonColumn.TypeMapping!.ClrType, jsonQueryExpression.JsonColumn.TypeMapping, - jsonQueryExpression.Path, jsonQueryExpression.IsNullable); var newJsonColumn = subquery.GenerateOuterColumn(subqueryTableReferenceExpression, jsonScalarExpression); @@ -3467,11 +3452,12 @@ JsonQueryExpression LiftJsonQueryFromSubquery(JsonQueryExpression jsonQueryExpre } // clear up the json path - we start from empty path after pushdown - return jsonQueryExpression.Update( + return new JsonQueryExpression( + jsonQueryExpression.EntityType, newJsonColumn, newKeyPropertyMap, - path: new SqlConstantExpression(Constant("$"), typeMapping: null), - newJsonColumn.IsNullable); + jsonQueryExpression.Type, + jsonQueryExpression.IsCollection); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 898b1e6fe22..0db76ba9fd7 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -761,13 +761,5 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) /// 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.Path); - _isSearchCondition = parentSearchCondition; - - return jsonScalarExpression.Update(jsonScalarExpression.JsonColumn, jsonPath); - } + protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) => jsonScalarExpression; } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index d7139c54d24..ffda9ec8a46 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; -using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -309,21 +308,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp Visit(jsonScalarExpression.JsonColumn); - var jsonPathStrings = new List(); - - if (jsonScalarExpression.Path != null) - { - var currentPath = jsonScalarExpression.Path; - 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)}')"); + Sql.Append($",'{string.Join("", jsonScalarExpression.Path.Select(e => e.ToString()))}')"); if (jsonScalarExpression.Type != typeof(JsonElement)) {