diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index edd36d48c74..0dcdf6cab23 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -299,6 +299,7 @@ The .NET Foundation licenses this file to you under the MIT license.
True
True
True
+ True
True
True
True
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 7b7f43e2888..a490ede6eac 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -1147,6 +1147,12 @@ public static string JsonNodeMustBeHandledByProviderSpecificVisitor
public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation
=> GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation");
+ ///
+ /// Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
+ ///
+ public static string JsonQueryLinqOperatorsNotSupported
+ => GetString("JsonQueryLinqOperatorsNotSupported");
+
///
/// Entity {entity} is required but the JSON element containing it is null.
///
@@ -1487,6 +1493,12 @@ public static string ReadonlyEntitySaved(object? entityType)
public static string RelationalNotInUse
=> GetString("RelationalNotInUse");
+ ///
+ /// SelectExpression can only be built over a JsonQueryExpression that represents a collection within the JSON document.
+ ///
+ public static string SelectCanOnlyBeBuiltOnCollectionJsonQuery
+ => GetString("SelectCanOnlyBeBuiltOnCollectionJsonQuery");
+
///
/// Cannot create a 'SelectExpression' with a custom 'TableExpressionBase' since the result type '{entityType}' is part of a hierarchy and does not contain a discriminator property.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index d212c1beb9e..63470dcbba1 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -550,6 +550,9 @@
The JSON property name should only be configured on nested owned navigations.
+
+ Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
+
Entity {entity} is required but the JSON element containing it is null.
@@ -983,6 +986,9 @@
Relational-specific methods can only be used when the context is using a relational database provider.
+
+ SelectExpression can only be built over a JsonQueryExpression that represents a collection within the JSON document.
+
Cannot create a 'SelectExpression' with a custom 'TableExpressionBase' since the result type '{entityType}' is part of a hierarchy and does not contain a discriminator property.
diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs
index 2e22dccab20..3f11f1676df 100644
--- a/src/EFCore.Relational/Query/JsonQueryExpression.cs
+++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs
@@ -16,8 +16,6 @@ namespace Microsoft.EntityFrameworkCore.Query;
///
public class JsonQueryExpression : Expression, IPrintableExpression
{
- private readonly IReadOnlyDictionary _keyPropertyMap;
-
///
/// Creates a new instance of the class.
///
@@ -57,7 +55,7 @@ private JsonQueryExpression(
EntityType = entityType;
JsonColumn = jsonColumn;
IsCollection = collection;
- _keyPropertyMap = keyPropertyMap;
+ KeyPropertyMap = keyPropertyMap;
Type = type;
Path = path;
IsNullable = nullable;
@@ -88,6 +86,15 @@ private JsonQueryExpression(
///
public virtual bool IsNullable { get; }
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ public virtual IReadOnlyDictionary KeyPropertyMap { get; }
+
///
public override ExpressionType NodeType
=> ExpressionType.Extension;
@@ -107,7 +114,7 @@ public virtual SqlExpression BindProperty(IProperty property)
RelationalStrings.UnableToBindMemberToEntityProjection("property", property.Name, EntityType.DisplayName()));
}
- if (_keyPropertyMap.TryGetValue(property, out var match))
+ if (KeyPropertyMap.TryGetValue(property, out var match))
{
return match;
}
@@ -145,11 +152,11 @@ public virtual JsonQueryExpression BindNavigation(INavigation navigation)
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);
+ 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];
+ newKeyPropertyMap[target] = KeyPropertyMap[source];
}
return new JsonQueryExpression(
@@ -178,7 +185,7 @@ public virtual JsonQueryExpression BindCollectionElement(SqlExpression collectio
return new JsonQueryExpression(
EntityType,
JsonColumn,
- _keyPropertyMap,
+ KeyPropertyMap,
newPath,
EntityType.ClrType,
collection: false,
@@ -194,7 +201,7 @@ public virtual JsonQueryExpression BindCollectionElement(SqlExpression collectio
public virtual JsonQueryExpression MakeNullable()
{
var keyPropertyMap = new Dictionary();
- foreach (var (property, columnExpression) in _keyPropertyMap)
+ foreach (var (property, columnExpression) in KeyPropertyMap)
{
keyPropertyMap[property] = columnExpression.MakeNullable();
}
@@ -222,7 +229,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn);
var newKeyPropertyMap = new Dictionary();
- foreach (var (property, column) in _keyPropertyMap)
+ foreach (var (property, column) in KeyPropertyMap)
{
newKeyPropertyMap[property] = (ColumnExpression)visitor.Visit(column);
}
@@ -241,8 +248,8 @@ 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)
+ || 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;
@@ -259,16 +266,16 @@ private bool Equals(JsonQueryExpression jsonQueryExpression)
&& IsCollection.Equals(jsonQueryExpression.IsCollection)
&& IsNullable == jsonQueryExpression.IsNullable
&& Path.SequenceEqual(jsonQueryExpression.Path)
- && KeyPropertyMapEquals(jsonQueryExpression._keyPropertyMap);
+ && KeyPropertyMapEquals(jsonQueryExpression.KeyPropertyMap);
private bool KeyPropertyMapEquals(IReadOnlyDictionary other)
{
- if (_keyPropertyMap.Count != other.Count)
+ if (KeyPropertyMap.Count != other.Count)
{
return false;
}
- foreach (var (key, value) in _keyPropertyMap)
+ foreach (var (key, value) in KeyPropertyMap)
{
if (!other.TryGetValue(key, out var column) || !value.Equals(column))
{
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index bc8643d363d..abf1c1dd2bc 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -220,6 +220,9 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString())
?? base.VisitExtension(extensionExpression);
+ case JsonQueryExpression jsonQueryExpression:
+ return TransformJsonQueryToTable(jsonQueryExpression) ?? base.VisitExtension(extensionExpression);
+
default:
return base.VisitExtension(extensionExpression);
}
@@ -304,6 +307,19 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
string tableAlias)
=> null;
+ ///
+ /// Invoked when LINQ operators are composed over a collection within a JSON document.
+ /// Transforms the provided - representing access to the collection - into a provider-specific
+ /// means to expand the JSON array into a relational table/rowset (e.g. SQL Server OPENJSON).
+ ///
+ /// The referencing the JSON array.
+ /// A if the translation was successful, otherwise .
+ protected virtual ShapedQueryExpression? TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
+ {
+ AddTranslationErrorDetails(RelationalStrings.JsonQueryLinqOperatorsNotSupported);
+ return null;
+ }
+
///
/// Translates an inline collection into a queryable SQL VALUES expression.
///
@@ -582,9 +598,9 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
protected override ShapedQueryExpression? TranslateDistinct(ShapedQueryExpression source)
{
var selectExpression = (SelectExpression)source.QueryExpression;
- if (selectExpression.Orderings.Count > 0
- && selectExpression.Limit == null
- && selectExpression.Offset == null)
+
+ if (selectExpression is { Orderings.Count: > 0, Limit: null, Offset: null }
+ && !IsNaturallyOrdered(selectExpression))
{
_queryCompilationContext.Logger.DistinctAfterOrderByWithoutRowLimitingOperatorWarning();
}
@@ -1831,6 +1847,16 @@ protected virtual Expression ApplyInferredTypeMappings(
protected virtual bool IsOrdered(SelectExpression selectExpression)
=> selectExpression.Orderings.Count > 0;
+ ///
+ /// Determines whether the given is naturally ordered, meaning that any ordering has been added
+ /// automatically by EF to preserve e.g. the natural ordering of a JSON array, and not because the original LINQ query contained
+ /// an explicit ordering.
+ ///
+ /// The to check for ordering.
+ /// Whether is ordered.
+ protected virtual bool IsNaturallyOrdered(SelectExpression selectExpression)
+ => false;
+
private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
{
var lambdaBody = ReplacingExpressionVisitor.Replace(
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index d515782c6df..996c43f79df 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -485,13 +485,13 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
throw new InvalidOperationException(RelationalStrings.SelectExpressionNonTphWithCustomTable(entityType.DisplayName()));
}
- var table = (tableExpressionBase as ITableBasedExpression)?.Table;
- Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
-
var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!);
AddTable(tableExpressionBase, tableReferenceExpression);
var propertyExpressions = new Dictionary();
+ var table = (tableExpressionBase as ITableBasedExpression)?.Table;
+ Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
+
foreach (var property in GetAllPropertiesInHierarchy(entityType))
{
propertyExpressions[property] = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false);
@@ -511,6 +511,95 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
}
}
+ ///
+ /// Constructs a over a collection within a JSON document.
+ ///
+ ///
+ /// The collection within a JSON document which the will represent.
+ ///
+ ///
+ /// The table for the ; typically a provider-specific table-valued function that converts a JSON
+ /// array to a relational table/rowset (e.g. SQL Server OPENJSON)
+ ///
+ public SelectExpression(JsonQueryExpression jsonQueryExpression, TableExpressionBase tableExpressionBase)
+ : base(null)
+ {
+ if (!jsonQueryExpression.IsCollection)
+ {
+ throw new ArgumentException(RelationalStrings.SelectCanOnlyBeBuiltOnCollectionJsonQuery, nameof(jsonQueryExpression));
+ }
+
+ var entityType = jsonQueryExpression.EntityType;
+
+ Check.DebugAssert(
+ entityType.BaseType is null && !entityType.GetDirectlyDerivedTypes().Any(),
+ "Inheritance encountered inside a JSON document");
+
+ var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!);
+ AddTable(tableExpressionBase, tableReferenceExpression);
+
+ // Create a dictionary mapping all properties to their ColumnExpressions, for the SelectExpression's projection.
+ var propertyExpressions = new Dictionary();
+
+ foreach (var property in GetAllPropertiesInHierarchy(entityType))
+ {
+ // Skip also properties with no JSON name (i.e. shadow keys containing the index in the collection, which don't actually exist
+ // in the JSON document and can't be bound to)
+ if (property.GetJsonPropertyName() is string jsonPropertyName)
+ {
+ propertyExpressions[property] = CreateColumnExpression(
+ tableExpressionBase, jsonPropertyName, property.ClrType, property.GetRelationalTypeMapping(), property.IsNullable);
+ }
+ }
+
+ var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions);
+
+ var containerColumnName = jsonQueryExpression.EntityType.GetContainerColumnName()!;
+ var jsonColumnTypeMapping = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
+ ?? entityType.GetDefaultMappings().Single().Table)
+ .FindColumn(containerColumnName)!.StoreTypeMapping;
+
+ foreach (var navigation in GetAllNavigationsInHierarchy(entityType)
+ .Where(
+ n => n.ForeignKey.IsOwnership
+ && n.TargetEntityType.IsMappedToJson()
+ && n.ForeignKey.PrincipalToDependent == n))
+ {
+ var targetEntityType = navigation.TargetEntityType;
+
+ var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
+ Check.DebugAssert(jsonNavigationName is not null, "Invalid navigation found on JSON-mapped entity");
+
+ // The TableExpressionBase represents a relational expansion of the JSON collection. We now need a ColumnExpression to represent
+ // the specific JSON property (projected as a relational column) which holds the JSON subtree for the target entity.
+ var jsonColumn = new ConcreteColumnExpression(
+ jsonNavigationName,
+ tableReferenceExpression,
+ jsonColumnTypeMapping.ClrType,
+ jsonColumnTypeMapping,
+ nullable: !navigation.ForeignKey.IsRequiredDependent || navigation.IsCollection);
+
+ var entityShaperExpression = new RelationalEntityShaperExpression(
+ targetEntityType,
+ new JsonQueryExpression(
+ targetEntityType,
+ jsonColumn,
+ jsonQueryExpression.KeyPropertyMap,
+ navigation.ClrType,
+ navigation.IsCollection),
+ !navigation.ForeignKey.IsRequiredDependent);
+
+ entityProjection.AddNavigationBinding(navigation, entityShaperExpression);
+ }
+
+ _projectionMapping[new ProjectionMember()] = entityProjection;
+
+ foreach (var (property, column) in jsonQueryExpression.KeyPropertyMap)
+ {
+ _identifier.Add((column, property.GetKeyValueComparer()));
+ }
+ }
+
private void AddJsonNavigationBindings(
IEntityType entityType,
EntityProjectionExpression entityProjection,
@@ -525,16 +614,20 @@ private void AddJsonNavigationBindings(
{
var targetEntityType = ownedJsonNavigation.TargetEntityType;
var jsonColumnName = targetEntityType.GetContainerColumnName()!;
- var jsonColumnTypeMapping = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
+ var jsonColumn = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? entityType.GetDefaultMappings().Single().Table)
- .FindColumn(jsonColumnName)!.StoreTypeMapping;
+ .FindColumn(jsonColumnName)!;
+ var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping;
+ var isNullable = jsonColumn.IsNullable
+ || !ownedJsonNavigation.ForeignKey.IsRequiredDependent
+ || ownedJsonNavigation.IsCollection;
- var jsonColumn = new ConcreteColumnExpression(
+ var jsonColumnExpression = new ConcreteColumnExpression(
jsonColumnName,
tableReferenceExpression,
jsonColumnTypeMapping.ClrType,
jsonColumnTypeMapping,
- nullable: !ownedJsonNavigation.ForeignKey.IsRequiredDependent || ownedJsonNavigation.IsCollection);
+ isNullable);
// for json collections we need to skip ordinal key (which is always the last one)
// simple copy from parent is safe here, because we only do it at top level
@@ -555,11 +648,11 @@ private void AddJsonNavigationBindings(
targetEntityType,
new JsonQueryExpression(
targetEntityType,
- jsonColumn,
+ jsonColumnExpression,
keyPropertiesMap,
ownedJsonNavigation.ClrType,
ownedJsonNavigation.IsCollection),
- !ownedJsonNavigation.ForeignKey.IsRequiredDependent);
+ isNullable);
entityProjection.AddNavigationBinding(ownedJsonNavigation, entityShaperExpression);
}
@@ -1764,9 +1857,7 @@ public void ReplaceProjection(IReadOnlyDictionary
foreach (var (projectionMember, expression) in projectionMapping)
{
Check.DebugAssert(
- expression is SqlExpression
- || expression is EntityProjectionExpression
- || expression is JsonQueryExpression,
+ expression is SqlExpression or EntityProjectionExpression or JsonQueryExpression,
"Invalid operation in the projection.");
_projectionMapping[projectionMember] = expression;
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
index b4869e373dc..85e38459a57 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
@@ -128,19 +128,9 @@ public override string? Alias
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
- {
- var changed = false;
- var arguments = new SqlExpression[Arguments.Count];
- for (var i = 0; i < arguments.Length; i++)
- {
- arguments[i] = (SqlExpression)visitor.Visit(Arguments[i]);
- changed |= arguments[i] != Arguments[i];
- }
-
- return changed
- ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations())
- : this;
- }
+ => visitor.VisitAndConvert(Arguments) is var visitedArguments && visitedArguments == Arguments
+ ? this
+ : new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, visitedArguments, GetAnnotations());
///
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
index 2a12317536f..7fed63fd339 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
@@ -37,8 +37,7 @@ public virtual SqlExpression JsonExpression
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual SqlExpression? Path
- => Arguments.Count == 1 ? null : Arguments[1];
+ public virtual IReadOnlyList? Path { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -54,16 +53,74 @@ public virtual SqlExpression? Path
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
+
public SqlServerOpenJsonExpression(
string alias,
SqlExpression jsonExpression,
- SqlExpression? path = null,
+ IReadOnlyList? path = null,
IReadOnlyList? columnInfos = null)
- : base(alias, "OPENJSON", schema: null, builtIn: true, path is null ? new[] { jsonExpression } : new[] { jsonExpression, path })
+ : base(alias, "OPENJSON", schema: null, builtIn: true, new[] { jsonExpression })
{
+ Path = path;
ColumnInfos = columnInfos;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var visitedJsonExpression = (SqlExpression)visitor.Visit(JsonExpression);
+
+ PathSegment[]? visitedPath = null;
+
+ if (Path is not null)
+ {
+ for (var i = 0; i < Path.Count; i++)
+ {
+ var segment = Path[i];
+ PathSegment newSegment;
+
+ if (segment.PropertyName is not null)
+ {
+ // PropertyName segments are (currently) constants, nothing to visit.
+ newSegment = segment;
+ }
+ else
+ {
+ var newArrayIndex = (SqlExpression)visitor.Visit(segment.ArrayIndex)!;
+ if (newArrayIndex == segment.ArrayIndex)
+ {
+ newSegment = segment;
+ }
+ else
+ {
+ newSegment = new PathSegment(newArrayIndex);
+
+ if (visitedPath is null)
+ {
+ visitedPath = new PathSegment[Path.Count];
+ for (var j = 0; j < i; i++)
+ {
+ visitedPath[j] = Path[j];
+ }
+ }
+ }
+ }
+
+ if (visitedPath is not null)
+ {
+ visitedPath[i] = newSegment;
+ }
+ }
+ }
+
+ return Update(visitedJsonExpression, visitedPath ?? Path, ColumnInfos);
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -72,11 +129,11 @@ public SqlServerOpenJsonExpression(
///
public virtual SqlServerOpenJsonExpression Update(
SqlExpression jsonExpression,
- SqlExpression? path,
+ IReadOnlyList? path,
IReadOnlyList? columnInfos = null)
=> jsonExpression == JsonExpression
- && path == Path
- && (columnInfos is null ? ColumnInfos is null : ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos))
+ && (ReferenceEquals(path, Path) || path is not null && Path is not null && path.SequenceEqual(Path))
+ && (ReferenceEquals(columnInfos, ColumnInfos) || columnInfos is not null && ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos))
? this
: new SqlServerOpenJsonExpression(Alias, jsonExpression, path, columnInfos);
@@ -100,12 +157,26 @@ public virtual TableExpressionBase Clone()
return clone;
}
- ///
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append(Name);
expressionPrinter.Append("(");
- expressionPrinter.VisitCollection(Arguments);
+ expressionPrinter.Visit(JsonExpression);
+
+ if (Path is not null)
+ {
+ expressionPrinter
+ .Append(", '")
+ .Append(string.Join(".", Path.Select(e => e.ToString())))
+ .Append("'");
+ }
+
expressionPrinter.Append(")");
if (ColumnInfos is not null)
@@ -124,11 +195,14 @@ protected override void Print(ExpressionPrinter expressionPrinter)
expressionPrinter
.Append(columnInfo.Name)
.Append(" ")
- .Append(columnInfo.StoreType ?? "");
+ .Append(columnInfo.TypeMapping.StoreType);
if (columnInfo.Path is not null)
{
- expressionPrinter.Append(" ").Append("'" + columnInfo.Path + "'");
+ expressionPrinter
+ .Append(" '")
+ .Append(string.Join(".", columnInfo.Path.Select(e => e.ToString())))
+ .Append("'");
}
if (columnInfo.AsJson)
@@ -141,6 +215,7 @@ protected override void Print(ExpressionPrinter expressionPrinter)
}
PrintAnnotations(expressionPrinter);
+
expressionPrinter.Append(" AS ");
expressionPrinter.Append(Alias);
}
@@ -149,11 +224,35 @@ protected override void Print(ExpressionPrinter expressionPrinter)
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj) || (obj is SqlServerOpenJsonExpression openJsonExpression && Equals(openJsonExpression));
- private bool Equals(SqlServerOpenJsonExpression openJsonExpression)
- => base.Equals(openJsonExpression)
- && (ColumnInfos is null
- ? openJsonExpression.ColumnInfos is null
- : openJsonExpression.ColumnInfos is not null && ColumnInfos.SequenceEqual(openJsonExpression.ColumnInfos));
+ private bool Equals(SqlServerOpenJsonExpression other)
+ {
+ if (!base.Equals(other) || ColumnInfos?.Count != other.ColumnInfos?.Count)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(ColumnInfos, other.ColumnInfos))
+ {
+ return true;
+ }
+
+ for (var i = 0; i < ColumnInfos!.Count; i++)
+ {
+ var (columnInfo, otherColumnInfo) = (ColumnInfos[i], other.ColumnInfos![i]);
+
+ if (columnInfo.Name != otherColumnInfo.Name
+ || !columnInfo.TypeMapping.Equals(otherColumnInfo.TypeMapping)
+ || (columnInfo.Path is null != otherColumnInfo.Path is null
+ || (columnInfo.Path is not null
+ && otherColumnInfo.Path is not null
+ && columnInfo.Path.SequenceEqual(otherColumnInfo.Path))))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
///
public override int GetHashCode()
@@ -165,5 +264,9 @@ public override int GetHashCode()
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public readonly record struct ColumnInfo(string Name, string StoreType, string? Path = null, bool AsJson = false);
+ public readonly record struct ColumnInfo(
+ string Name,
+ RelationalTypeMapping TypeMapping,
+ IReadOnlyList? Path = null,
+ bool AsJson = false);
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index 8a32db8ccda..91da583f5f0 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -421,8 +421,25 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
Visit(jsonScalarExpression.Json);
- Sql.Append(", '$");
- foreach (var pathSegment in jsonScalarExpression.Path)
+ Sql.Append(", ");
+ GenerateJsonPath(jsonScalarExpression.Path);
+ Sql.Append(")");
+
+ if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
+ {
+ Sql.Append(" AS ");
+ Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
+ Sql.Append(")");
+ }
+
+ return jsonScalarExpression;
+ }
+
+ private void GenerateJsonPath(IReadOnlyList path)
+ {
+ Sql.Append("'$");
+
+ foreach (var pathSegment in path)
{
switch (pathSegment)
{
@@ -435,7 +452,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
if (arrayIndex is SqlConstantExpression)
{
- Visit(pathSegment.ArrayIndex);
+ Visit(arrayIndex);
}
else if (_supportsJsonValueExpressions)
{
@@ -458,16 +475,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
}
}
- Sql.Append("')");
-
- if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
- {
- Sql.Append(" AS ");
- Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
- Sql.Append(")");
- }
-
- return jsonScalarExpression;
+ Sql.Append("'");
}
///
@@ -480,11 +488,18 @@ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression
{
// OPENJSON docs: https://learn.microsoft.com/sql/t-sql/functions/openjson-transact-sql
- // OPENJSON is a regular table-valued function with a special WITH clause at the end
- // Copy-paste from VisitTableValuedFunction, because that appends the 'AS ' but we need to insert WITH before that
+ // OPENJSON is a regular table-valued function with an optional special WITH clause at the end.
+ // The second argument is the JSON path, which can either be a regular SqlExpression, or a list of PathSegments, from which we
+ // generate a constant JSONPATH from.
Sql.Append("OPENJSON(");
- GenerateList(openJsonExpression.Arguments, e => Visit(e));
+ Visit(openJsonExpression.JsonExpression);
+
+ if (openJsonExpression.Path is not null)
+ {
+ Sql.Append(", ");
+ GenerateJsonPath(openJsonExpression.Path);
+ }
Sql.Append(")");
@@ -492,27 +507,43 @@ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression
{
Sql.Append(" WITH (");
- for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++)
+ if (openJsonExpression.ColumnInfos is [var singleColumnInfo])
{
- var columnInfo = openJsonExpression.ColumnInfos[i];
+ GenerateColumnInfo(singleColumnInfo);
+ }
+ else
+ {
+ Sql.AppendLine();
+ using var _ = Sql.Indent();
- if (i > 0)
+ for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++)
{
- Sql.Append(", ");
+ var columnInfo = openJsonExpression.ColumnInfos[i];
+
+ if (i > 0)
+ {
+ Sql.AppendLine(",");
+ }
+
+ GenerateColumnInfo(columnInfo);
}
- Check.DebugAssert(columnInfo.StoreType is not null, "Unset OPENJSON column store type");
+ Sql.AppendLine();
+ }
+ Sql.Append(")");
+
+ void GenerateColumnInfo(SqlServerOpenJsonExpression.ColumnInfo columnInfo)
+ {
Sql
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnInfo.Name))
.Append(" ")
- .Append(columnInfo.StoreType);
+ .Append(columnInfo.TypeMapping.StoreType);
if (columnInfo.Path is not null)
{
- Sql
- .Append(" ")
- .Append(_typeMappingSource.GetMapping("varchar(max)").GenerateSqlLiteral(columnInfo.Path));
+ Sql.Append(" ");
+ GenerateJsonPath(columnInfo.Path);
}
if (columnInfo.AsJson)
@@ -520,8 +551,6 @@ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression
Sql.Append(" AS JSON");
}
}
-
- Sql.Append(")");
}
Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(openJsonExpression.Alias));
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
index a9a249cb8b1..dc5c6563cd9 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
@@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using System.Text;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
@@ -15,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SqlServerQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
{
- private readonly OpenJsonPostprocessor _openJsonPostprocessor;
+ private readonly JsonPostprocessor _openJsonPostprocessor;
private readonly SkipWithoutOrderByInSplitQueryVerifier _skipWithoutOrderByInSplitQueryVerifier = new();
///
@@ -88,18 +90,22 @@ private sealed class SkipWithoutOrderByInSplitQueryVerifier : ExpressionVisitor
/// ordering still exists on the [key] column, i.e. when the ordering of the original JSON array needs to be preserved
/// (e.g. limit/offset).
///
- private sealed class OpenJsonPostprocessor : ExpressionVisitor
+ private sealed class JsonPostprocessor : ExpressionVisitor
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
- private readonly Dictionary<(SqlServerOpenJsonExpression, string), RelationalTypeMapping> _castsToApply = new();
+ private readonly Dictionary<(SqlServerOpenJsonExpression, string), (SelectExpression, SqlServerOpenJsonExpression.ColumnInfo)> _columnsToRewrite = new();
- public OpenJsonPostprocessor(IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory)
+ private RelationalTypeMapping? _nvarcharMaxTypeMapping, _nvarchar4000TypeMapping;
+
+ public JsonPostprocessor(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory)
=> (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory);
public Expression Process(Expression expression)
{
- _castsToApply.Clear();
+ _columnsToRewrite.Clear();
return Visit(expression);
}
@@ -147,33 +153,97 @@ public Expression Process(Expression expression)
selectExpression.Offset);
// Record the OPENJSON expression and its projected column(s), along with the store type we just removed from the WITH
- // clause. Then visit the select expression, adding a cast around the matching ColumnExpressions.
+ // clause. Then visit the select expression, replacing all matching ColumnExpressions - see below for the details.
// TODO: Need to pass through the type mapping API for converting the JSON value (nvarchar) to the relational store type
// (e.g. datetime2), see #30677
- foreach (var column in openJsonExpression.ColumnInfos)
+ foreach (var columnInfo in openJsonExpression.ColumnInfos)
{
- var typeMapping = _typeMappingSource.FindMapping(column.StoreType);
- Check.DebugAssert(
- typeMapping is not null,
- $"Could not find mapping for store type {column.StoreType} when converting OPENJSON/WITH");
-
- _castsToApply.Add((newOpenJsonExpression, column.Name), typeMapping);
+ _columnsToRewrite.Add((newOpenJsonExpression, columnInfo.Name), new(newSelectExpression, columnInfo));
}
var result = base.Visit(newSelectExpression);
foreach (var column in openJsonExpression.ColumnInfos)
{
- _castsToApply.Remove((newOpenJsonExpression, column.Name));
+ _columnsToRewrite.Remove((newOpenJsonExpression, column.Name));
}
return result;
}
case ColumnExpression { Table: SqlServerOpenJsonExpression openJsonTable, Name: var name } columnExpression
- when _castsToApply.TryGetValue((openJsonTable, name), out var typeMapping):
+ when _columnsToRewrite.TryGetValue((openJsonTable, name), out var columnRewriteInfo):
+ {
+ // We found a ColumnExpression that refers to the OPENJSON table, we need to rewrite it.
+
+ var (selectExpression, columnInfo) = columnRewriteInfo;
+
+ // The new OPENJSON (without WITH) always projects a `value` column, instead of a properly named column for individual
+ // values inside; create a new ColumnExpression with that name.
+ SqlExpression rewrittenColumn = selectExpression.CreateColumnExpression(
+ columnExpression.Table, "value", columnExpression.Type, _nvarcharMaxTypeMapping, columnExpression.IsNullable);
+
+ // If the WITH column info contained a path, we need to wrap the new column expression with a JSON_VALUE for that path.
+ if (columnInfo.Path is not (null or []))
+ {
+ if (columnInfo.AsJson)
+ {
+ throw new InvalidOperationException(
+ "IMPOSSIBLE. AS JSON signifies an owned sub-entity being projected out of OPENJSON/WITH. "
+ + "Columns referring to that must be wrapped be Json{Scalar,Query}Expression and will have been already " +
+ "dealt with below");
+ }
+
+ _nvarchar4000TypeMapping ??= _typeMappingSource.FindMapping("nvarchar(4000)");
+
+ rewrittenColumn = new JsonScalarExpression(
+ rewrittenColumn, columnInfo.Path, rewrittenColumn.Type, _nvarchar4000TypeMapping, columnExpression.IsNullable);
+ }
+
+ // OPENJSON with WITH specified the store type in the WITH, but the version without just always projects
+ // nvarchar(max); add a CAST to convert. Note that for AS JSON the type mapping is always nvarchar(max), and we don't
+ // need to add a CAST over the JSON_QUERY returned above.
+ if (columnInfo.TypeMapping.StoreType != "nvarchar(max)")
+ {
+ _nvarcharMaxTypeMapping ??= _typeMappingSource.FindMapping("nvarchar(max)");
+
+ rewrittenColumn = _sqlExpressionFactory.Convert(
+ rewrittenColumn,
+ columnExpression.Type,
+ columnInfo.TypeMapping);
+ }
+
+ return rewrittenColumn;
+ }
+
+ // JsonScalarExpression over a column coming out of OPENJSON/WITH; this means that the column represents an owned sub-
+ // entity, and therefore must have AS JSON. Rewrite the column and simply collapse the paths together.
+ case JsonScalarExpression
+ {
+ Json: ColumnExpression { Table: SqlServerOpenJsonExpression openJsonTable } columnExpression
+ } jsonScalarExpression
+ when _columnsToRewrite.TryGetValue((openJsonTable, columnExpression.Name), out var columnRewriteInfo):
{
- return _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, typeMapping);
+ var (selectExpression, columnInfo) = columnRewriteInfo;
+
+ Check.DebugAssert(
+ columnInfo.AsJson,
+ "JsonScalarExpression over a column coming out of OPENJSON is only valid when that column represents an owned "
+ + "sub-entity, which means it must have AS JSON");
+
+ // The new OPENJSON (without WITH) always projects a `value` column, instead of a properly named column for individual
+ // values inside; create a new ColumnExpression with that name.
+ SqlExpression rewrittenColumn = selectExpression.CreateColumnExpression(
+ columnExpression.Table, "value", columnExpression.Type, _nvarcharMaxTypeMapping, columnExpression.IsNullable);
+
+ // Prepend the path from the OPENJSON/WITH to the path in the JsonScalarExpression
+ var path = columnInfo.Path is null
+ ? jsonScalarExpression.Path
+ : columnInfo.Path.Concat(jsonScalarExpression.Path).ToList();
+
+ return new JsonScalarExpression(
+ rewrittenColumn, path, jsonScalarExpression.Type, jsonScalarExpression.TypeMapping,
+ jsonScalarExpression.IsNullable);
}
default:
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index 45db664494c..ba7b85a3c11 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -20,6 +20,8 @@ public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQu
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
+ private RelationalTypeMapping? _nvarcharMaxTypeMapping;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -131,9 +133,15 @@ protected override ShapedQueryExpression TranslateCollection(
var openJsonExpression = elementTypeMapping is null
? new SqlServerOpenJsonExpression(tableAlias, sqlExpression)
: new SqlServerOpenJsonExpression(
- tableAlias, sqlExpression, columnInfos: new[]
+ tableAlias, sqlExpression,
+ columnInfos: new[]
{
- new SqlServerOpenJsonExpression.ColumnInfo { Name = "value", StoreType = elementTypeMapping.StoreType, Path = "$" }
+ new SqlServerOpenJsonExpression.ColumnInfo
+ {
+ Name = "value",
+ TypeMapping = elementTypeMapping,
+ Path = Array.Empty()
+ }
});
// TODO: This is a temporary CLR type-based check; when we have proper metadata to determine if the element is nullable, use it here
@@ -170,6 +178,94 @@ protected override ShapedQueryExpression TranslateCollection(
return new ShapedQueryExpression(selectExpression, shaperExpression);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
+ {
+ // Calculate the table alias for the OPENJSON expression based on the last named path segment
+ // (or the JSON column name if there are none)
+ var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null);
+ var tableAlias = char.ToLowerInvariant((lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name)[0]).ToString();
+
+ // We now add all of projected entity's the properties and navigations into the OPENJSON's WITH clause. Note that navigations
+ // get AS JSON, which projects out the JSON sub-document for them as text, which can be further navigated into.
+ var columnInfos = new List();
+
+ // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
+ foreach (var property in GetAllPropertiesInHierarchy(jsonQueryExpression.EntityType))
+ {
+ if (property.GetJsonPropertyName() is string jsonPropertyName)
+ {
+ columnInfos.Add(new()
+ {
+ Name = jsonPropertyName,
+ TypeMapping = property.GetRelationalTypeMapping(),
+ Path = new PathSegment[] { new(jsonPropertyName) }
+ });
+ }
+ }
+
+ foreach (var navigation in GetAllNavigationsInHierarchy(jsonQueryExpression.EntityType)
+ .Where(
+ n => n.ForeignKey.IsOwnership
+ && n.TargetEntityType.IsMappedToJson()
+ && n.ForeignKey.PrincipalToDependent == n))
+ {
+ var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
+ Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}");
+
+ columnInfos.Add(new()
+ {
+ Name = jsonNavigationName,
+ TypeMapping = _nvarcharMaxTypeMapping ??= _typeMappingSource.FindMapping("nvarchar(max)")!,
+ Path = new PathSegment[] { new(jsonNavigationName) },
+ AsJson = true
+ });
+ }
+
+ var openJsonExpression = new SqlServerOpenJsonExpression(
+ tableAlias, jsonQueryExpression.JsonColumn, jsonQueryExpression.Path, columnInfos);
+
+ var selectExpression = new SelectExpression(jsonQueryExpression, openJsonExpression);
+
+ // See note on OPENJSON and ordering in TranslateCollection
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ _sqlExpressionFactory.Convert(
+ selectExpression.CreateColumnExpression(
+ openJsonExpression,
+ "key",
+ typeof(string),
+ typeMapping: _typeMappingSource.FindMapping("nvarchar(4000)"),
+ columnNullable: false),
+ typeof(int),
+ _typeMappingSource.FindMapping(typeof(int))),
+ ascending: true));
+
+ return new ShapedQueryExpression(
+ selectExpression,
+ new RelationalEntityShaperExpression(
+ jsonQueryExpression.EntityType,
+ new ProjectionBindingExpression(
+ selectExpression,
+ new ProjectionMember(),
+ typeof(ValueBuffer)),
+ false));
+
+ // TODO: Move these to IEntityType?
+ static IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredProperties());
+
+ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredNavigations());
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -254,6 +350,30 @@ protected override ShapedQueryExpression TranslateCollection(
return base.TranslateElementAtOrDefault(source, index, returnDefault);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
+ => selectExpression is
+ {
+ Tables: [SqlServerOpenJsonExpression openJsonExpression, ..],
+ Orderings:
+ [
+ {
+ Expression: SqlUnaryExpression
+ {
+ OperatorType: ExpressionType.Convert,
+ Operand: ColumnExpression { Name: "key", Table: var orderingTable }
+ },
+ IsAscending: true
+ }
+ ]
+ }
+ && orderingTable == openJsonExpression;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -435,8 +555,10 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress
return openJsonExpression;
}
- Check.DebugAssert(openJsonExpression.Path is null, "openJsonExpression.Path is null");
- Check.DebugAssert(openJsonExpression.ColumnInfos is null, "Invalid SqlServerOpenJsonExpression");
+ Check.DebugAssert(
+ openJsonExpression.Path is null, "OpenJsonExpression path is non-null when applying an inferred type mapping");
+ Check.DebugAssert(
+ openJsonExpression.ColumnInfos is null, "OpenJsonExpression has no ColumnInfos when applying an inferred type mapping");
// We need to apply the inferred type mapping in two places: the collection type mapping on the parameter expanded by OPENJSON,
// and on the WITH clause determining the conversion out on the SQL Server side
@@ -463,20 +585,7 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress
return openJsonExpression.Update(
parameterExpression.ApplyTypeMapping(parameterTypeMapping),
path: null,
- new[] { new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping.StoreType, "$") });
+ new[] { new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, Array.Empty()) });
}
-
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public virtual SqlExpression ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping)
- // TODO: this should be part of #30677
- // OPENJSON's value column has type nvarchar(max); apply a CAST() unless that's the inferred element type mapping
- => typeMapping.StoreType is "nvarchar(max)"
- ? columnExpression
- : _sqlExpressionFactory.Convert(columnExpression, typeMapping.ClrType, typeMapping);
}
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
index 8e0622a64ae..0fa1e853271 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
@@ -43,6 +43,10 @@ protected override Expression VisitExtension(Expression extensionExpression)
GenerateRegexp(regexpExpression);
return extensionExpression;
+ case JsonEachExpression jsonEachExpression:
+ GenerateJsonEach(jsonEachExpression);
+ return extensionExpression;
+
default:
return base.VisitExtension(extensionExpression);
}
@@ -123,6 +127,76 @@ private void GenerateRegexp(RegexpExpression regexpExpression, bool negated = fa
Visit(regexpExpression.Pattern);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected virtual void GenerateJsonEach(JsonEachExpression jsonEachExpression)
+ {
+ // json_each docs: https://www.sqlite.org/json1.html#jeach
+
+ // json_each is a regular table-valued function; however, since it accepts an (optional) JSONPATH argument - which we represent
+ // as IReadOnlyList, and that can only be rendered as a string here in the QuerySqlGenerator, we have a special
+ // expression type for it.
+ Sql.Append("json_each(");
+
+ Visit(jsonEachExpression.JsonExpression);
+
+ var path = jsonEachExpression.Path;
+
+ if (path is not null)
+ {
+ Sql.Append(", ");
+
+ // Note the difference with the JSONPATH rendering in VisitJsonScalar below, where we take advantage of SQLite's ->> operator
+ // (we can't do that here).
+ Sql.Append("'$");
+
+ var inJsonpathString = true;
+
+ for (var i = 0; i < path.Count; i++)
+ {
+ switch (path[i])
+ {
+ case { PropertyName: string propertyName }:
+ Sql.Append(".").Append(propertyName);
+ break;
+
+ case { ArrayIndex: SqlExpression arrayIndex }:
+ Sql.Append("[");
+
+ if (arrayIndex is SqlConstantExpression)
+ {
+ Visit(arrayIndex);
+ }
+ else
+ {
+ Sql.Append("' + ");
+ Visit(arrayIndex);
+ Sql.Append(" + '");
+ }
+
+ Sql.Append("]");
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ if (inJsonpathString)
+ {
+ Sql.Append("'");
+ }
+ }
+
+ Sql.Append(")");
+
+ Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(jsonEachExpression.Alias));
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -131,16 +205,15 @@ private void GenerateRegexp(RegexpExpression regexpExpression, bool negated = fa
///
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
{
+ Visit(jsonScalarExpression.Json);
+
// TODO: Stop producing empty JsonScalarExpressions, #30768
var path = jsonScalarExpression.Path;
if (path.Count == 0)
{
- Visit(jsonScalarExpression.Json);
return jsonScalarExpression;
}
- Visit(jsonScalarExpression.Json);
-
var inJsonpathString = false;
for (var i = 0; i < path.Count; i++)
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
index f0eec83f85d..d99665c3bdb 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
@@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Query.SqlExpressions.Internal;
using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
@@ -48,6 +49,15 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor(
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
+ => new SqliteQueryableMethodTranslatingExpressionVisitor(this);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -83,15 +93,6 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor(
return base.TranslateAny(source, predicate);
}
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
- => new SqliteQueryableMethodTranslatingExpressionVisitor(this);
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -199,7 +200,7 @@ protected override ShapedQueryExpression TranslateCollection(
{
var elementClrType = sqlExpression.Type.GetSequenceType();
- var jsonEachExpression = new TableValuedFunctionExpression(tableAlias, "json_each", new[] { sqlExpression });
+ var jsonEachExpression = new JsonEachExpression(tableAlias, sqlExpression);
// TODO: This is a temporary CLR type-based check; when we have proper metadata to determine if the element is nullable, use it here
var isColumnNullable = elementClrType.IsNullableType();
@@ -222,7 +223,7 @@ protected override ShapedQueryExpression TranslateCollection(
"key",
typeof(int),
typeMapping: _typeMappingSource.FindMapping(typeof(int)),
- isColumnNullable),
+ columnNullable: false),
ascending: true));
Expression shaperExpression = new ProjectionBindingExpression(
@@ -240,6 +241,146 @@ protected override ShapedQueryExpression TranslateCollection(
return new ShapedQueryExpression(selectExpression, shaperExpression);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
+ {
+ var entityType = jsonQueryExpression.EntityType;
+ var textTypeMapping = _typeMappingSource.FindMapping(typeof(string));
+
+ // TODO: Refactor this out
+ // Calculate the table alias for the json_each expression based on the last named path segment
+ // (or the JSON column name if there are none)
+ var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null);
+ var tableAlias = char.ToLowerInvariant((lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name)[0]).ToString();
+
+ // Handling a non-primitive JSON array is complicated on SQLite; unlike SQL Server OPENJSON and PostgreSQL jsonb_to_recordset,
+ // SQLite's json_each can only project elements of the array, and not properties within those elements. For example:
+ // SELECT value FROM json_each('[{"a":1,"b":"foo"}, {"a":2,"b":"bar"}]')
+ // This will return two rows, each with a string column representing an array element (i.e. {"a":1,"b":"foo"}). To decompose that
+ // into a and b columns, a further extraction is needed:
+ // SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each('[{"a":1,"b":"foo"}, {"a":2,"b":"bar"}]')
+
+ // We therefore generate a minimal subquery projecting out all the properties and navigations, wrapped by a SelectExpression
+ // containing that:
+ // SELECT ...
+ // FROM (SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each(, )) AS j
+ // WHERE j.a = 8;
+
+ // Unfortunately, while the subquery projects the entity, our EntityProjectionExpression currently supports only bare
+ // ColumnExpression (the above requires JsonScalarExpression). So we hack as if the subquery projects an anonymous type instead,
+ // with a member for each JSON property that needs to be projected. We then wrap it with a SelectExpression the projects a proper
+ // EntityProjectionExpression.
+
+ var jsonEachExpression = new JsonEachExpression(tableAlias, jsonQueryExpression.JsonColumn, jsonQueryExpression.Path);
+
+ var selectExpression = new SelectExpression(jsonQueryExpression, jsonEachExpression);
+
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ jsonEachExpression,
+ "key",
+ typeof(int),
+ typeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnNullable: false),
+ ascending: true));
+
+ var propertyJsonScalarExpression = new Dictionary();
+
+ var jsonColumn = selectExpression.CreateColumnExpression(
+ jsonEachExpression, "value", typeof(string), _typeMappingSource.FindMapping(typeof(string))); // TODO: nullable?
+
+ var containerColumnName = entityType.GetContainerColumnName();
+ Check.DebugAssert(containerColumnName is not null, "JsonQueryExpression to entity type without a container column name");
+
+ // First step: build a SelectExpression that will execute json_each and project all properties and navigations out, e.g.
+ // (SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each(c."JsonColumn", '$.Something.SomeCollection')
+
+ // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
+ foreach (var property in GetAllPropertiesInHierarchy(entityType))
+ {
+ if (property.GetJsonPropertyName() is string jsonPropertyName)
+ {
+ // HACK: currently the only way to project multiple values from a SelectExpression is to simulate a Select out to an anonymous
+ // type; this requires the MethodInfos of the anonymous type properties, from which the projection alias gets taken.
+ // So we create fake members to hold the JSON property name for the alias.
+ var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonPropertyName));
+
+ propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression(
+ jsonColumn,
+ new[] { new PathSegment(property.GetJsonPropertyName()!) },
+ property.ClrType.UnwrapNullableType(),
+ property.GetRelationalTypeMapping(),
+ property.IsNullable);
+ }
+ }
+
+ foreach (var navigation in GetAllNavigationsInHierarchy(jsonQueryExpression.EntityType)
+ .Where(
+ n => n.ForeignKey.IsOwnership
+ && n.TargetEntityType.IsMappedToJson()
+ && n.ForeignKey.PrincipalToDependent == n))
+ {
+ var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
+ Check.DebugAssert(jsonNavigationName is not null, "Invalid navigation found on JSON-mapped entity");
+
+ var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName));
+
+ propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression(
+ jsonColumn,
+ new[] { new PathSegment(jsonNavigationName) },
+ typeof(string),
+ textTypeMapping,
+ !navigation.ForeignKey.IsRequiredDependent);
+ }
+
+ selectExpression.ReplaceProjection(propertyJsonScalarExpression);
+
+ // Second step: push the above SelectExpression down to a subquery, and project an entity projection from the outer
+ // SelectExpression, i.e.
+ // SELECT "t"."a", "t"."b"
+ // FROM (SELECT value ->> 'a' ... FROM json_each(...))
+
+ selectExpression.PushdownIntoSubquery();
+ var subquery = selectExpression.Tables[0];
+
+ var newOuterSelectExpression = new SelectExpression(jsonQueryExpression, subquery);
+
+ newOuterSelectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ subquery,
+ "key",
+ typeof(int),
+ typeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnNullable: false),
+ ascending: true));
+
+ return new ShapedQueryExpression(
+ newOuterSelectExpression,
+ new RelationalEntityShaperExpression(
+ jsonQueryExpression.EntityType,
+ new ProjectionBindingExpression(
+ newOuterSelectExpression,
+ new ProjectionMember(),
+ typeof(ValueBuffer)),
+ false));
+
+ // TODO: Move these to IEntityType?
+ static IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredProperties());
+
+ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredNavigations());
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -306,6 +447,26 @@ protected override ShapedQueryExpression TranslateCollection(
return base.TranslateElementAtOrDefault(source, index, returnDefault);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
+ => selectExpression is
+ {
+ Tables: [JsonEachExpression openJsonExpression, ..],
+ Orderings:
+ [
+ {
+ Expression: ColumnExpression { Name: "key", Table: var orderingTable },
+ IsAscending: true
+ }
+ ]
+ }
+ && orderingTable == openJsonExpression;
+
private static Type GetProviderType(SqlExpression expression)
=> expression.TypeMapping?.Converter?.ProviderClrType
?? expression.TypeMapping?.ClrType
@@ -448,4 +609,22 @@ private static SqlExpression ApplyTypeMappingOnColumn(SqlExpression expression,
_ => expression
};
+
+ private class FakeMemberInfo : MemberInfo
+ {
+ public FakeMemberInfo(string name)
+ => Name = name;
+
+ public override string Name { get; }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ => throw new NotSupportedException();
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ => throw new NotSupportedException();
+ public override bool IsDefined(Type attributeType, bool inherit)
+ => throw new NotSupportedException();
+ public override Type? DeclaringType => throw new NotSupportedException();
+ public override MemberTypes MemberType => throw new NotSupportedException();
+ public override Type? ReflectedType => throw new NotSupportedException();
+ }
}
diff --git a/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/JsonEachExpression.cs b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/JsonEachExpression.cs
new file mode 100644
index 00000000000..0a45c8c784d
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/JsonEachExpression.cs
@@ -0,0 +1,185 @@
+// 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.Sqlite.Query.SqlExpressions.Internal;
+
+///
+/// An expression that represents a SQLite json_each function call in a SQL tree.
+///
+///
+///
+/// See json_each for more information and examples.
+///
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+///
+public class JsonEachExpression : TableValuedFunctionExpression, IClonableTableExpressionBase
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlExpression JsonExpression
+ => Arguments[0];
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? Path { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public JsonEachExpression(
+ string alias,
+ SqlExpression jsonExpression,
+ IReadOnlyList? path = null)
+ : base(alias, "json_each", schema: null, builtIn: true, new[] { jsonExpression })
+ {
+ Path = path;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var visitedJsonExpression = (SqlExpression)visitor.Visit(JsonExpression);
+
+ PathSegment[]? visitedPath = null;
+
+ if (Path is not null)
+ {
+ for (var i = 0; i < Path.Count; i++)
+ {
+ var segment = Path[i];
+ PathSegment newSegment;
+
+ if (segment.PropertyName is not null)
+ {
+ // PropertyName segments are (currently) constants, nothing to visit.
+ newSegment = segment;
+ }
+ else
+ {
+ var newArrayIndex = (SqlExpression)visitor.Visit(segment.ArrayIndex)!;
+ if (newArrayIndex == segment.ArrayIndex)
+ {
+ newSegment = segment;
+ }
+ else
+ {
+ newSegment = new PathSegment(newArrayIndex);
+
+ if (visitedPath is null)
+ {
+ visitedPath = new PathSegment[Path.Count];
+ for (var j = 0; j < i; i++)
+ {
+ visitedPath[j] = Path[j];
+ }
+ }
+ }
+ }
+
+ if (visitedPath is not null)
+ {
+ visitedPath[i] = newSegment;
+ }
+ }
+ }
+
+ return Update(visitedJsonExpression, visitedPath ?? Path);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual JsonEachExpression Update(
+ SqlExpression jsonExpression,
+ IReadOnlyList? path)
+ => jsonExpression == JsonExpression
+ && (ReferenceEquals(path, Path) || path is not null && Path is not null && path.SequenceEqual(Path))
+ ? this
+ : new JsonEachExpression(Alias, jsonExpression, path);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ // TODO: Deep clone, see #30982
+ public virtual TableExpressionBase Clone()
+ {
+ var clone = new JsonEachExpression(Alias, JsonExpression, Path);
+
+ foreach (var annotation in GetAnnotations())
+ {
+ clone.AddAnnotation(annotation.Name, annotation.Value);
+ }
+
+ return clone;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append(Name);
+ expressionPrinter.Append("(");
+ expressionPrinter.Visit(JsonExpression);
+
+ if (Path is not null)
+ {
+ expressionPrinter
+ .Append(", '")
+ .Append(string.Join(".", Path.Select(e => e.ToString())))
+ .Append("'");
+ }
+
+ expressionPrinter.Append(")");
+
+ PrintAnnotations(expressionPrinter);
+
+ expressionPrinter.Append(" AS ");
+ expressionPrinter.Append(Alias);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj) || (obj is JsonEachExpression jsonEachExpression && Equals(jsonEachExpression));
+
+ private bool Equals(JsonEachExpression other)
+ => base.Equals(other)
+ && (ReferenceEquals(Path, other.Path)
+ || (Path is not null && other.Path is not null && Path.SequenceEqual(other.Path)));
+
+ ///
+ public override int GetHashCode()
+ => base.GetHashCode();
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs
index bc11470d327..6e30ced00aa 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs
@@ -63,12 +63,15 @@ public virtual ISetSource GetExpectedData()
Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Name, aa.Name);
- AssertOwnedRoot(ee.OwnedReferenceRoot, aa.OwnedReferenceRoot);
-
- Assert.Equal(ee.OwnedCollectionRoot.Count, aa.OwnedCollectionRoot.Count);
- for (var i = 0; i < ee.OwnedCollectionRoot.Count; i++)
+ if (ee.OwnedReferenceRoot is not null || aa.OwnedReferenceRoot is not null)
{
- AssertOwnedRoot(ee.OwnedCollectionRoot[i], aa.OwnedCollectionRoot[i]);
+ AssertOwnedRoot(ee.OwnedReferenceRoot, aa.OwnedReferenceRoot);
+
+ Assert.Equal(ee.OwnedCollectionRoot.Count, aa.OwnedCollectionRoot.Count);
+ for (var i = 0; i < ee.OwnedCollectionRoot.Count; i++)
+ {
+ AssertOwnedRoot(ee.OwnedCollectionRoot[i], aa.OwnedCollectionRoot[i]);
+ }
}
}
}
@@ -294,7 +297,7 @@ public virtual ISetSource GetExpectedData()
},
}.ToDictionary(e => e.Key, e => (object)e.Value);
- private static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual)
+ public static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual)
{
Assert.Equal(expected.Name, actual.Name);
Assert.Equal(expected.Number, actual.Number);
@@ -307,7 +310,7 @@ private static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual
}
}
- private static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch actual)
+ public static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch actual)
{
Assert.Equal(expected.Date, actual.Date);
Assert.Equal(expected.Fraction, actual.Fraction);
@@ -322,7 +325,7 @@ private static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch
}
}
- private static void AssertOwnedLeaf(JsonOwnedLeaf expected, JsonOwnedLeaf actual)
+ public static void AssertOwnedLeaf(JsonOwnedLeaf expected, JsonOwnedLeaf actual)
=> Assert.Equal(expected.SomethingSomething, actual.SomethingSomething);
public static void AssertCustomNameRoot(JsonOwnedCustomNameRoot expected, JsonOwnedCustomNameRoot actual)
diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
index b31cf584ca8..26ba6f29b4f 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
@@ -683,28 +683,28 @@ public virtual Task Json_entity_backtracking(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_basic(bool async)
+ public virtual Task Json_collection_index_in_projection_basic(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[1]).AsNoTracking());
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAt(bool async)
+ public virtual Task Json_collection_ElementAt_in_projection(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1)).AsNoTracking());
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async)
+ public virtual Task Json_collection_ElementAtOrDefault_in_projection(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAtOrDefault(1)).AsNoTracking());
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_project_collection(bool async)
+ public virtual Task Json_collection_index_in_projection_project_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[1].OwnedCollectionBranch).AsNoTracking(),
@@ -712,7 +712,7 @@ public virtual Task Json_collection_element_access_in_projection_project_collect
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async)
+ public virtual Task Json_collection_ElementAt_project_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set()
@@ -722,7 +722,7 @@ public virtual Task Json_collection_element_access_in_projection_using_ElementAt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async)
+ public virtual Task Json_collection_ElementAtOrDefault_project_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set()
@@ -732,7 +732,7 @@ public virtual Task Json_collection_element_access_in_projection_using_ElementAt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_parameter(bool async)
+ public virtual Task Json_collection_index_in_projection_using_parameter(bool async)
{
var prm = 0;
@@ -743,7 +743,7 @@ public virtual Task Json_collection_element_access_in_projection_using_parameter
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_column(bool async)
+ public virtual Task Json_collection_index_in_projection_using_column(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[x.Id]).AsNoTracking());
@@ -753,7 +753,7 @@ private static int MyMethod(int value)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_in_projection_using_untranslatable_client_method(bool async)
+ public virtual async Task Json_collection_index_in_projection_using_untranslatable_client_method(bool async)
{
var message = (await Assert.ThrowsAsync(
() => AssertQuery(
@@ -767,7 +767,7 @@ public virtual async Task Json_collection_element_access_in_projection_using_unt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_in_projection_using_untranslatable_client_method2(bool async)
+ public virtual async Task Json_collection_index_in_projection_using_untranslatable_client_method2(bool async)
{
var message = (await Assert.ThrowsAsync(
() => AssertQuery(
@@ -781,7 +781,7 @@ public virtual async Task Json_collection_element_access_in_projection_using_unt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_outside_bounds(bool async)
+ public virtual Task Json_collection_index_outside_bounds(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[25]).AsNoTracking(),
@@ -789,7 +789,7 @@ public virtual Task Json_collection_element_access_outside_bounds(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_outside_bounds_with_property_access(bool async)
+ public virtual Task Json_collection_index_outside_bounds_with_property_access(bool async)
=> AssertQueryScalar(
async,
ss => ss.Set().OrderBy(x => x.Id).Select(x => (int?)x.OwnedCollectionRoot[25].Number),
@@ -797,7 +797,7 @@ public virtual Task Json_collection_element_access_outside_bounds_with_property_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested(bool async)
+ public virtual Task Json_collection_index_in_projection_nested(bool async)
{
var prm = 1;
@@ -808,7 +808,7 @@ public virtual Task Json_collection_element_access_in_projection_nested(bool asy
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_scalar(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_scalar(bool async)
{
var prm = 1;
@@ -819,7 +819,7 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_reference(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_reference(bool async)
{
var prm = 1;
@@ -830,7 +830,7 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_collection(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_collection(bool async)
{
var prm = 1;
@@ -846,7 +846,7 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_collection_anonymous_projection(bool async)
{
var prm = 1;
@@ -865,14 +865,14 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_constant(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_constant(bool async)
=> AssertQueryScalar(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[0].Name != "Foo").Select(x => x.Id));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_variable(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_variable(bool async)
{
var prm = 1;
@@ -883,7 +883,7 @@ public virtual Task Json_collection_element_access_in_predicate_using_variable(b
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_column(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_column(bool async)
=> AssertQuery(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[x.Id].Name == "e1_c2").Select(x => new { x.Id, x }),
@@ -897,7 +897,7 @@ public virtual Task Json_collection_element_access_in_predicate_using_column(boo
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_complex_expression1(bool async)
=> AssertQuery(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[x.Id == 1 ? 0 : 1].Name == "e1_c1").Select(x => new { x.Id, x }),
@@ -911,7 +911,7 @@ public virtual Task Json_collection_element_access_in_predicate_using_complex_ex
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_complex_expression2(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_complex_expression2(bool async)
=> AssertQuery(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[ss.Set().Max(x => x.Id)].Name == "e1_c2").Select(x => new { x.Id, x }),
@@ -925,14 +925,14 @@ public virtual Task Json_collection_element_access_in_predicate_using_complex_ex
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_ElementAt(bool async)
+ public virtual Task Json_collection_ElementAt_in_predicate(bool async)
=> AssertQueryScalar(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1).Name != "Foo").Select(x => x.Id));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ public virtual Task Json_collection_index_in_predicate_nested_mix(bool async)
{
var prm = 0;
@@ -944,7 +944,7 @@ public virtual Task Json_collection_element_access_in_predicate_nested_mix(bool
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_manual_Element_at_and_pushdown(bool async)
+ public virtual Task Json_collection_ElementAt_and_pushdown(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -955,101 +955,143 @@ public virtual Task Json_collection_element_access_manual_Element_at_and_pushdow
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative(bool async)
- {
- var prm = 0;
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot[prm].OwnedCollectionBranch.Select(xx => "Foo").ElementAt(0)
- })))).Message;
+ public virtual Task Json_collection_Any_with_predicate(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch.Any(b => b.OwnedReferenceLeaf.SomethingSomething == "e1_c2_c1_c1")));
+ // TODO: Need entries
- Assert.Equal(CoreStrings.TranslationFailed("""JsonQueryExpression(j.OwnedCollectionRoot, "[__prm_0].OwnedCollectionBranch")"""), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_Where_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(j =>
+ j.OwnedReferenceRoot.OwnedCollectionBranch
+ .Where(o => o.Date == new DateTime(2102, 1, 1))
+ .ElementAt(0).Fraction == 10.2M),
+ entryCount: 40);
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative2(bool async)
- {
- var prm = 0;
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot[prm + x.Id].OwnedCollectionBranch.Select(xx => x.Id).ElementAt(0)
- })))).Message;
+ public virtual Task Json_collection_Skip(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch
+ .Skip(1)
+ .ElementAt(0).OwnedReferenceLeaf.SomethingSomething == "e1_r_c2_r"),
+ entryCount: 40);
- Assert.Equal(CoreStrings.TranslationFailed("""JsonQueryExpression(j.OwnedCollectionRoot, "[(...)].OwnedCollectionBranch")"""), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_OrderByDescending_Skip_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch
+ .OrderByDescending(b => b.Date)
+ .Skip(1)
+ .ElementAt(0).OwnedReferenceLeaf.SomethingSomething == "e1_r_c1_r"),
+ entryCount: 40);
+ // If this test is failing because of DistinctAfterOrderByWithoutRowLimitingOperatorWarning, this is because EF warns/errors by
+ // default for Distinct after OrderBy (without Skip/Take); but you likely have a naturally-ordered JSON collection, where the
+ // ordering has been added by the provider as part of the collection translation.
+ // Consider overriding RelationalQueryableMethodTranslatingExpressionVisitor.IsNaturallyOrdered() to identify such naturally-ordered
+ // collections, exempting them from the warning.
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative3(bool async)
- {
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => x.OwnedReferenceRoot).ElementAt(0)
- })))).Message;
+ public virtual Task Json_collection_Distinct_Count_with_predicate(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch
+ .Distinct()
+ .Count(b => b.OwnedReferenceLeaf.SomethingSomething == "e1_r_c2_r") == 1),
+ entryCount: 40);
- Assert.Equal(CoreStrings.TranslationFailed("""JsonQueryExpression(j.OwnedCollectionRoot, "")"""), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_within_collection_Count(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedCollectionRoot.Any(c => c.OwnedCollectionBranch.Count == 2)),
+ entryCount: 40);
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative4(bool async)
+ public virtual async Task Json_collection_index_with_parameter_Select_ElementAt(bool async)
{
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => x.OwnedCollectionRoot).ElementAt(0)
- })))).Message;
+ var prm = 0;
- Assert.Equal(CoreStrings.TranslationFailed("""JsonQueryExpression(j.OwnedCollectionRoot, "")"""), message);
+ await AssertQuery(
+ async,
+ ss => ss.Set().Select(
+ x => new { x.Id, CollectionElement = x.OwnedCollectionRoot[prm].OwnedCollectionBranch.Select(xx => "Foo").ElementAt(0) }));
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative5(bool async)
+ public virtual async Task Json_collection_index_with_expression_Select_ElementAt(bool async)
{
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => new { xx.OwnedReferenceBranch }).ElementAt(0)
- })))).Message;
+ var prm = 0;
- Assert.Equal(CoreStrings.TranslationFailed("""JsonQueryExpression(j.OwnedCollectionRoot, "")"""), message);
+ await AssertQuery(
+ async,
+ ss => ss.Set().Select(
+ j => j.OwnedCollectionRoot[prm + j.Id].OwnedCollectionBranch
+ .Select(b => b.OwnedReferenceLeaf.SomethingSomething)
+ .ElementAt(0)),
+ ss => ss.Set().Select(
+ j => j.OwnedCollectionRoot.Count > prm + j.Id
+ ? j.OwnedCollectionRoot[prm + j.Id].OwnedCollectionBranch
+ .Select(b => b.OwnedReferenceLeaf.SomethingSomething)
+ .ElementAt(0)
+ : null));
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative6(bool async)
- {
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
+ public virtual async Task Json_collection_Select_entity_collection_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set()
+ .AsNoTracking()
+ .Select(x => x.OwnedCollectionRoot.Select(xx => xx.OwnedCollectionBranch).ElementAt(0)),
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Count, a.Count);
+ for (var i = 0; i < e.Count; i++)
{
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => new JsonEntityBasic { Id = x.Id }).ElementAt(0)
- })))).Message;
+ JsonQueryFixtureBase.AssertOwnedBranch(e[i], a[i]);
+ }
+ });
- Assert.Equal(CoreStrings.TranslationFailed("""JsonQueryExpression(j.OwnedCollectionRoot, "")"""), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_Select_entity_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set().AsNoTracking().Select(x =>
+ x.OwnedCollectionRoot.Select(xx => xx.OwnedReferenceBranch).ElementAt(0)));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_Select_entity_in_anonymous_object_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set().AsNoTracking().Select(x =>
+ x.OwnedCollectionRoot.Select(xx => new { xx.OwnedReferenceBranch }).ElementAt(0)));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set().Select(
+ x => x.OwnedCollectionRoot.Select(xx => new JsonEntityBasic { Id = x.Id }).ElementAt(0)));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -1126,7 +1168,7 @@ public virtual Task Json_projection_deduplication_with_collection_in_original_an
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public virtual Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1144,7 +1186,7 @@ public virtual Task Json_collection_element_access_in_projection_using_constant_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public virtual Task Json_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
var prm = 1;
@@ -1166,7 +1208,7 @@ public virtual Task Json_collection_element_access_in_projection_using_parameter
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_after_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public virtual Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1184,7 +1226,7 @@ public virtual Task Json_collection_after_collection_element_access_in_projectio
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_after_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public virtual Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
var prm = 1;
@@ -1206,7 +1248,7 @@ public virtual Task Json_collection_after_collection_element_access_in_projectio
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_when_owner_is_present_misc1(bool async)
+ public virtual Task Json_collection_index_in_projection_when_owner_is_present_misc1(bool async)
{
var prm = 1;
@@ -1228,7 +1270,7 @@ public virtual Task Json_collection_element_access_in_projection_when_owner_is_p
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_when_owner_is_present_misc2(bool async)
+ public virtual Task Json_collection_index_in_projection_when_owner_is_present_misc2(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1246,7 +1288,7 @@ public virtual Task Json_collection_element_access_in_projection_when_owner_is_p
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_when_owner_is_present_multiple(bool async)
+ public virtual Task Json_collection_index_in_projection_when_owner_is_present_multiple(bool async)
{
var prm = 1;
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index 3cf9f2bab6e..75541cdd0da 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -453,6 +453,19 @@ public virtual Task Column_collection_Any(bool async)
ss => ss.Set().Where(c => c.Ints.Any()),
entryCount: 2);
+ // If this test is failing because of DistinctAfterOrderByWithoutRowLimitingOperatorWarning, this is because EF warns/errors by
+ // default for Distinct after OrderBy (without Skip/Take); but you likely have a naturally-ordered JSON collection, where the
+ // ordering has been added by the provider as part of the collection translation.
+ // Consider overriding RelationalQueryableMethodTranslatingExpressionVisitor.IsNaturallyOrdered() to identify such naturally-ordered
+ // collections, exempting them from the warning.
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Distinct(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Distinct().Count() == 3),
+ entryCount: 1);
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_projection_from_top_level(bool async)
@@ -654,9 +667,6 @@ protected override string StoreName
public Func GetContextCreator()
=> () => CreateContext();
- public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => base.AddOptions(builder).ConfigureWarnings(c => c.Log(CoreEventId.DistinctAfterOrderByWithoutRowLimitingOperatorWarning));
-
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
=> modelBuilder.Entity().Property(p => p.Id).ValueGeneratedNever();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
index f057572bd58..e3dadfa10d2 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
@@ -604,9 +604,9 @@ public override async Task Json_entity_backtracking(bool async)
@"");
}
- public override async Task Json_collection_element_access_in_projection_basic(bool async)
+ public override async Task Json_collection_index_in_projection_basic(bool async)
{
- await base.Json_collection_element_access_in_projection_basic(async);
+ await base.Json_collection_index_in_projection_basic(async);
AssertSql(
"""
@@ -615,9 +615,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAt(bool async)
+ public override async Task Json_collection_ElementAt_in_projection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAt(async);
+ await base.Json_collection_ElementAt_in_projection(async);
AssertSql(
"""
@@ -626,9 +626,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async)
+ public override async Task Json_collection_ElementAtOrDefault_in_projection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault(async);
+ await base.Json_collection_ElementAtOrDefault_in_projection(async);
AssertSql(
"""
@@ -637,9 +637,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_project_collection(bool async)
+ public override async Task Json_collection_index_in_projection_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_project_collection(async);
+ await base.Json_collection_index_in_projection_project_collection(async);
AssertSql(
"""
@@ -648,9 +648,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async)
+ public override async Task Json_collection_ElementAt_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAt_project_collection(async);
+ await base.Json_collection_ElementAt_project_collection(async);
AssertSql(
"""
@@ -659,9 +659,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async)
+ public override async Task Json_collection_ElementAtOrDefault_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(async);
+ await base.Json_collection_ElementAtOrDefault_project_collection(async);
AssertSql(
"""
@@ -671,9 +671,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_using_parameter(bool async)
+ public override async Task Json_collection_index_in_projection_using_parameter(bool async)
{
- await base.Json_collection_element_access_in_projection_using_parameter(async);
+ await base.Json_collection_index_in_projection_using_parameter(async);
AssertSql(
"""
@@ -685,9 +685,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_using_column(bool async)
+ public override async Task Json_collection_index_in_projection_using_column(bool async)
{
- await base.Json_collection_element_access_in_projection_using_column(async);
+ await base.Json_collection_index_in_projection_using_column(async);
AssertSql(
"""
@@ -696,23 +696,23 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_untranslatable_client_method(bool async)
+ public override async Task Json_collection_index_in_projection_using_untranslatable_client_method(bool async)
{
- await base.Json_collection_element_access_in_projection_using_untranslatable_client_method(async);
+ await base.Json_collection_index_in_projection_using_untranslatable_client_method(async);
AssertSql();
}
- public override async Task Json_collection_element_access_in_projection_using_untranslatable_client_method2(bool async)
+ public override async Task Json_collection_index_in_projection_using_untranslatable_client_method2(bool async)
{
- await base.Json_collection_element_access_in_projection_using_untranslatable_client_method2(async);
+ await base.Json_collection_index_in_projection_using_untranslatable_client_method2(async);
AssertSql();
}
- public override async Task Json_collection_element_access_outside_bounds(bool async)
+ public override async Task Json_collection_index_outside_bounds(bool async)
{
- await base.Json_collection_element_access_outside_bounds(async);
+ await base.Json_collection_index_outside_bounds(async);
AssertSql(
"""
@@ -721,9 +721,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_outside_bounds_with_property_access(bool async)
+ public override async Task Json_collection_index_outside_bounds_with_property_access(bool async)
{
- await base.Json_collection_element_access_outside_bounds_with_property_access(async);
+ await base.Json_collection_index_outside_bounds_with_property_access(async);
AssertSql(
"""
@@ -734,9 +734,9 @@ ORDER BY [j].[Id]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested(bool async)
+ public override async Task Json_collection_index_in_projection_nested(bool async)
{
- await base.Json_collection_element_access_in_projection_nested(async);
+ await base.Json_collection_index_in_projection_nested(async);
AssertSql(
"""
@@ -748,9 +748,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_scalar(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_scalar(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_scalar(async);
+ await base.Json_collection_index_in_projection_nested_project_scalar(async);
AssertSql(
"""
@@ -762,9 +762,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_reference(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_reference(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_reference(async);
+ await base.Json_collection_index_in_projection_nested_project_reference(async);
AssertSql(
"""
@@ -776,9 +776,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_collection(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_collection(async);
+ await base.Json_collection_index_in_projection_nested_project_collection(async);
AssertSql(
"""
@@ -791,9 +791,9 @@ ORDER BY [j].[Id]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_collection_anonymous_projection(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(async);
+ await base.Json_collection_index_in_projection_nested_project_collection_anonymous_projection(async);
AssertSql(
"""
@@ -804,9 +804,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_predicate_using_constant(bool async)
+ public override async Task Json_collection_index_in_predicate_using_constant(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_constant(async);
+ await base.Json_collection_index_in_predicate_using_constant(async);
AssertSql(
"""
@@ -817,9 +817,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[0].Name') <> N'Foo' OR JSON_VALUE
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_variable(bool async)
+ public override async Task Json_collection_index_in_predicate_using_variable(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_variable(async);
+ await base.Json_collection_index_in_predicate_using_variable(async);
AssertSql(
"""
@@ -832,9 +832,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[' + CAST(@__prm_0 AS nvarchar(max
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_column(bool async)
+ public override async Task Json_collection_index_in_predicate_using_column(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_column(async);
+ await base.Json_collection_index_in_predicate_using_column(async);
AssertSql(
"""
@@ -845,9 +845,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[' + CAST([j].[Id] AS nvarchar(max
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async)
+ public override async Task Json_collection_index_in_predicate_using_complex_expression1(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_complex_expression1(async);
+ await base.Json_collection_index_in_predicate_using_complex_expression1(async);
AssertSql(
"""
@@ -861,9 +861,9 @@ END AS nvarchar(max)) + '].Name') = N'e1_c1'
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_complex_expression2(bool async)
+ public override async Task Json_collection_index_in_predicate_using_complex_expression2(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_complex_expression2(async);
+ await base.Json_collection_index_in_predicate_using_complex_expression2(async);
AssertSql(
"""
@@ -875,9 +875,9 @@ SELECT MAX([j0].[Id])
""");
}
- public override async Task Json_collection_element_access_in_predicate_using_ElementAt(bool async)
+ public override async Task Json_collection_ElementAt_in_predicate(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_ElementAt(async);
+ await base.Json_collection_ElementAt_in_predicate(async);
AssertSql(
"""
@@ -888,9 +888,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[1].Name') <> N'Foo' OR JSON_VALUE
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ public override async Task Json_collection_index_in_predicate_nested_mix(bool async)
{
- await base.Json_collection_element_access_in_predicate_nested_mix(async);
+ await base.Json_collection_index_in_predicate_nested_mix(async);
AssertSql(
"""
@@ -902,9 +902,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[1].OwnedCollectionBranch[' + CAST
""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown(bool async)
+ public override async Task Json_collection_ElementAt_and_pushdown(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown(async);
+ await base.Json_collection_ElementAt_and_pushdown(async);
AssertSql(
"""
@@ -913,46 +913,208 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative(bool async)
+ public override async Task Json_collection_Any_with_predicate(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative(async);
+ await base.Json_collection_Any_with_predicate(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE EXISTS (
+ SELECT 1
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') WITH (
+ [Date] datetime2 '$.Date',
+ [Enum] nvarchar(max) '$.Enum',
+ [Fraction] decimal(18,2) '$.Fraction',
+ [NullableEnum] nvarchar(max) '$.NullableEnum',
+ [OwnedCollectionLeaf] nvarchar(max) '$.OwnedCollectionLeaf' AS JSON,
+ [OwnedReferenceLeaf] nvarchar(max) '$.OwnedReferenceLeaf' AS JSON
+ ) AS [o]
+ WHERE JSON_VALUE([o].[OwnedReferenceLeaf], '$.SomethingSomething') = N'e1_c2_c1_c1')
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative2(bool async)
+ public override async Task Json_collection_Where_ElementAt(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative2(async);
+ await base.Json_collection_Where_ElementAt(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE (
+ SELECT CAST(JSON_VALUE([o].[value], '$.Fraction') AS decimal(18,2))
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') AS [o]
+ WHERE CAST(JSON_VALUE([o].[value], '$.Date') AS datetime2) = '2102-01-01T00:00:00.0000000'
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = 10.2
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative3(bool async)
+ public override async Task Json_collection_Skip(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative3(async);
+ await base.Json_collection_Skip(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE (
+ SELECT [t].[c]
+ FROM (
+ SELECT JSON_VALUE([o].[value], '$.OwnedReferenceLeaf.SomethingSomething') AS [c], [j].[Id], CAST([o].[key] AS int) AS [c0]
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') AS [o]
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [t]
+ ORDER BY [t].[c0]
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = N'e1_r_c2_r'
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative4(bool async)
+ public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative4(async);
+ await base.Json_collection_OrderByDescending_Skip_ElementAt(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE (
+ SELECT [t].[c]
+ FROM (
+ SELECT JSON_VALUE([o].[OwnedReferenceLeaf], '$.SomethingSomething') AS [c], [j].[Id], [o].[Date] AS [c0]
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') WITH (
+ [Date] datetime2 '$.Date',
+ [Enum] nvarchar(max) '$.Enum',
+ [Fraction] decimal(18,2) '$.Fraction',
+ [NullableEnum] nvarchar(max) '$.NullableEnum',
+ [OwnedCollectionLeaf] nvarchar(max) '$.OwnedCollectionLeaf' AS JSON,
+ [OwnedReferenceLeaf] nvarchar(max) '$.OwnedReferenceLeaf' AS JSON
+ ) AS [o]
+ ORDER BY [o].[Date] DESC
+ OFFSET 1 ROWS
+ ) AS [t]
+ ORDER BY [t].[c0] DESC
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = N'e1_r_c1_r'
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative5(bool async)
+ public override async Task Json_collection_Distinct_Count_with_predicate(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative5(async);
+ await base.Json_collection_Distinct_Count_with_predicate(async);
AssertSql();
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative6(bool async)
+ public override async Task Json_collection_within_collection_Count(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative6(async);
+ await base.Json_collection_within_collection_Count(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE EXISTS (
+ SELECT 1
+ FROM OPENJSON([j].[OwnedCollectionRoot], '$') WITH (
+ [Name] nvarchar(max) '$.Name',
+ [Number] int '$.Number',
+ [OwnedCollectionBranch] nvarchar(max) '$.OwnedCollectionBranch' AS JSON,
+ [OwnedReferenceBranch] nvarchar(max) '$.OwnedReferenceBranch' AS JSON
+ ) AS [o]
+ WHERE (
+ SELECT COUNT(*)
+ FROM OPENJSON([o].[OwnedCollectionBranch], '$') WITH (
+ [Date] datetime2 '$.Date',
+ [Enum] nvarchar(max) '$.Enum',
+ [Fraction] decimal(18,2) '$.Fraction',
+ [NullableEnum] nvarchar(max) '$.NullableEnum',
+ [OwnedCollectionLeaf] nvarchar(max) '$.OwnedCollectionLeaf' AS JSON,
+ [OwnedReferenceLeaf] nvarchar(max) '$.OwnedReferenceLeaf' AS JSON
+ ) AS [o0]) = 2)
+""");
+ }
+
+ public override async Task Json_collection_index_with_parameter_Select_ElementAt(bool async)
+ {
+ await base.Json_collection_index_with_parameter_Select_ElementAt(async);
+
+ AssertSql(
+"""
+@__prm_0='0'
+
+SELECT [j].[Id], (
+ SELECT N'Foo'
+ FROM OPENJSON([j].[OwnedCollectionRoot], '$[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionBranch') AS [o]
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [CollectionElement]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_index_with_expression_Select_ElementAt(bool async)
+ {
+ await base.Json_collection_index_with_expression_Select_ElementAt(async);
+
+ AssertSql(
+"""
+@__prm_0='0'
+
+SELECT JSON_VALUE([j].[OwnedCollectionRoot], '$[' + CAST(@__prm_0 + [j].[Id] AS nvarchar(max)) + '].OwnedCollectionBranch[0].OwnedReferenceLeaf.SomethingSomething')
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_collection_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_collection_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot], '$[0].OwnedCollectionBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot], '$[0].OwnedReferenceBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_in_anonymous_object_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_in_anonymous_object_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot], '$[0].OwnedReferenceBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_with_initializer_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT [t].[Id], [t].[c]
+FROM [JsonEntitiesBasic] AS [j]
+OUTER APPLY (
+ SELECT [j].[Id], 1 AS [c]
+ FROM OPENJSON([j].[OwnedCollectionRoot], '$') AS [o]
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
+) AS [t]
+""");
}
public override async Task Json_projection_deduplication_with_collection_indexer_in_original(bool async)
@@ -993,9 +1155,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public override async Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
{
- await base.Json_collection_element_access_in_projection_using_constant_when_owner_is_present(async);
+ await base.Json_collection_index_in_projection_using_constant_when_owner_is_present(async);
AssertSql(
"""
@@ -1004,9 +1166,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public override async Task Json_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
- await base.Json_collection_element_access_in_projection_using_parameter_when_owner_is_present(async);
+ await base.Json_collection_index_in_projection_using_parameter_when_owner_is_present(async);
AssertSql(
"""
@@ -1017,9 +1179,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_after_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public override async Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
{
- await base.Json_collection_after_collection_element_access_in_projection_using_constant_when_owner_is_present(async);
+ await base.Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(async);
AssertSql(
"""
@@ -1028,9 +1190,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_after_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public override async Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
- await base.Json_collection_after_collection_element_access_in_projection_using_parameter_when_owner_is_present(async);
+ await base.Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(async);
AssertSql(
"""
@@ -1041,9 +1203,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_when_owner_is_present_misc1(bool async)
+ public override async Task Json_collection_index_in_projection_when_owner_is_present_misc1(bool async)
{
- await base.Json_collection_element_access_in_projection_when_owner_is_present_misc1(async);
+ await base.Json_collection_index_in_projection_when_owner_is_present_misc1(async);
AssertSql(
"""
@@ -1054,9 +1216,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_when_owner_is_present_misc2(bool async)
+ public override async Task Json_collection_index_in_projection_when_owner_is_present_misc2(bool async)
{
- await base.Json_collection_element_access_in_projection_when_owner_is_present_misc2(async);
+ await base.Json_collection_index_in_projection_when_owner_is_present_misc2(async);
AssertSql(
"""
@@ -1065,9 +1227,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_when_owner_is_present_multiple(bool async)
+ public override async Task Json_collection_index_in_projection_when_owner_is_present_multiple(bool async)
{
- await base.Json_collection_element_access_in_projection_when_owner_is_present_multiple(async);
+ await base.Json_collection_index_in_projection_when_owner_is_present_multiple(async);
AssertSql(
"""
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index 8c9675236a4..6f0774444fb 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -394,6 +394,9 @@ public override Task Column_collection_OrderByDescending_ElementAt(bool async)
public override Task Column_collection_Any(bool async)
=> AssertTranslationFailed(() => base.Column_collection_Any(async));
+ public override Task Column_collection_Distinct(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Distinct(async));
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
index b3993a6484d..7cd4defc5bd 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -659,6 +659,23 @@ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i])
""");
}
+ public override async Task Column_collection_Distinct(bool async)
+ {
+ await base.Column_collection_Distinct(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT DISTINCT [i].[value]
+ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
+ ) AS [t]) = 3
+""");
+ }
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
index 7d73ffcec7a..2964f93c76c 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
@@ -67,6 +67,95 @@ public override async Task Project_json_entity_FirstOrDefault_subquery_deduplica
() => base.Project_json_entity_FirstOrDefault_subquery_deduplication_outer_reference_and_pruning(async)))
.Message);
+ public override async Task Json_collection_Any_with_predicate(bool async)
+ {
+ await base.Json_collection_Any_with_predicate(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE EXISTS (
+ SELECT 1
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ WHERE "t"."OwnedReferenceLeaf" ->> 'SomethingSomething' = 'e1_c2_c1_c1')
+""");
+ }
+
+ public override async Task Json_collection_Skip(bool async)
+ {
+ await base.Json_collection_Skip(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE (
+ SELECT "t0"."c"
+ FROM (
+ SELECT "t"."OwnedReferenceLeaf" ->> 'SomethingSomething' AS "c", "j"."Id", "t"."key"
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ ORDER BY "t"."key"
+ LIMIT -1 OFFSET 1
+ ) AS "t0"
+ ORDER BY "t0"."key"
+ LIMIT 1 OFFSET 0) = 'e1_r_c2_r'
+""");
+ }
+
+ public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool async)
+ {
+ await base.Json_collection_OrderByDescending_Skip_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE (
+ SELECT "t0"."c"
+ FROM (
+ SELECT "t"."OwnedReferenceLeaf" ->> 'SomethingSomething' AS "c", "j"."Id", "t"."Date" AS "c0"
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ ORDER BY "t"."Date" DESC
+ LIMIT -1 OFFSET 1
+ ) AS "t0"
+ ORDER BY "t0"."c0" DESC
+ LIMIT 1 OFFSET 0) = 'e1_r_c1_r'
+""");
+ }
+
+ public override async Task Json_collection_within_collection_Count(bool async)
+ {
+ await base.Json_collection_within_collection_Count(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE EXISTS (
+ SELECT 1
+ FROM (
+ SELECT "o"."value" ->> 'Name' AS "Name", "o"."value" ->> 'Number' AS "Number", "o"."value" ->> 'OwnedCollectionBranch' AS "OwnedCollectionBranch", "o"."value" ->> 'OwnedReferenceBranch' AS "OwnedReferenceBranch", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedCollectionRoot", '$') AS "o"
+ ) AS "t"
+ WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT "o0"."value" ->> 'Date' AS "Date", "o0"."value" ->> 'Enum' AS "Enum", "o0"."value" ->> 'Fraction' AS "Fraction", "o0"."value" ->> 'NullableEnum' AS "NullableEnum", "o0"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o0"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o0"."key"
+ FROM json_each("t"."OwnedCollectionBranch", '$') AS "o0"
+ ) AS "t0") = 2)
+""");
+ }
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_on_entity_with_json_with_predicate(bool async)
@@ -90,9 +179,9 @@ await AssertQuery(
""");
}
- public override async Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ public override async Task Json_collection_index_in_predicate_nested_mix(bool async)
{
- await base.Json_collection_element_access_in_predicate_nested_mix(async);
+ await base.Json_collection_index_in_predicate_nested_mix(async);
AssertSql(
"""
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
index 5cdad15f18e..36b4e065466 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
@@ -645,6 +645,23 @@ WHERE json_array_length("p"."Ints") > 0
""");
}
+ public override async Task Column_collection_Distinct(bool async)
+ {
+ await base.Column_collection_Distinct(async);
+
+ AssertSql(
+"""
+SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings"
+FROM "PrimitiveCollectionsEntity" AS "p"
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT DISTINCT "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ ) AS "t") = 3
+""");
+ }
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);