From 4e20eb69645ec1c1c19e73a08bc444a84d98669b Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:21:33 +0200 Subject: [PATCH] Fix: ToList on structural collection property is treaded as scalar in projection Don't bind non scalar subquery results for ToList projections --- .../CosmosProjectionBindingExpressionVisitor.cs | 11 +++++++++-- .../ComplexPropertiesCollectionCosmosTest.cs | 15 ++------------- .../Query/JsonQueryCosmosTest.cs | 17 +---------------- .../Query/OwnedQueryCosmosTest.cs | 4 ++-- 4 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index c1f5033a0a8..69d881f15a3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -619,6 +619,8 @@ UnaryExpression unaryExpression EnumerableMethods.Select.MakeGenericMethod(method.GetGenericArguments()), shaper, lambda); + default: + throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); } } else if (method is { Name: nameof(Enumerable.ToList), IsGenericMethod: true } @@ -632,14 +634,19 @@ UnaryExpression unaryExpression throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); } + if (array is not SqlExpression scalarArray) + { + // #34067 + return Visit(argument); + } + // If ToList() was composed over a subquery with operators, the result here is an ArrayExpression (ARRAY(SELECT ...)), whose // CLR Type is IEnumerable. This can be directly used in the resulting ProjectingBindingExpression - the shaper will // simply read the JSON results out successfully. // But if ToList() is composed directly over an array property, that property could have type e.g. T[], which will be read // in the shaper, and then the cast from T[] to List will fail. As a result, wrap the array in an additional // "reprojection" subquery, effectively to change the CLR type. - if (array is SqlExpression scalarArray - && !(array.Type.IsGenericType && array.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + if (!(array.Type.IsGenericType && array.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>))) { Check.DebugAssert( array is not ScalarArrayExpression and not ObjectArrayExpression, "ArrayExpression should be IEnumerable"); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs index 277fbdd576f..fa06e5f141f 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -110,19 +110,8 @@ FROM a IN c["AssociateCollection"] public override Task Distinct() => AssertTranslationFailed(base.Distinct); - public override async Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Distinct_projected(queryTrackingBehavior); - - AssertSql( - """ -SELECT VALUE ARRAY( - SELECT DISTINCT VALUE a - FROM a IN c["AssociateCollection"]) -FROM root c -ORDER BY c["Id"] -"""); - } + public override Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) + => AssertTranslationFailed(() => base.Distinct_projected(queryTrackingBehavior)); public override Task Distinct_over_projected_nested_collection() => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs index 6e23673b9cc..105ca3e51b3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs @@ -881,22 +881,7 @@ public override Task Json_collection_in_projection_with_composition_where_and_an => base.Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_scalars(async); public override Task Json_collection_leaf_filter_in_projection(bool async) - => Fixture.NoSyncTest( - async, async a => - { - await base.Json_collection_leaf_filter_in_projection(a); - - AssertSql( - """ -SELECT VALUE ARRAY( - SELECT VALUE o - FROM o IN c["OwnedReferenceRoot"]["OwnedReferenceBranch"]["OwnedCollectionLeaf"] - WHERE (o["SomethingSomething"] != "Baz")) -FROM root c -WHERE (c["Discriminator"] = "Basic") -ORDER BY c["Id"] -"""); - }); + => AssertTranslationFailed(() => base.Json_collection_leaf_filter_in_projection(async)); public override Task Json_collection_of_primitives_contains_in_predicate(bool async) => Fixture.NoSyncTest( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index 56a1d000f6e..1767e12809b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -465,7 +465,7 @@ await AssertQuery( AssertSql( """ -SELECT VALUE o["Details"] +SELECT VALUE o FROM root c JOIN o IN c["Orders"] WHERE (c["Terminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(o["Details"]) = 1)) @@ -546,7 +546,7 @@ await AssertQuery( AssertSql( """ -SELECT VALUE o["Details"] +SELECT VALUE o FROM root c JOIN o IN c["Orders"] WHERE (c["Terminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(o["Details"]) = 1))