diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 233ce58aea1..b03aa5a0b23 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -11,6 +11,9 @@ namespace Microsoft.EntityFrameworkCore.Query;
///
public partial class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor
{
+ private static readonly bool UseOldBehavior37016 =
+ AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37016", out var enabled) && enabled;
+
private const string SqlQuerySingleColumnAlias = "Value";
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
@@ -1945,12 +1948,19 @@ static TableExpressionBase FindRootTableExpressionForColumn(SelectExpression sel
}
source = source.UnwrapTypeConversion(out var convertedType);
- if (source is not StructuralTypeShaperExpression shaper)
+
+ var type = source switch
+ {
+ StructuralTypeShaperExpression shaper => shaper.StructuralType,
+ JsonQueryExpression jsonQuery when !UseOldBehavior37016 => jsonQuery.StructuralType,
+ _ => null
+ };
+
+ if (type is null)
{
return null;
}
- var type = shaper.StructuralType;
if (convertedType != null)
{
Check.DebugAssert(
@@ -1969,22 +1979,65 @@ static TableExpressionBase FindRootTableExpressionForColumn(SelectExpression sel
var property = type.FindProperty(memberName);
if (property?.IsPrimitiveCollection is true)
{
- return source.CreateEFPropertyExpression(property);
+ return source!.CreateEFPropertyExpression(property);
}
// See comments on indexing-related hacks in VisitMethodCall above
- if (_bindComplexProperties
- && type.FindComplexProperty(memberName) is { IsCollection: true } complexProperty)
+ if (UseOldBehavior37016)
+ {
+ if (_bindComplexProperties && type.FindComplexProperty(memberName) is { IsCollection: true } complexProperty)
+ {
+ Check.DebugAssert(complexProperty.ComplexType.IsMappedToJson());
+
+ if (queryableTranslator._sqlTranslator.TryBindMember(
+ queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
+ out var translatedExpression, out _)
+ && translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery })
+ {
+ return jsonQuery;
+ }
+ }
+ }
+ else if (_bindComplexProperties && type.FindComplexProperty(memberName) is IComplexProperty complexProperty)
{
- Check.DebugAssert(complexProperty.ComplexType.IsMappedToJson());
+ Expression? translatedExpression;
- if (queryableTranslator._sqlTranslator.TryBindMember(
- queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
- out var translatedExpression, out _)
- && translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery })
+ if (source is JsonQueryExpression jsonSource)
{
- return jsonQuery;
+ translatedExpression = jsonSource.BindStructuralProperty(complexProperty);
}
+ else if (!queryableTranslator._sqlTranslator.TryBindMember(
+ queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
+ out translatedExpression, out _))
+ {
+ return null;
+ }
+
+ // Hack: when returning a StructuralTypeShaperExpression, _sqlTranslator returns it wrapped by a
+ // StructuralTypeReferenceExpression, which is supposed to be a private wrapper only within the SQL translator.
+ // Call TranslateProjection to unwrap it (need to look into getting rid StructuralTypeReferenceExpression altogether).
+ if (translatedExpression is not JsonQueryExpression and not CollectionResultExpression)
+ {
+ if (queryableTranslator._sqlTranslator.TranslateProjection(translatedExpression) is { } unwrappedTarget)
+ {
+ translatedExpression = unwrappedTarget;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return complexProperty switch
+ {
+ { IsCollection: false } when translatedExpression is StructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression jsonQuery }
+ => jsonQuery,
+ { IsCollection: true } when translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery }
+ => jsonQuery,
+ { IsCollection: true } when translatedExpression is JsonQueryExpression jsonQuery
+ => jsonQuery,
+ _ => null
+ };
}
return null;
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index 71805ab0516..e111c3a7018 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -20,6 +20,9 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
+ private static readonly bool UseOldBehavior37016 =
+ AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37016", out var enabled) && enabled;
+
private readonly SqlServerQueryCompilationContext _queryCompilationContext;
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
@@ -377,7 +380,7 @@ IComplexType complexType
} selectExpression
when TranslateExpression(index) is { } translatedIndex
&& _sqlServerSingletonOptions.SupportsJsonFunctions
- && TryTranslate(selectExpression, valuesParameter, translatedIndex, out var result):
+ && TryTranslate(selectExpression, valuesParameter, path: null, translatedIndex, out var result):
return result;
// Index on JSON array
@@ -406,7 +409,7 @@ when TranslateExpression(index) is { } translatedIndex
} selectExpression
when orderingTableAlias == openJsonExpression.Alias
&& TranslateExpression(index) is { } translatedIndex
- && TryTranslate(selectExpression, jsonArrayColumn, translatedIndex, out var result):
+ && TryTranslate(selectExpression, jsonArrayColumn, openJsonExpression.Path, translatedIndex, out var result):
return result;
}
}
@@ -415,7 +418,8 @@ when TranslateExpression(index) is { } translatedIndex
bool TryTranslate(
SelectExpression selectExpression,
- SqlExpression jsonArrayColumn,
+ SqlExpression jsonColumn,
+ IReadOnlyList? path,
SqlExpression translatedIndex,
[NotNullWhen(true)] out ShapedQueryExpression? result)
{
@@ -441,16 +445,22 @@ bool TryTranslate(
return false;
}
- // If the inner expression happens to itself be a JsonScalarExpression, simply append the two paths to avoid creating
+ // If the inner expression happens to itself be a JsonScalarExpression, simply append the paths to avoid creating
// JSON_VALUE within JSON_VALUE.
- var (json, path) = jsonArrayColumn is JsonScalarExpression innerJsonScalarExpression
- ? (innerJsonScalarExpression.Json,
- innerJsonScalarExpression.Path.Append(new(translatedIndex)).ToArray())
- : (jsonArrayColumn, [new(translatedIndex)]);
+ var (json, newPath) = jsonColumn is JsonScalarExpression innerJsonScalarExpression
+ ? (innerJsonScalarExpression.Json, new List(innerJsonScalarExpression.Path))
+ : (jsonColumn, []);
+
+ if (!UseOldBehavior37016 && path is not null)
+ {
+ newPath.AddRange(path);
+ }
+
+ newPath.Add(new(translatedIndex));
var translation = new JsonScalarExpression(
json,
- path,
+ newPath,
projection.Type,
projection.TypeMapping,
projectionColumn.IsNullable);
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs
index 1bd1252a191..674facf26dd 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs
@@ -126,6 +126,18 @@ FROM root c
""");
}
+ public override async Task Index_on_nested_collection()
+ {
+ await base.Index_on_nested_collection();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["NestedCollection"][0]["Int"] = 8)
+""");
+ }
+
#endregion Index
#region GroupBy
diff --git a/test/EFCore.Specification.Tests/Query/Associations/AssociationsCollectionTestBase.cs b/test/EFCore.Specification.Tests/Query/Associations/AssociationsCollectionTestBase.cs
index 09260bbe576..a588cbd1d96 100644
--- a/test/EFCore.Specification.Tests/Query/Associations/AssociationsCollectionTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/Associations/AssociationsCollectionTestBase.cs
@@ -75,6 +75,14 @@ public virtual Task Index_column()
ss => ss.Set().Where(e => e.AssociateCollection[e.Id - 1].Int == 8),
ss => ss.Set().Where(e => e.AssociateCollection.Count > e.Id - 1 && e.AssociateCollection[e.Id - 1].Int == 8)));
+ [ConditionalFact]
+ public virtual Task Index_on_nested_collection()
+ => AssertOrderedCollectionQuery(() => AssertQuery(
+ ss => ss.Set().Where(e => e.RequiredAssociate.NestedCollection[0].Int == 8),
+ ss => ss.Set().Where(
+ e => e.RequiredAssociate.NestedCollection.Count > 0
+ && e.RequiredAssociate.NestedCollection[0].Int == 8)));
+
[ConditionalFact]
public virtual Task Index_out_of_bounds()
=> AssertOrderedCollectionQuery(() => AssertQuery(
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionSqlServerTest.cs
index a53ce1cc644..f25e0bf81b2 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionSqlServerTest.cs
@@ -244,6 +244,30 @@ WHERE CAST(JSON_VALUE([r].[AssociateCollection], '$[' + CAST([r].[Id] - 1 AS nva
}
}
+ public override async Task Index_on_nested_collection()
+ {
+ await base.Index_on_nested_collection();
+
+ if (Fixture.UsingJsonType)
+ {
+ AssertSql(
+ """
+SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
+FROM [RootEntity] AS [r]
+WHERE JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int' RETURNING int) = 8
+""");
+ }
+ else
+ {
+ AssertSql(
+ """
+SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
+FROM [RootEntity] AS [r]
+WHERE CAST(JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int') AS int) = 8
+""");
+ }
+ }
+
public override async Task Index_out_of_bounds()
{
await base.Index_out_of_bounds();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionSqlServerTest.cs
index eeda032ab4d..4d672656a85 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionSqlServerTest.cs
@@ -199,6 +199,13 @@ public override async Task Index_column()
AssertSql();
}
+ public override async Task Index_on_nested_collection()
+ {
+ await base.Index_on_nested_collection();
+
+ AssertSql();
+ }
+
public override async Task Index_out_of_bounds()
{
await base.Index_out_of_bounds();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionSqlServerTest.cs
index 50641e91e54..22977341c9f 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionSqlServerTest.cs
@@ -317,6 +317,30 @@ WHERE CAST(JSON_VALUE([r].[AssociateCollection], '$[' + CAST([r].[Id] - 1 AS nva
}
}
+ public override async Task Index_on_nested_collection()
+ {
+ await base.Index_on_nested_collection();
+
+ if (Fixture.UsingJsonType)
+ {
+ AssertSql(
+ """
+SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
+FROM [RootEntity] AS [r]
+WHERE JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int' RETURNING int) = 8
+""");
+ }
+ else
+ {
+ AssertSql(
+ """
+SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
+FROM [RootEntity] AS [r]
+WHERE CAST(JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int') AS int) = 8
+""");
+ }
+ }
+
public override async Task Index_out_of_bounds()
{
await base.Index_out_of_bounds();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs
index 7c00809cf1a..06fa9a4be73 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs
@@ -202,6 +202,13 @@ public override async Task Index_column()
AssertSql();
}
+ public override async Task Index_on_nested_collection()
+ {
+ await base.Index_on_nested_collection();
+
+ AssertSql();
+ }
+
public override async Task Index_out_of_bounds()
{
await base.Index_out_of_bounds();