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