diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index b7ef50f00fa..88f4c3c55fd 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -1407,6 +1407,12 @@ public static string ParameterNotObjectArray(object? parameter)
public static string PendingAmbientTransaction
=> GetString("PendingAmbientTransaction");
+ ///
+ /// The query projects an entity mapped to JSON and accesses a JSON collection element. Such queries require 'AsNoTracking' option, even when the parent entity is projected.
+ ///
+ public static string ProjectingJsonCollectionElementRequiresNoTracking
+ => GetString("ProjectingJsonCollectionElementRequiresNoTracking");
+
///
/// Unable to translate set operations when both sides don't assign values to the same properties in the nominal type. Please make sure that the same properties are included on both sides, and consider assigning default values if a property doesn't require a specific value.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index d10d59b928c..68f6c2b6989 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -950,6 +950,9 @@
This connection was used with an ambient transaction. The original ambient transaction needs to be completed before this connection can be used outside of it.
+
+ The query projects an entity mapped to JSON and accesses a JSON collection element. Such queries require 'AsNoTracking' option, even when the parent entity is projected.
+
Unable to translate set operations when both sides don't assign values to the same properties in the nominal type. Please make sure that the same properties are included on both sides, and consider assigning default values if a property doesn't require a specific value.
diff --git a/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs
new file mode 100644
index 00000000000..34a35d0ca33
--- /dev/null
+++ b/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+internal class CollectionIndexerToElementAtConvertingExpressionVisitor : ExpressionVisitor
+{
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ // Convert list[x] to list.ElementAt(x)
+ if (methodCallExpression.Method is { Name: "get_Item", IsStatic: false, DeclaringType: { IsGenericType: true } declaringType }
+ && declaringType.GetGenericTypeDefinition() == typeof(List<>)
+ && ShouldConvert(methodCallExpression.Type))
+ {
+ var source = Visit(methodCallExpression.Object!);
+ var index = Visit(methodCallExpression.Arguments[0]);
+ var sourceTypeArgument = source.Type.GetSequenceType();
+
+ return Expression.Call(
+ QueryableMethods.ElementAt.MakeGenericMethod(sourceTypeArgument),
+ Expression.Call(
+ QueryableMethods.AsQueryable.MakeGenericMethod(sourceTypeArgument),
+ source),
+ index);
+ }
+
+ return base.VisitMethodCall(methodCallExpression);
+ }
+
+ protected override Expression VisitBinary(BinaryExpression binaryExpression)
+ {
+ // Convert array[x] to list.ElementAt(x)
+ if (binaryExpression.NodeType == ExpressionType.ArrayIndex
+ && ShouldConvert(binaryExpression.Type))
+ {
+ var source = Visit(binaryExpression.Left);
+ var index = Visit(binaryExpression.Right);
+ var sourceTypeArgument = source.Type.GetSequenceType();
+
+ return Expression.Call(
+ QueryableMethods.ElementAt.MakeGenericMethod(sourceTypeArgument),
+ Expression.Call(
+ QueryableMethods.AsQueryable.MakeGenericMethod(sourceTypeArgument),
+ source),
+ index);
+ }
+
+ return base.VisitBinary(binaryExpression);
+ }
+
+ private static bool ShouldConvert(Type type)
+ => !type.IsPrimitive && !type.IsEnum && type != typeof(object) && type != typeof(string);
+}
diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs
index d646b593216..40893b7ce89 100644
--- a/src/EFCore.Relational/Query/JsonQueryExpression.cs
+++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs
@@ -161,6 +161,30 @@ public virtual JsonQueryExpression BindNavigation(INavigation navigation)
IsNullable || !navigation.ForeignKey.IsRequiredDependent);
}
+ ///
+ /// Binds a collection element access with this JSON query expression to get the SQL representation.
+ ///
+ /// The collection index to bind.
+ public virtual JsonQueryExpression BindCollectionElement(SqlExpression collectionIndexExpression)
+ {
+ Debug.Assert(
+ Path.Last().ArrayIndex == null,
+ "Already accessing JSON array element.");
+
+ var newPath = Path.ToList();
+ newPath.Add(new PathSegment(collectionIndexExpression));
+
+ return new JsonQueryExpression(
+ EntityType,
+ JsonColumn,
+ _keyPropertyMap,
+ newPath,
+ EntityType.ClrType,
+ collection: false,
+ // TODO: is this the right nullable?
+ nullable: true);
+ }
+
///
/// Makes this JSON query expression nullable.
///
diff --git a/src/EFCore.Relational/Query/PathSegment.cs b/src/EFCore.Relational/Query/PathSegment.cs
index e251ea2fc01..7a9529c2611 100644
--- a/src/EFCore.Relational/Query/PathSegment.cs
+++ b/src/EFCore.Relational/Query/PathSegment.cs
@@ -17,22 +17,40 @@ namespace Microsoft.EntityFrameworkCore.Query;
public class PathSegment
{
///
- /// Creates a new instance of the class.
+ /// Creates a new instance of the class representing JSON property access.
///
- /// A key which is being accessed in the JSON.
- public PathSegment(string key)
+ /// A name of JSON property which is being accessed.
+ public PathSegment(string propertyName)
{
- Key = key;
+ PropertyName = propertyName;
+ ArrayIndex = null;
}
///
- /// The key which is being accessed in the JSON.
+ /// Creates a new instance of the class representing JSON array element access.
///
- public virtual string Key { get; }
+ /// An index of an element which is being accessed in the JSON array.
+ public PathSegment(SqlExpression arrayIndex)
+ {
+ ArrayIndex = arrayIndex;
+ PropertyName = null;
+ }
+
+ ///
+ /// The name of JSON property which is being accessed.
+ ///
+ public virtual string? PropertyName { get; }
+
+ ///
+ /// The index of an element which is being accessed in the JSON array.
+ ///
+ public virtual SqlExpression? ArrayIndex { get; }
///
public override string ToString()
- => (Key == "$" ? "" : ".") + Key;
+ => (PropertyName == "$" ? "" : ".")
+ + (PropertyName == null ? "" : PropertyName)
+ + (ArrayIndex == null ? "" : $"[{ArrayIndex}]");
///
public override bool Equals(object? obj)
@@ -42,9 +60,11 @@ public override bool Equals(object? obj)
&& Equals(pathSegment));
private bool Equals(PathSegment pathSegment)
- => Key == pathSegment.Key;
+ => PropertyName == pathSegment.PropertyName
+ && ((ArrayIndex == null && pathSegment.ArrayIndex == null)
+ || (ArrayIndex != null && ArrayIndex.Equals(pathSegment.ArrayIndex)));
///
public override int GetHashCode()
- => HashCode.Combine(Key);
+ => HashCode.Combine(PropertyName, ArrayIndex);
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
index 42e5416464b..1a5cb9783fc 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
@@ -37,6 +37,7 @@ public override Expression NormalizeQueryableMethod(Expression expression)
expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression);
expression = base.NormalizeQueryableMethod(expression);
expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression);
+ expression = new CollectionIndexerToElementAtConvertingExpressionVisitor().Visit(expression);
return expression;
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 7c0e5934361..6b32bb4f01a 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -1641,6 +1641,62 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
?? methodCallExpression.Update(null!, new[] { source, methodCallExpression.Arguments[1] });
}
+ if (methodCallExpression.Method.IsGenericMethod
+ && (methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.ElementAt
+ || methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.ElementAtOrDefault))
+ {
+ source = methodCallExpression.Arguments[0];
+ var selectMethodCallExpression = default(MethodCallExpression);
+
+ if (source is MethodCallExpression sourceMethodCall
+ && sourceMethodCall.Method.IsGenericMethod
+ && sourceMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.Select)
+ {
+ selectMethodCallExpression = sourceMethodCall;
+ source = sourceMethodCall.Arguments[0];
+ }
+
+ if (source is MethodCallExpression asQueryableMethodCall
+ && asQueryableMethodCall.Method.IsGenericMethod
+ && asQueryableMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable)
+ {
+ source = asQueryableMethodCall.Arguments[0];
+ }
+
+ source = Visit(source);
+
+ if (source is JsonQueryExpression jsonQueryExpression)
+ {
+ var collectionIndexExpression = _sqlTranslator.Translate(methodCallExpression.Arguments[1]!);
+
+ if (collectionIndexExpression == null)
+ {
+ return methodCallExpression.Update(null!, new[] { source, methodCallExpression.Arguments[1] });
+ }
+
+ collectionIndexExpression = _sqlExpressionFactory.ApplyDefaultTypeMapping(collectionIndexExpression);
+ var newJsonQuery = jsonQueryExpression.BindCollectionElement(collectionIndexExpression!);
+
+ var entityShaper = new RelationalEntityShaperExpression(
+ jsonQueryExpression.EntityType,
+ newJsonQuery,
+ nullable: true);
+
+ if (selectMethodCallExpression != null)
+ {
+ var selectorLambda = selectMethodCallExpression.Arguments[1].UnwrapLambdaFromQuote();
+ var replaced = new ReplacingExpressionVisitor(new[] { selectorLambda.Parameters[0] }, new[] { entityShaper })
+ .Visit(selectorLambda.Body);
+
+ var result = Visit(replaced);
+
+ return result;
+ }
+
+ return entityShaper;
+ }
+ }
+
return base.VisitMethodCall(methodCallExpression);
}
diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
index 92736474a3c..3bab5a75895 100644
--- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
@@ -416,9 +416,14 @@ protected override Expression VisitExtension(Expression extensionExpression)
{
if (!_variableShaperMapping.TryGetValue(entityShaperExpression.ValueBufferExpression, out var accessor))
{
- if (GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[]>
+ if (GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[], int>
jsonProjectionIndex)
{
+ if (_isTracking && jsonProjectionIndex.Item4 > 0)
+ {
+ throw new InvalidOperationException(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking);
+ }
+
// json entity at the root
var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess(
jsonProjectionIndex,
@@ -510,8 +515,13 @@ protected override Expression VisitExtension(Expression extensionExpression)
case CollectionResultExpression collectionResultExpression
when collectionResultExpression.Navigation is INavigation navigation
&& GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression)
- is ValueTuple, string[]> jsonProjectionIndex:
+ is ValueTuple, string[], int> jsonProjectionIndex:
{
+ if (_isTracking && jsonProjectionIndex.Item4 > 0)
+ {
+ throw new InvalidOperationException(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking);
+ }
+
// json entity collection at the root
var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess(
jsonProjectionIndex,
@@ -781,7 +791,7 @@ when collectionResultExpression.Navigation is INavigation navigation
// json include case
if (projectionBindingExpression != null
- && GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[]>
+ && GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[], int>
jsonProjectionIndex)
{
var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess(
@@ -1236,16 +1246,17 @@ private Expression CreateJsonShapers(
}
private (ParameterExpression, ParameterExpression) JsonShapingPreProcess(
- ValueTuple, string[]> projectionIndex,
+ ValueTuple, string[], int> projectionIndex,
IEntityType entityType,
bool isCollection)
{
var jsonColumnProjectionIndex = projectionIndex.Item1;
var keyInfo = projectionIndex.Item2;
var additionalPath = projectionIndex.Item3;
+ var specifiedCollectionIndexesCount = projectionIndex.Item4;
var keyValuesParameter = Expression.Parameter(typeof(object[]));
- var keyValues = new Expression[keyInfo.Count];
+ var keyValues = new Expression[keyInfo.Count + specifiedCollectionIndexesCount];
for (var i = 0; i < keyInfo.Count; i++)
{
@@ -1262,6 +1273,12 @@ private Expression CreateJsonShapers(
typeof(object));
}
+ for (var i = 0; i < specifiedCollectionIndexesCount; i++)
+ {
+ keyValues[keyInfo.Count + i] = Expression.Convert(
+ Expression.Constant(0), typeof(object));
+ }
+
var keyValuesInitialize = Expression.NewArrayInit(typeof(object), keyValues);
var keyValuesAssignment = Expression.Assign(keyValuesParameter, keyValuesInitialize);
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index bf688dac9ec..43198241753 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -1529,15 +1529,15 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express
remappedConstant = Constant(newDictionary);
}
- else if (constantValue is ValueTuple>, string[]> tuple)
+ else if (constantValue is ValueTuple, string[], int> tuple)
{
- var newList = new List>();
+ var newList = new List<(IProperty, int)>();
foreach (var item in tuple.Item2)
{
newList.Add((item.Item1, projectionIndexMap[item.Item2]));
}
- remappedConstant = Constant((projectionIndexMap[tuple.Item1], newList, tuple.Item3));
+ remappedConstant = Constant((projectionIndexMap[tuple.Item1], newList, tuple.Item3, tuple.Item4));
}
else
{
@@ -1590,7 +1590,8 @@ static Dictionary BuildJsonProjection
{
var ordered = projections
.OrderBy(x => $"{x.JsonColumn.TableAlias}.{x.JsonColumn.Name}")
- .ThenBy(x => x.Path.Count);
+ .ThenBy(x => x.Path.Count)
+ .ThenBy(x => x.Path.Last().ArrayIndex != null);
var needed = new List();
foreach (var orderedElement in ordered)
@@ -1638,12 +1639,15 @@ ConstantExpression AddEntityProjection(EntityProjectionExpression entityProjecti
ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression, JsonScalarExpression jsonScalarToAdd)
{
- var additionalPath = new string[0];
+ var additionalPath = Array.Empty();
+
+ // TODO: change this when implementing #29513
+ // deduplication doesn't happen currently if the additional path contains indexes
+ Debug.Assert(jsonQueryExpression.Path.Skip(jsonScalarToAdd.Path.Count).All(x => x.PropertyName != null));
- // this will be more tricky once we support more complicated json path options
additionalPath = jsonQueryExpression.Path
.Skip(jsonScalarToAdd.Path.Count)
- .Select(x => x.Key)
+ .Select(x => x.PropertyName!)
.ToArray();
var jsonColumnIndex = AddToProjection(jsonScalarToAdd);
@@ -1656,7 +1660,9 @@ ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression, Js
keyInfo.Add((keyProperty, AddToProjection(keyColumn)));
}
- return Constant((jsonColumnIndex, keyInfo, additionalPath));
+ var specifiedCollectionIndexesCount = jsonScalarToAdd.Path.Count(x => x.ArrayIndex != null);
+
+ return Constant((jsonColumnIndex, keyInfo, additionalPath, specifiedCollectionIndexesCount));
}
static IReadOnlyList GetMappedKeyProperties(IKey key)
@@ -1697,7 +1703,15 @@ static bool JsonEntityContainedIn(JsonScalarExpression sourceExpression, JsonQue
return false;
}
- return sourcePath.SequenceEqual(targetPath.Take(sourcePath.Count));
+ if (!sourcePath.SequenceEqual(targetPath.Take(sourcePath.Count)))
+ {
+ return false;
+ }
+
+ // we can only perform deduplication if there additional path doesn't contain any collection indexes
+ // collection indexes can only be part of the source path
+ // see issue #29513
+ return targetPath.Skip(sourcePath.Count).All(x => x.ArrayIndex == null);
}
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index bfac93e291b..ec08ad38f62 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -321,7 +321,36 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
Visit(jsonScalarExpression.JsonColumn);
- Sql.Append($",'{string.Join("", jsonScalarExpression.Path.Select(e => e.ToString()))}')");
+ Sql.Append(",'");
+ foreach (var pathSegment in jsonScalarExpression.Path)
+ {
+ if (pathSegment.PropertyName != null)
+ {
+ Sql.Append((pathSegment.PropertyName == "$" ? "" : ".") + pathSegment.PropertyName);
+ }
+
+ if (pathSegment.ArrayIndex != null)
+ {
+ Sql.Append("[");
+
+ if (pathSegment.ArrayIndex is SqlConstantExpression)
+ {
+ Visit(pathSegment.ArrayIndex);
+ }
+ else
+ {
+ Sql.Append("' + CAST(");
+ Visit(pathSegment.ArrayIndex);
+ Sql.Append(" AS ");
+ Sql.Append(_typeMappingSource.GetMapping(typeof(string)).StoreType);
+ Sql.Append(") + '");
+ }
+
+ Sql.Append("]");
+ }
+ }
+
+ Sql.Append("')");
if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
{
diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
index dc1ff2e3076..f629ffcbcf2 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
@@ -112,7 +112,8 @@ public virtual Task Basic_json_projection_enum_inside_json_entity(bool async)
ss => ss.Set().Select(
x => new
{
- x.Id, x.OwnedReferenceRoot.OwnedReferenceBranch.Enum,
+ x.Id,
+ x.OwnedReferenceRoot.OwnedReferenceBranch.Enum,
}),
elementSorter: e => e.Id,
elementAsserter: (e, a) =>
@@ -632,15 +633,326 @@ public virtual Task Json_entity_backtracking(bool async)
public virtual Task Json_collection_element_access_in_projection_basic(bool async)
=> AssertQuery(
async,
- ss => ss.Set().Select(x => x.OwnedCollectionRoot[0]).AsNoTracking());
+ 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)
+ => 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)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAtOrDefault(1)).AsNoTracking());
- [ConditionalTheory(Skip = "issue #28648")]
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_project_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot[1].OwnedCollectionBranch).AsNoTracking(),
+ elementAsserter: (e, a) => AssertCollection(e, a, ordered: true));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1).OwnedCollectionBranch)
+ .AsNoTracking(),
+ elementAsserter: (e, a) => AssertCollection(e, a, ordered: true));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAtOrDefault(1).OwnedCollectionBranch)
+ .AsNoTracking(),
+ elementAsserter: (e, a) => AssertCollection(e, a, ordered: true));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_using_parameter(bool async)
+ {
+ var prm = 0;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot[prm]).AsNoTracking());
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_using_column(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot[x.Id]).AsNoTracking());
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_nested(bool async)
+ {
+ var prm = 1;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm]).AsNoTracking());
+ }
+
+ [ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate(bool async)
+ public virtual Task Json_collection_element_access_in_projection_nested_project_scalar(bool async)
+ {
+ var prm = 1;
+
+ return AssertQueryScalar(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].Date));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_nested_project_reference(bool async)
+ {
+ var prm = 1;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].OwnedReferenceLeaf).AsNoTracking());
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_nested_project_collection(bool async)
+ {
+ var prm = 1;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set()
+ .OrderBy(x => x.Id)
+ .Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].OwnedCollectionLeaf)
+ .AsNoTracking(),
+ assertOrder: true,
+ elementAsserter: (e, a) => AssertCollection(e, a, ordered: true));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async)
+ {
+ var prm = 1;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set()
+ .Select(x => new { x.Id, x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].OwnedCollectionLeaf })
+ .AsNoTracking(),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Id, a.Id);
+ AssertCollection(e.OwnedCollectionLeaf, a.OwnedCollectionLeaf, ordered: true);
+ });
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_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)
+ {
+ var prm = 1;
+
+ return AssertQueryScalar(
+ async,
+ ss => ss.Set().Where(x => x.OwnedCollectionRoot[prm].Name != "Foo").Select(x => x.Id));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_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 }),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Id, a.Id);
+ AssertEqual(e.x, a.x);
+ },
+ entryCount: 40);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_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 }),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Id, a.Id);
+ AssertEqual(e.x, a.x);
+ },
+ entryCount: 40);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_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 }),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Id, a.Id);
+ AssertEqual(e.x, a.x);
+ },
+ entryCount: 40);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_element_access_in_predicate_using_ElementAt(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)
+ {
+ var prm = 0;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(x => x.OwnedCollectionRoot[1].OwnedCollectionBranch[prm].OwnedCollectionLeaf[x.Id - 1].SomethingSomething == "e1_c2_c1_c1"),
+ entryCount: 40);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_projection_deduplication_with_collection_indexer_in_original(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => new
+ {
+ x.Id,
+ Duplicate1 = x.OwnedCollectionRoot[0].OwnedReferenceBranch,
+ Original = x.OwnedCollectionRoot[0],
+ Duplicate2 = x.OwnedCollectionRoot[0].OwnedReferenceBranch.OwnedCollectionLeaf
+ }).AsNoTracking(),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ AssertEqual(e.Id, a.Id);
+ AssertEqual(e.Original, a.Original);
+ AssertEqual(e.Duplicate1, a.Duplicate1);
+ AssertCollection(e.Duplicate2, a.Duplicate2, ordered: true);
+ });
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_projection_deduplication_with_collection_indexer_in_target(bool async)
+ {
+ var prm = 1;
+
+ // issue #29513
+ return AssertQuery(
+ async,
+ ss => ss.Set().Select(x => new
+ {
+ x.Id,
+ Duplicate1 = x.OwnedReferenceRoot.OwnedCollectionBranch[1],
+ Original = x.OwnedReferenceRoot,
+ Duplicate2 = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf[prm]
+ }).AsNoTracking(),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ AssertEqual(e.Id, a.Id);
+ AssertEqual(e.Original, a.Original);
+ AssertEqual(e.Duplicate1, a.Duplicate1);
+ AssertEqual(e.Duplicate2, a.Duplicate2);
+ });
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(bool async)
+ // issue #29513
+ => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => new
+ {
+ x.Id,
+ Original = x.OwnedReferenceRoot.OwnedCollectionBranch,
+ Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[1],
+ }).AsNoTracking(),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ AssertEqual(e.Id, a.Id);
+ AssertCollection(e.Original, a.Original, ordered: true);
+ AssertEqual(e.Duplicate, a.Duplicate);
+ });
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(bool async)
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => new
+ {
+ x,
+ CollectionElement = x.OwnedCollectionRoot[1]
+ }),
+ elementSorter: e => e.x.Id,
+ elementAsserter: (e, a) =>
+ {
+ AssertEqual(e.x, a.x);
+ AssertEqual(e.CollectionElement, a.CollectionElement);
+ }));
+
+ Assert.Equal(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking, exception.Message);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present2(bool async)
+ {
+ var message = (await Assert.ThrowsAsync(
+ () => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => new
+ {
+ x,
+ CollectionElement = x.OwnedCollectionRoot[1].OwnedReferenceBranch
+ }),
+ elementSorter: e => e.x.Id,
+ elementAsserter: (e, a) =>
+ {
+ AssertEqual(e.x, a.x);
+ AssertEqual(e.CollectionElement, a.CollectionElement);
+ }))).Message;
+
+ Assert.Equal(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking, message);
+ }
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Json_scalar_required_null_semantics(bool async)
@@ -667,6 +979,15 @@ public virtual Task Group_by_on_json_scalar(bool async)
ss => ss.Set()
.GroupBy(x => x.OwnedReferenceRoot.Name).Select(x => new { x.Key, Count = x.Count() }));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Group_by_on_json_scalar_using_collection_indexer(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .GroupBy(x => x.OwnedCollectionRoot[0].Name).Select(x => new { x.Key, Count = x.Count() }));
+
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Group_by_First_on_json_scalar(bool async)
diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
index adb2a61365c..2cf7861456f 100644
--- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
@@ -8224,6 +8224,20 @@ public virtual Task Where_subquery_with_ElementAt_using_column_as_index(bool asy
ss => ss.Set().Where(s => s.Members.OrderBy(m => m.Nickname).ElementAt(s.Id).Nickname == "Cole Train"),
ss => ss.Set().Where(s => s.Members.OrderBy(m => m.Nickname).ElementAtOrDefault(s.Id).Nickname == "Cole Train"));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Using_indexer_on_byte_array_and_string_in_projection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Select(x => new { x.Id, ByteArray = x.Banner[0], String = x.Name[1] }),
+ elementSorter: e => e.Id,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Id, a.Id);
+ Assert.Equal(e.ByteArray, a.ByteArray);
+ Assert.Equal(e.String, a.String);
+ });
+
protected GearsOfWarContext CreateContext()
=> Fixture.CreateContext();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
index b5d77fa52af..97fc8406506 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
@@ -10032,6 +10032,17 @@ ORDER BY [g].[Nickname]
""");
}
+ public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async)
+ {
+ await base.Using_indexer_on_byte_array_and_string_in_projection(async);
+
+ AssertSql(
+"""
+SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name]
+FROM [Squads] AS [s]
+""");
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
index 2470fcd8b92..e65455b147a 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
@@ -584,20 +584,311 @@ public override async Task Json_collection_element_access_in_projection_basic(bo
{
await base.Json_collection_element_access_in_projection_basic(async);
- // array element access in projection is currently done on the client - issue 28648
AssertSql(
"""
-SELECT [j].[OwnedCollectionRoot], [j].[Id]
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1]'), [j].[Id]
FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_predicate(bool async)
+ public override async Task Json_collection_element_access_in_projection_using_ElementAt(bool async)
{
- await base.Json_collection_element_access_in_predicate(async);
+ await base.Json_collection_element_access_in_projection_using_ElementAt(async);
AssertSql(
- @"");
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1]'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1]'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_projection_project_collection(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_project_collection(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_using_ElementAt_project_collection(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_using_parameter(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_using_parameter(async);
+
+ AssertSql(
+"""
+@__prm_0='0'
+
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + ']'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_using_column(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_using_column(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[' + CAST([j].[Id] AS nvarchar(max)) + ']'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_nested(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_nested(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + ']'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_nested_project_scalar(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_nested_project_scalar(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].Date') AS datetime2)
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_nested_project_reference(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_nested_project_reference(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedReferenceLeaf'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_nested_project_collection(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_nested_project_collection(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+ORDER BY [j].[Id]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT [j].[Id], JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf')
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_predicate_using_constant(bool async)
+ {
+ await base.Json_collection_element_access_in_predicate_using_constant(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') <> N'Foo' OR (JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') IS NULL)
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_predicate_using_variable(bool async)
+ {
+ await base.Json_collection_element_access_in_predicate_using_variable(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + '].Name') <> N'Foo' OR (JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + '].Name') IS NULL)
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_predicate_using_column(bool async)
+ {
+ await base.Json_collection_element_access_in_predicate_using_column(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST([j].[Id] AS nvarchar(max)) + '].Name') = N'e1_c2'
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async)
+ {
+ await base.Json_collection_element_access_in_predicate_using_complex_expression1(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(CASE
+ WHEN [j].[Id] = 1 THEN 0
+ ELSE 1
+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)
+ {
+ await base.Json_collection_element_access_in_predicate_using_complex_expression2(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST((
+ SELECT MAX([j].[Id])
+ FROM [JsonEntitiesBasic] AS [j]) AS nvarchar(max)) + '].Name') = N'e1_c2'
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_predicate_using_ElementAt(bool async)
+ {
+ await base.Json_collection_element_access_in_predicate_using_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[1].Name') <> N'Foo' OR (JSON_VALUE([j].[OwnedCollectionRoot],'$[1].Name') IS NULL)
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ {
+ await base.Json_collection_element_access_in_predicate_nested_mix(async);
+
+ AssertSql(
+"""
+@__prm_0='0'
+
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf[' + CAST([j].[Id] - 1 AS nvarchar(max)) + '].SomethingSomething') = N'e1_c2_c1_c1'
+""");
+ }
+
+ public override async Task Json_projection_deduplication_with_collection_indexer_in_original(bool async)
+ {
+ await base.Json_projection_deduplication_with_collection_indexer_in_original(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id], JSON_QUERY([j].[OwnedCollectionRoot],'$[0]')
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
+ public override async Task Json_projection_deduplication_with_collection_indexer_in_target(bool async)
+ {
+ await base.Json_projection_deduplication_with_collection_indexer_in_target(async);
+
+ AssertSql(
+"""
+@__prm_0='1'
+
+SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch[1]'), [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedCollectionLeaf[' + CAST(@__prm_0 AS nvarchar(max)) + ']')
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(bool async)
+ {
+ await base.Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(async);
+
+ AssertSql(
+"""
+SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch'), JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch[1]')
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(async);
+
+ AssertSql();
+ }
+
+ public override async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present2(bool async)
+ {
+ await base.Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present2(async);
+
+ AssertSql();
}
public override async Task Json_scalar_required_null_semantics(bool async)
@@ -639,6 +930,21 @@ GROUP BY [t].[Key]
""");
}
+ public override async Task Group_by_on_json_scalar_using_collection_indexer(bool async)
+ {
+ await base.Group_by_on_json_scalar_using_collection_indexer(async);
+
+ AssertSql(
+"""
+SELECT [t].[Key], COUNT(*) AS [Count]
+FROM (
+ SELECT JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') AS [Key]
+ FROM [JsonEntitiesBasic] AS [j]
+) AS [t]
+GROUP BY [t].[Key]
+""");
+ }
+
public override async Task Group_by_First_on_json_scalar(bool async)
{
await base.Group_by_First_on_json_scalar(async);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs
index 648539cedde..c3281b75bf2 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs
@@ -13126,6 +13126,17 @@ ORDER BY [t].[Nickname]
""");
}
+ public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async)
+ {
+ await base.Using_indexer_on_byte_array_and_string_in_projection(async);
+
+ AssertSql(
+"""
+SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name]
+FROM [Squads] AS [s]
+""");
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
index 94fc8da2bf6..71d9c0fccde 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
@@ -11288,6 +11288,17 @@ ORDER BY [g].[Nickname]
""");
}
+ public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async)
+ {
+ await base.Using_indexer_on_byte_array_and_string_in_projection(async);
+
+ AssertSql(
+"""
+SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name]
+FROM [Squads] AS [s]
+""");
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs
index e26fc12d9d5..2081b1e7282 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs
@@ -9862,6 +9862,17 @@ ORDER BY [g].[Nickname]
""");
}
+ public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async)
+ {
+ await base.Using_indexer_on_byte_array_and_string_in_projection(async);
+
+ AssertSql(
+"""
+SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name]
+FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s]
+""");
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs
index c1fa32d4e62..64f7ba6ecce 100644
--- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs
@@ -17,5 +17,6 @@ public enum SqlServerCondition
SupportsTemporalTablesCascadeDelete = 1 << 8,
SupportsUtf8 = 1 << 9,
SupportsFunctions2019 = 1 << 10,
- SupportsFunctions2017 = 1 << 11
+ SupportsFunctions2017 = 1 << 11,
+ SupportsJsonPathExpressions = 1 << 12,
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs
index c4f50b7552d..11aea373e8a 100644
--- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs
@@ -82,6 +82,11 @@ public ValueTask IsMetAsync()
isMet &= TestEnvironment.IsFunctions2017Supported;
}
+ if (Conditions.HasFlag(SqlServerCondition.SupportsJsonPathExpressions))
+ {
+ isMet &= TestEnvironment.SupportsJsonPathExpressions;
+ }
+
return new ValueTask(isMet);
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs
index 2320b24b7bb..206b9170cc7 100644
--- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs
@@ -43,6 +43,8 @@ public static class TestEnvironment
private static bool? _supportsFunctions2019;
+ private static bool? _supportsJsonPathExpressions;
+
private static byte? _productMajorVersion;
private static int? _engineEdition;
@@ -327,6 +329,35 @@ public static bool IsFunctions2019Supported
}
}
+ public static bool SupportsJsonPathExpressions
+ {
+ get
+ {
+ if (!IsConfigured)
+ {
+ return false;
+ }
+
+ if (_supportsJsonPathExpressions.HasValue)
+ {
+ return _supportsJsonPathExpressions.Value;
+ }
+
+ try
+ {
+ _productMajorVersion = GetProductMajorVersion();
+
+ _supportsJsonPathExpressions = _productMajorVersion >= 14 || IsSqlAzure;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ _supportsJsonPathExpressions = false;
+ }
+
+ return _supportsJsonPathExpressions.Value;
+ }
+ }
+
public static byte SqlServerMajorVersion
=> GetProductMajorVersion();
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs
index 4dbe4b83639..22881f07e98 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs
@@ -9433,6 +9433,17 @@ ORDER BY "g"."Nickname"
""");
}
+ public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async)
+ {
+ await base.Using_indexer_on_byte_array_and_string_in_projection(async);
+
+ AssertSql(
+"""
+SELECT "s"."Id", "s"."Banner", "s"."Name"
+FROM "Squads" AS "s"
+""");
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}