From d352388a2fef6b06382611428b799ab896c6ca9d Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:28:23 +0100 Subject: [PATCH 01/28] WIP: Add some tests --- .../ComplexPropertiesCollectionCosmosTest.cs | 173 ++++++++++++++++++ ...omplexPropertiesMiscellaneousCosmosTest.cs | 98 ++++++++++ ...PropertiesPrimitiveCollectionCosmosTest.cs | 97 ++++++++++ .../ComplexPropertiesProjectionCosmosTest.cs | 12 +- ...omplexPropertiesSetOperationsCosmosTest.cs | 67 +++++++ ...xPropertiesStructuralEqualityCosmosTest.cs | 170 +++++++++++++++++ 6 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs new file mode 100644 index 00000000000..7e5d1b5e165 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Azure.Cosmos; + +namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties; + +public class ComplexPropertiesCollectionCosmosTest : ComplexPropertiesCollectionTestBase, IClassFixture +{ + public ComplexPropertiesCollectionCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Count() + { + await base.Count(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (ARRAY_LENGTH(c["AssociateCollection"]) = 2) +"""); + } + + public override async Task Where() + { + await base.Where(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (( + SELECT VALUE COUNT(1) + FROM a IN c["AssociateCollection"] + WHERE (a["Int"] != 8)) = 2) +"""); + } + + public override async Task OrderBy_ElementAt() + { + // 'ORDER BY' is not supported in subqueries. + await Assert.ThrowsAsync(() => base.OrderBy_ElementAt()); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (ARRAY( + SELECT VALUE a["Int"] + FROM a IN c["AssociateCollection"] + ORDER BY a["Id"])[0] = 8) +"""); + } + + #region Distinct + + public override Task Distinct() + => AssertTranslationFailed(base.Distinct); + + 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); + + public override Task Distinct_over_projected_filtered_nested_collection() + => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); + + #endregion Distinct + + #region Index + + public override async Task Index_constant() + { + await base.Index_constant(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["AssociateCollection"][0]["Int"] = 8) +"""); + } + + public override async Task Index_parameter() + { + await base.Index_parameter(); + + AssertSql( + """ +@i='0' + +SELECT VALUE c +FROM root c +WHERE (c["AssociateCollection"][@i]["Int"] = 8) +"""); + } + + public override async Task Index_column() + { + // The specified query includes 'member indexer' which is currently not supported + await Assert.ThrowsAsync(() => base.Index_column()); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["AssociateCollection"][(c["Id"] - 1)]["Int"] = 8) +"""); + } + + public override async Task Index_out_of_bounds() + { + await base.Index_out_of_bounds(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["AssociateCollection"][9999]["Int"] = 8) +"""); + } + + 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 + + [ConditionalFact] + public override Task GroupBy() + => AssertTranslationFailed(base.GroupBy); + + #endregion GroupBy + + public override async Task Select_within_Select_within_Select_with_aggregates() + { + await base.Select_within_Select_within_Select_with_aggregates(); + + AssertSql( + """ +SELECT VALUE ( + SELECT VALUE SUM(( + SELECT VALUE MAX(n["Int"]) + FROM n IN a["NestedCollection"])) + FROM a IN c["AssociateCollection"]) +FROM root c +"""); + } + + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs new file mode 100644 index 00000000000..ed72d52fdc4 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs @@ -0,0 +1,98 @@ +// 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.Associations.ComplexProperties; + +public class ComplexPropertiesMiscellaneousCosmosTest + : ComplexPropertiesMiscellaneousTestBase +{ + public ComplexPropertiesMiscellaneousCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); + } + + public override async Task Where_on_associate_scalar_property() + { + await base.Where_on_associate_scalar_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["Int"] = 8) +"""); + } + + public override async Task Where_on_optional_associate_scalar_property() + { + await base.Where_on_optional_associate_scalar_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"]["Int"] = 8) +"""); + } + + public override async Task Where_on_nested_associate_scalar_property() + { + await base.Where_on_nested_associate_scalar_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = 8) +"""); + } + + #region Value types + + public override async Task Where_property_on_non_nullable_value_type() + { + await base.Where_property_on_non_nullable_value_type(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["Int"] = 8) +"""); + } + + public override async Task Where_property_on_nullable_value_type_Value() + { + await base.Where_property_on_nullable_value_type_Value(); + + AssertSql(""" +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"]["Int"] = 8) +"""); + } + + public override async Task Where_HasValue_on_nullable_value_type() + { + // @TODO: Structural equality. + await base.Where_HasValue_on_nullable_value_type(); + + AssertSql(""" +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"] != null) +"""); + //var ex = await Assert.ThrowsAsync(() => base.Where_HasValue_on_nullable_value_type()); + //Assert.Equal(CoreStrings.EntityEqualityOnKeylessEntityNotSupported("!=", "ValueRootEntity.OptionalAssociate#ValueAssociateType"), ex.Message); + } + + #endregion Value types + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs new file mode 100644 index 00000000000..cf3fb5fba77 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs @@ -0,0 +1,97 @@ +// 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.Associations.ComplexProperties; + +public class ComplexPropertiesPrimitiveCollectionCosmosTest + : ComplexPropertiesPrimitiveCollectionTestBase +{ + public ComplexPropertiesPrimitiveCollectionCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); + } + + public override async Task Count() + { + await base.Count(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (ARRAY_LENGTH(c["RequiredAssociate"]["Ints"]) = 3) +"""); + } + + public override async Task Index() + { + await base.Index(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["Ints"][0] = 1) +"""); + } + + public override async Task Contains() + { + await base.Contains(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(c["RequiredAssociate"]["Ints"], 3) +"""); + } + + public override async Task Any_predicate() + { + await base.Any_predicate(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(c["RequiredAssociate"]["Ints"], 2) +"""); + } + + public override async Task Nested_Count() + { + await base.Nested_Count(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (ARRAY_LENGTH(c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"]) = 3) +"""); + } + + public override async Task Select_Sum() + { + await base.Select_Sum(); + + AssertSql( + """ +SELECT VALUE ( + SELECT VALUE SUM(i0) + FROM i0 IN c["RequiredAssociate"]["Ints"]) +FROM root c +WHERE (( + SELECT VALUE SUM(i) + FROM i IN c["RequiredAssociate"]["Ints"]) >= 6) +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs index 07a6105b37a..406db555159 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit.Sdk; - namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties; public class ComplexPropertiesProjectionCosmosTest : ComplexPropertiesProjectionTestBase @@ -26,7 +24,6 @@ FROM root c #region Scalar properties - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); @@ -38,7 +35,6 @@ FROM root c """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) { // When OptionalAssociate is null, the property access on it evaluates to undefined in Cosmos, causing the @@ -55,7 +51,6 @@ FROM root c """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) { // When OptionalAssociate is null, the property access on it evaluates to undefined in Cosmos, causing the @@ -72,7 +67,6 @@ FROM root c """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) { // When OptionalAssociate is null, the property access on it evaluates to undefined in Cosmos, causing the @@ -174,7 +168,6 @@ FROM root c """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); @@ -226,7 +219,6 @@ ORDER BY c["Id"] """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) { await base.SelectMany_associate_collection(queryTrackingBehavior); @@ -239,7 +231,6 @@ JOIN a IN c["AssociateCollection"] """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) { await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); @@ -252,7 +243,6 @@ JOIN n IN c["RequiredAssociate"]["NestedCollection"] """); } - [ConditionalTheory(Skip = "TODO: Query projection")] public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) { await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); @@ -293,6 +283,7 @@ public override Task Select_subquery_optional_related_FirstOrDefault(QueryTracki #endregion Subquery #region Value types + public override async Task Select_root_with_value_types(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_root_with_value_types(queryTrackingBehavior); @@ -316,6 +307,7 @@ ORDER BY c["Id"] """); } + public override async Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_nullable_value_type(queryTrackingBehavior); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs new file mode 100644 index 00000000000..ab07d0fdee2 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs @@ -0,0 +1,67 @@ +// 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.Associations.ComplexProperties; + +public class ComplexPropertiesSetOperationsCosmosTest + : ComplexPropertiesSetOperationsTestBase +{ + public ComplexPropertiesSetOperationsCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); + } + + public override async Task Over_associate_collections() + { + await base.Over_associate_collections(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (ARRAY_LENGTH(ARRAY_CONCAT(ARRAY( + SELECT VALUE a + FROM a IN c["AssociateCollection"] + WHERE (a["Int"] = 8)), ARRAY( + SELECT VALUE a0 + FROM a0 IN c["AssociateCollection"] + WHERE (a0["String"] = "foo")))) = 4) +"""); + } + + public override Task Over_associate_collection_projected(QueryTrackingBehavior queryTrackingBehavior) + => Assert.ThrowsAsync(() => base.Over_associate_collection_projected(queryTrackingBehavior)); + + public override Task Over_assocate_collection_Select_nested_with_aggregates_projected(QueryTrackingBehavior queryTrackingBehavior) + => Assert.ThrowsAsync( + () => base.Over_assocate_collection_Select_nested_with_aggregates_projected(queryTrackingBehavior)); + + public override async Task Over_nested_associate_collection() + { + await base.Over_nested_associate_collection(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (ARRAY_LENGTH(ARRAY_CONCAT(ARRAY( + SELECT VALUE n + FROM n IN c["RequiredAssociate"]["NestedCollection"] + WHERE (n["Int"] = 8)), ARRAY( + SELECT VALUE n0 + FROM n0 IN c["RequiredAssociate"]["NestedCollection"] + WHERE (n0["String"] = "foo")))) = 4) +"""); + } + + public override Task Over_different_collection_properties() + => AssertTranslationFailed(base.Over_different_collection_properties); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs new file mode 100644 index 00000000000..d849872b710 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -0,0 +1,170 @@ +// 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.Associations.ComplexProperties; + +public class ComplexPropertiesStructuralEqualityCosmosTest : ComplexPropertiesStructuralEqualityTestBase +{ + public ComplexPropertiesStructuralEqualityCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); + } + + public override Task Two_associates() + => AssertTranslationFailed(base.Two_associates); + + public override async Task Two_nested_associates() + { + await base.Two_nested_associates(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["RequiredNestedAssociate"] = null) AND (c["OptionalAssociate"]["RequiredNestedAssociate"] = null)) OR ((c["OptionalAssociate"]["RequiredNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["String"])))) +"""); + } + + public override Task Not_equals() + => AssertTranslationFailed(base.Not_equals); + + public override async Task Associate_with_inline_null() + { + await base.Associate_with_inline_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"] = null) +"""); + } + + public override Task Associate_with_parameter_null() + => AssertTranslationFailed(base.Associate_with_parameter_null); + + public override async Task Nested_associate_with_inline_null() + { + await base.Nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = null) +"""); + } + + public override async Task Nested_associate_with_inline() + { + await base.Nested_associate_with_inline(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = 1000) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = 8)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = [1,2,3])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = "Root1_RequiredAssociate_RequiredNestedAssociate")) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = "foo")) +"""); + } + + public override async Task Nested_associate_with_parameter() + { + await base.Nested_associate_with_parameter(); + + AssertSql( + """ +@entity_equality_nested='{}' +@entity_equality_nested_Id='1000' +@entity_equality_nested_Int='8' +@entity_equality_nested_Ints='[1,2,3]' +@entity_equality_nested_Name='Root1_RequiredAssociate_RequiredNestedAssociate' +@entity_equality_nested_String='foo' + +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["RequiredNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = @entity_equality_nested_String)))) +"""); + } + + [ConditionalFact] + public async Task Nested_associate_with_parameter_null() + { + NestedAssociateType? nested = null; + await AssertQuery( + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested), + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested)); + + AssertSql( + """ +@entity_equality_nested=null +@entity_equality_nested_Id=null +@entity_equality_nested_Int=null +@entity_equality_nested_Ints=null +@entity_equality_nested_Name=null +@entity_equality_nested_String=null + +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) +"""); + } + + public override Task Two_nested_collections() + => AssertTranslationFailed(base.Two_nested_collections); + + public override Task Nested_collection_with_inline() + => AssertTranslationFailed(base.Nested_collection_with_inline); + + public override Task Nested_collection_with_parameter() + => AssertTranslationFailed(base.Nested_collection_with_parameter); + + [ConditionalFact] + public override async Task Nullable_value_type_with_null() + { + await base.Nullable_value_type_with_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"] = null) +"""); + } + + #region Contains + + public override async Task Contains_with_inline() + { + // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. + await Assert.ThrowsAsync(() => base.Contains_with_inline()); + + AssertSql(); + } + + public override async Task Contains_with_parameter() + { + + await Assert.ThrowsAsync(base.Contains_with_parameter); + } + + public override async Task Contains_with_operators_composed_on_the_collection() + { + + await Assert.ThrowsAsync(base.Contains_with_operators_composed_on_the_collection); + } + + public override async Task Contains_with_nested_and_composed_operators() + { + await Assert.ThrowsAsync(base.Contains_with_nested_and_composed_operators); + } + + #endregion Contains + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} From 6490d7b4bcf0d81d82f520b595d396be379f3d8e Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:05:48 +0100 Subject: [PATCH 02/28] Copy over files: Use StructuralType where needed in query translation pipeline --- .../CosmosShapedQueryExpressionExtensions.cs | 2 +- .../Query/Internal/CosmosAliasManager.cs | 2 +- ...osmosProjectionBindingExpressionVisitor.cs | 14 +- .../Query/Internal/CosmosQuerySqlGenerator.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 53 ++-- ...ionBindingRemovingExpressionVisitorBase.cs | 14 +- .../CosmosSqlTranslatingExpressionVisitor.cs | 293 ++++++++++++------ .../Expressions/ObjectAccessExpression.cs | 37 ++- .../ObjectArrayAccessExpression.cs | 39 ++- .../Expressions/ObjectReferenceExpression.cs | 8 +- .../Internal/Expressions/SelectExpression.cs | 4 +- .../Expressions/SqlObjectAccessExpression.cs | 90 ++++++ ... => StructuralTypeProjectionExpression.cs} | 109 +++++-- .../Query/Internal/SqlExpressionVisitor.cs | 4 +- 14 files changed, 483 insertions(+), 188 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs rename src/EFCore.Cosmos/Query/Internal/Expressions/{EntityProjectionExpression.cs => StructuralTypeProjectionExpression.cs} (75%) diff --git a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs index be295279ee4..4f57188376d 100644 --- a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs @@ -167,7 +167,7 @@ public static bool TryExtractArray( projectedStructuralTypeShaper = shaper; projection = shaper.ValueBufferExpression; if (projection is ProjectionBindingExpression { ProjectionMember: { } projectionMember } - && select.GetMappedProjection(projectionMember) is EntityProjectionExpression entityProjection) + && select.GetMappedProjection(projectionMember) is StructuralTypeProjectionExpression entityProjection) { projection = entityProjection.Object; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs index 6227029eae3..15328419326 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs @@ -226,7 +226,7 @@ protected override Expression VisitExtension(Expression node) ScalarReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) => new ScalarReferenceExpression(newAlias, reference.Type, reference.TypeMapping), ObjectReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias) - => new ObjectReferenceExpression(reference.EntityType, newAlias), + => new ObjectReferenceExpression(reference.StructuralType, newAlias), _ => base.VisitExtension(node) }; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 8c53bec67ee..04ce3273e59 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -214,7 +214,7 @@ protected override Expression VisitExtension(Expression extensionExpression) if (_clientEval) { - var entityProjection = (EntityProjectionExpression)projection; + var entityProjection = (StructuralTypeProjectionExpression)projection; return entityShaperExpression.Update( new ProjectionBindingExpression( @@ -306,11 +306,11 @@ protected override Expression VisitMember(MemberExpression memberExpression) var innerEntityProjection = shaperExpression.ValueBufferExpression switch { ProjectionBindingExpression innerProjectionBindingExpression - => (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression, + => (StructuralTypeProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression, // Unwrap EntityProjectionExpression when the root entity is not projected UnaryExpression unaryExpression - => (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand, + => (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand, _ => throw new InvalidOperationException(CoreStrings.TranslationFailed(memberExpression.Print())) }; @@ -326,7 +326,7 @@ UnaryExpression unaryExpression switch (navigationProjection) { - case EntityProjectionExpression entityProjection: + case StructuralTypeProjectionExpression entityProjection: return new StructuralTypeShaperExpression( navigation.TargetEntityType, Expression.Convert(Expression.Convert(entityProjection, typeof(object)), typeof(ValueBuffer)), @@ -527,14 +527,14 @@ when _collectionShaperMapping.TryGetValue(parameterExpression, out var collectio var innerEntityProjection = shaperExpression.ValueBufferExpression switch { - EntityProjectionExpression entityProjection + StructuralTypeProjectionExpression entityProjection => entityProjection, ProjectionBindingExpression innerProjectionBindingExpression - => (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression, + => (StructuralTypeProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression, UnaryExpression unaryExpression - => (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand, + => (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand, _ => throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())) }; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 8ac1a8dcaa9..4acf5951d2a 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -62,7 +62,7 @@ public virtual CosmosSqlQuery GetSqlQuery( /// 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 VisitEntityProjection(EntityProjectionExpression entityProjectionExpression) + protected override Expression VisitEntityProjection(StructuralTypeProjectionExpression entityProjectionExpression) { Visit(entityProjectionExpression.Object); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 3f826412a13..4cebd8c8c5a 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Numerics; +using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; @@ -252,7 +255,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var alias = _aliasManager.GenerateSourceAlias(fromSql); var selectExpression = new SelectExpression( new SourceExpression(fromSql, alias), - new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType)); + new StructuralTypeProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType)); return CreateShapedQueryExpression(entityType, selectExpression) ?? QueryCompilationContext.NotTranslatedExpression; default: @@ -300,7 +303,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis var alias = _aliasManager.GenerateSourceAlias("c"); var selectExpression = new SelectExpression( new SourceExpression(new ObjectReferenceExpression(entityType, "root"), alias), - new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType)); + new StructuralTypeProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType)); // Add discriminator predicate var concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToList(); @@ -323,7 +326,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis "Missing discriminator property in hierarchy"); if (discriminatorProperty is not null) { - var discriminatorColumn = ((EntityProjectionExpression)selectExpression.GetMappedProjection(new ProjectionMember())) + var discriminatorColumn = ((StructuralTypeProjectionExpression)selectExpression.GetMappedProjection(new ProjectionMember())) .BindProperty(discriminatorProperty, clientEval: false); var success = TryApplyPredicate( @@ -340,9 +343,9 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis return CreateShapedQueryExpression(entityType, selectExpression); } - private ShapedQueryExpression? CreateShapedQueryExpression(IEntityType entityType, SelectExpression queryExpression) + private ShapedQueryExpression? CreateShapedQueryExpression(ITypeBase structuralType, SelectExpression queryExpression) { - if (!entityType.IsOwned()) + if (structuralType is IEntityType entityType && !entityType.IsOwned()) { var existingEntityType = _queryCompilationContext.RootEntityType; if (existingEntityType is not null && existingEntityType != entityType) @@ -358,7 +361,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis return new ShapedQueryExpression( queryExpression, new StructuralTypeShaperExpression( - entityType, + structuralType, new ProjectionBindingExpression(queryExpression, new ProjectionMember(), typeof(ValueBuffer)), nullable: false)); } @@ -532,6 +535,13 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return null; } + // @TODO: Without this check, this will generate a query with DISTINCT over a complex type. This does work, but it is a bitwise comparison. Meaning if properties are in different order, or there are extra properties, this will not work as expected for EF Distinct. Currently for owned types this will create a subquery because owned types are translated to Include's. The subquery will be checked for isdistinct and return null due to sub query pushdown not implement. + // Are we ok with a bitwise comparison for complex types? If not we need to implement GROUP BY { properties}. However, that could be out of scope for now? Have to discuss + if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IComplexType }) + { + return null; + } + select.ApplyDistinct(); return source; @@ -607,7 +617,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou var translatedSelect = new SelectExpression( - new EntityProjectionExpression(translation, (IEntityType)projectedStructuralTypeShaper.StructuralType)); + new StructuralTypeProjectionExpression(translation, projectedStructuralTypeShaper.StructuralType)); return source.Update( translatedSelect, new StructuralTypeShaperExpression( @@ -896,7 +906,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou var projectionMember = projectionBindingExpression.ProjectionMember; Check.DebugAssert(new ProjectionMember().Equals(projectionMember), "Invalid ProjectionMember when processing OfType"); - var entityProjectionExpression = (EntityProjectionExpression)select.GetMappedProjection(projectionMember); + var entityProjectionExpression = (StructuralTypeProjectionExpression)select.GetMappedProjection(projectionMember); select.ReplaceProjectionMapping( new Dictionary { @@ -1131,9 +1141,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s var translatedSelect = SelectExpression.CreateForCollection( slice, alias, - new EntityProjectionExpression( - new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias), - (IEntityType)projectedStructuralTypeShaper.StructuralType)); + new StructuralTypeProjectionExpression( + new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias), + projectedStructuralTypeShaper.StructuralType)); return source.Update( translatedSelect, new StructuralTypeShaperExpression( @@ -1270,9 +1280,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s var translatedSelect = SelectExpression.CreateForCollection( slice, alias, - new EntityProjectionExpression( - new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias), - (IEntityType)projectedStructuralTypeShaper.StructuralType)); + new StructuralTypeProjectionExpression( + new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias), + projectedStructuralTypeShaper.StructuralType)); return source.Update( translatedSelect, new StructuralTypeShaperExpression( @@ -1378,20 +1388,19 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s switch (translatedExpression) { - case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true }: + case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true } + or IComplexProperty { IsCollection: true }: { - var targetEntityType = (IEntityType)shaper.StructuralType; - var projection = new EntityProjectionExpression( - new ObjectReferenceExpression(targetEntityType, sourceAlias), targetEntityType); + var targetStructuralType = shaper.StructuralType; + var projection = new StructuralTypeProjectionExpression( + new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType); var select = SelectExpression.CreateForCollection( shaper.ValueBufferExpression, sourceAlias, projection); - return CreateShapedQueryExpression(targetEntityType, select); + return CreateShapedQueryExpression(targetStructuralType, select); } - // TODO: Collection of complex type (#31253) - // Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor // (no collection -> no queryable operators) @@ -1658,7 +1667,7 @@ private bool TryPushdownIntoSubquery(SelectExpression select) var translation = new ObjectFunctionExpression(functionName, [array1, array2], arrayType); var alias = _aliasManager.GenerateSourceAlias(translation); var select = SelectExpression.CreateForCollection( - translation, alias, new ObjectReferenceExpression((IEntityType)structuralType1, alias)); + translation, alias, new ObjectReferenceExpression(structuralType1, alias)); return CreateShapedQueryExpression(select, structuralType1.ClrType); } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index aafd01a66e8..5985d8368ac 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -94,7 +94,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) storeName = e.PropertyName; break; - case EntityProjectionExpression e: + case StructuralTypeProjectionExpression e: storeName = e.PropertyName; break; } @@ -108,7 +108,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) objectArrayProjectionExpression.Object, storeName, parameterExpression.Type); break; - case EntityProjectionExpression entityProjectionExpression: + case StructuralTypeProjectionExpression entityProjectionExpression: var accessExpression = entityProjectionExpression.Object; _projectionBindings[accessExpression] = parameterExpression; @@ -127,7 +127,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) accessExpression = objectAccessExpression.Object; storeNames.Add(objectAccessExpression.PropertyName); _ownerMappings[objectAccessExpression] - = (objectAccessExpression.Navigation.DeclaringEntityType, accessExpression); + = ((IEntityType)objectAccessExpression.PropertyBase.DeclaringType, accessExpression); } valueExpression = CreateGetValueExpression(accessExpression, (string)null, typeof(JObject)); @@ -165,16 +165,16 @@ when jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObject } else { - EntityProjectionExpression entityProjectionExpression; + StructuralTypeProjectionExpression entityProjectionExpression; if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression) { var projection = GetProjection(projectionBindingExpression); - entityProjectionExpression = (EntityProjectionExpression)projection.Expression; + entityProjectionExpression = (StructuralTypeProjectionExpression)projection.Expression; } else { var projection = ((UnaryExpression)((UnaryExpression)newExpression.Arguments[0]).Operand).Operand; - entityProjectionExpression = (EntityProjectionExpression)projection; + entityProjectionExpression = (StructuralTypeProjectionExpression)projection; } _materializationContextBindings[parameterExpression] = entityProjectionExpression.Object; @@ -288,7 +288,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var accessExpression = objectArrayAccess.InnerProjection.Object; _projectionBindings[accessExpression] = jObjectParameter; _ownerMappings[accessExpression] = - (objectArrayAccess.Navigation.DeclaringEntityType, objectArrayAccess.Object); + ((IEntityType)objectArrayAccess.PropertyBase.DeclaringType, objectArrayAccess.Object); _ordinalParameterBindings[accessExpression] = Add( ordinalParameter, Constant(1, typeof(int))); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index f963e03ee06..37dd802b860 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,6 +4,9 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -26,6 +29,9 @@ public class CosmosSqlTranslatingExpressionVisitor( { private const string RuntimeParameterPrefix = "entity_equality_"; + private static readonly MethodInfo ParameterPropertyValueExtractorMethod = + typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!; + private static readonly MethodInfo ParameterValueExtractorMethod = typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; @@ -209,24 +215,24 @@ when TryRewriteEntityEquality( ?? QueryCompilationContext.NotTranslatedExpression; } - Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, Type comparisonType, bool match) + Expression ProcessGetType(StructuralTypeReferenceExpression structuralTypeReferenceExpression, Type comparisonType, bool match) { - var entityType = entityReferenceExpression.EntityType; + var structuralType = structuralTypeReferenceExpression.StructuralType; - if (entityType.BaseType == null - && !entityType.GetDirectlyDerivedTypes().Any()) + if (structuralType.BaseType == null + && !structuralType.GetDirectlyDerivedTypes().Any()) { // No hierarchy - return sqlExpressionFactory.Constant((entityType.ClrType == comparisonType) == match); + return sqlExpressionFactory.Constant((structuralType.ClrType == comparisonType) == match); } - if (entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType)) + if (structuralType is IEntityType entityType && entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType)) { // EntitySet will never contain a type of base type return sqlExpressionFactory.Constant(!match); } - var derivedType = entityType.GetDerivedTypesInclusive().SingleOrDefault(et => et.ClrType == comparisonType); + var derivedType = structuralType.GetDerivedTypesInclusive().SingleOrDefault(et => et.ClrType == comparisonType); // If no derived type matches then fail the translation if (derivedType != null) { @@ -239,8 +245,8 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T // Or add predicate for matching that particular type discriminator value // All hierarchies have discriminator property if (TryBindMember( - entityReferenceExpression, - MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()), + structuralTypeReferenceExpression, + MemberIdentity.Create(structuralType.GetDiscriminatorPropertyName()), out var discriminatorMember, out _) && discriminatorMember is SqlExpression discriminatorColumn) @@ -258,17 +264,17 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T return QueryCompilationContext.NotTranslatedExpression; } - bool IsGetTypeMethodCall(Expression expression, [NotNullWhen(true)] out EntityReferenceExpression? entityReferenceExpression) + bool IsGetTypeMethodCall(Expression expression, [NotNullWhen(true)] out StructuralTypeReferenceExpression? structuralTypeReferenceExpression) { - entityReferenceExpression = null; + structuralTypeReferenceExpression = null; if (expression is not MethodCallExpression methodCallExpression || methodCallExpression.Method != GetTypeMethodInfo) { return false; } - entityReferenceExpression = Visit(methodCallExpression.Object) as EntityReferenceExpression; - return entityReferenceExpression != null; + structuralTypeReferenceExpression = Visit(methodCallExpression.Object) as StructuralTypeReferenceExpression; + return structuralTypeReferenceExpression != null; } static bool IsTypeConstant(Expression expression, [NotNullWhen(true)] out Type? type) @@ -339,8 +345,8 @@ protected override Expression VisitExtension(Expression extensionExpression) { switch (extensionExpression) { - case EntityProjectionExpression: - case EntityReferenceExpression: + case StructuralTypeProjectionExpression: + case StructuralTypeReferenceExpression: case SqlExpression: return extensionExpression; @@ -348,7 +354,7 @@ protected override Expression VisitExtension(Expression extensionExpression) return new SqlParameterExpression(queryParameter.Name, queryParameter.Type, null); case StructuralTypeShaperExpression shaper: - return new EntityReferenceExpression(shaper); + return new StructuralTypeReferenceExpression(shaper); // var result = Visit(entityShaperExpression.ValueBufferExpression); // @@ -392,7 +398,7 @@ protected override Expression VisitExtension(Expression extensionExpression) && (convertedType == null || convertedType.IsAssignableFrom(ese.Type))) { - return new EntityReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); + return new StructuralTypeReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); } if (innerExpression is ProjectionBindingExpression pbe @@ -493,6 +499,19 @@ protected override Expression VisitMember(MemberExpression memberExpression) { var innerExpression = Visit(memberExpression.Expression); + if (innerExpression != null && memberExpression.Member.DeclaringType?.IsNullableValueType() == true) + { + if (memberExpression.Member.Name == nameof(Nullable<>.HasValue)) + { + return Visit(Expression.NotEqual(memberExpression.Expression!, Expression.Constant(null, memberExpression.Member.DeclaringType))); + } + + if (memberExpression.Member.Name == nameof(Nullable<>.Value)) + { + return Visit(memberExpression.Expression)!; + } + } + return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression, out _) ? expression : (TranslationFailed(memberExpression.Expression, innerExpression, out var sqlInnerExpression) @@ -802,10 +821,10 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) { var operand = Visit(unaryExpression.Operand); - if (operand is EntityReferenceExpression entityReferenceExpression + if (operand is StructuralTypeReferenceExpression structuralTypeReferenceExpression && unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs) { - return entityReferenceExpression.Convert(unaryExpression.Type); + return structuralTypeReferenceExpression.Convert(unaryExpression.Type); } if (TranslationFailed(unaryExpression.Operand, operand, out var sqlOperand)) @@ -851,9 +870,9 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp var innerExpression = Visit(typeBinaryExpression.Expression); if (typeBinaryExpression.NodeType == ExpressionType.TypeIs - && innerExpression is EntityReferenceExpression entityReferenceExpression) + && innerExpression is StructuralTypeReferenceExpression structuralTypeReferenceExpression + && structuralTypeReferenceExpression.StructuralType is IEntityType entityType) { - var entityType = entityReferenceExpression.EntityType; if (entityType.GetAllBaseTypesInclusive().Any(et => et.ClrType == typeBinaryExpression.TypeOperand)) { return sqlExpressionFactory.Constant(true); @@ -862,7 +881,7 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp var derivedType = entityType.GetDerivedTypes().SingleOrDefault(et => et.ClrType == typeBinaryExpression.TypeOperand); if (derivedType != null && TryBindMember( - entityReferenceExpression, + structuralTypeReferenceExpression, MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()), out var discriminatorMember, out _) @@ -898,7 +917,7 @@ public virtual bool TryBindMember( [NotNullWhen(true)] out IPropertyBase? property, bool wrapResultExpressionInReferenceExpression = true) { - if (source is not EntityReferenceExpression typeReference) + if (source is not StructuralTypeReferenceExpression typeReference) { expression = null; property = null; @@ -909,7 +928,7 @@ public virtual bool TryBindMember( { case { Parameter: { } shaper }: var valueBufferExpression = Visit(shaper.ValueBufferExpression); - var entityProjection = (EntityProjectionExpression)valueBufferExpression; + var entityProjection = (StructuralTypeProjectionExpression)valueBufferExpression; expression = member switch { @@ -938,7 +957,7 @@ public virtual bool TryBindMember( AddTranslationErrorDetails( CoreStrings.QueryUnableToTranslateMember( member.Name, - typeReference.EntityType.DisplayName())); + typeReference.StructuralType.DisplayName())); return false; } @@ -947,7 +966,7 @@ public virtual bool TryBindMember( switch (expression) { case StructuralTypeShaperExpression shaper when wrapResultExpressionInReferenceExpression: - expression = new EntityReferenceExpression(shaper); + expression = new StructuralTypeReferenceExpression(shaper); return true; // case ObjectArrayAccessExpression objectArrayProjectionExpression: // expression = objectArrayProjectionExpression; @@ -992,12 +1011,12 @@ private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNu { result = null; - if (item is not EntityReferenceExpression itemEntityReference) + // @TODO: support composite keys? Then we can support complex types as well. + if (item is not StructuralTypeReferenceExpression itemEntityReference || itemEntityReference.StructuralType is not IEntityType entityType) { return false; } - var entityType = itemEntityReference.EntityType; var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; switch (primaryKeyProperties) @@ -1065,62 +1084,37 @@ private bool TryRewriteEntityEquality( bool equalsMethod, [NotNullWhen(true)] out Expression? result) { - var leftEntityReference = left as EntityReferenceExpression; - var rightEntityReference = right as EntityReferenceExpression; - - if (leftEntityReference == null - && rightEntityReference == null) + var structuralReference = left as StructuralTypeReferenceExpression ?? right as StructuralTypeReferenceExpression; + if (structuralReference == null) { result = null; return false; } + var structuralType = structuralReference.StructuralType; + var compareReference = structuralReference == left ? right : left; - if (IsNullSqlConstantExpression(left) - || IsNullSqlConstantExpression(right)) + // Null equality + if (IsNullSqlConstantExpression(compareReference)) { - var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; - var entityType1 = nonNullEntityReference.EntityType; - var primaryKeyProperties1 = entityType1.FindPrimaryKey()?.Properties; - if (primaryKeyProperties1 == null) + if (structuralType is IEntityType entityType1 && entityType1.IsDocumentRoot() && structuralReference.Subquery == null) { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType1.DisplayName())); + // Document root can never be be null + result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); + return true; } - result = Visit( - primaryKeyProperties1.Select(p => - Expression.MakeBinary( - nodeType, CreatePropertyAccessExpression(nonNullEntityReference, p), - Expression.Constant(null, p.ClrType.MakeNullable()))) - .Aggregate((l, r) => nodeType == ExpressionType.Equal ? Expression.OrElse(l, r) : Expression.AndAlso(l, r))); - + // Treat type as object for null comparison + var access = new SqlObjectAccessExpression(structuralReference.Object); + result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object), null)!, typeMappingSource.FindMapping(typeof(bool)))!; return true; } - var leftEntityType = leftEntityReference?.EntityType; - var rightEntityType = rightEntityReference?.EntityType; - var entityType = leftEntityType ?? rightEntityType; - - Check.DebugAssert(entityType != null, "At least either side should be entityReference so entityType should be non-null."); - - if (leftEntityType != null - && rightEntityType != null - && leftEntityType.GetRootType() != rightEntityType.GetRootType()) + // IEntityType type comparison + if (structuralType is IEntityType entityType) { - result = sqlExpressionFactory.Constant(false); - return true; - } - - var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; - if (primaryKeyProperties == null) - { - throw new InvalidOperationException( + if (entityType.FindPrimaryKey()?.Properties is not { } primaryKeyProperties) + { + throw new InvalidOperationException( CoreStrings.EntityEqualityOnKeylessEntityNotSupported( nodeType == ExpressionType.Equal ? equalsMethod ? nameof(object.Equals) : "==" @@ -1128,22 +1122,122 @@ private bool TryRewriteEntityEquality( ? "!" + nameof(object.Equals) : "!=", entityType.DisplayName())); + } + + if (compareReference is StructuralTypeReferenceExpression compareStructuralTypeReference) + { + // Comparing of 2 different entity types is always false. + if (structuralType.GetRootType() != compareStructuralTypeReference.StructuralType.GetRootType()) + { + result = Visit(Expression.Constant(false)); + return true; + } + } + + // Compare primary keys of entity type + result = CreateStructuralComparison(primaryKeyProperties); + + return result is not null; } + // Complex type equality + else if (structuralType is IComplexType complexType) + { + if (complexType.ComplexProperty.IsCollection) + { + // @TODO: We could compare by: + /* + WHERE ARRAY_LENGTH(c.items) = ARRAY_LENGTH(@items) + AND NOT EXISTS ( + SELECT VALUE i + FROM i IN c.items + WHERE NOT ARRAY_CONTAINS(@items, i, true) + ) + * */ + result = null; + return false; + } - result = Visit( - primaryKeyProperties.Select(p => - Expression.MakeBinary( - nodeType, - CreatePropertyAccessExpression(left, p), - CreatePropertyAccessExpression(right, p))) - .Aggregate((l, r) => nodeType == ExpressionType.Equal - ? Expression.AndAlso(l, r) - : Expression.OrElse(l, r))); + // Compare to another structural type reference x => x.ComplexProp1 == x.ComplexProp2 || + // Compare to constant complex type x => x.ComplexProp1 == new ComplexType() + // Compare to parameter complex type x => x.ComplexProp1 == param + if (compareReference is StructuralTypeReferenceExpression compareStructuralTypeReference && compareStructuralTypeReference.StructuralType.ClrType == structuralType.ClrType || + compareReference is SqlConstantExpression constant && constant.Type.MakeNullable() == structuralType.ClrType.MakeNullable() || + compareReference is SqlParameterExpression parameter && parameter.Type.MakeNullable() == structuralType.ClrType.MakeNullable()) + { + if (compareReference is SqlParameterExpression p) + { + compareReference = new SqlParameterExpression( + p.Name, + structuralType.ClrType, + new CosmosTypeMapping(typeof(object), null, null, null, null) + ); + } - return true; + var allProperties = complexType.GetComplexProperties().Cast().Concat(complexType.GetProperties()); + result = CreateStructuralComparison(allProperties); + + return result is not null; + } + } + + Expression? CreateStructuralComparison(IEnumerable properties) + => CreateStructuralComparisonBy(properties, p => CreatePropertyAccessExpression(right, p)); + + Expression? CreateStructuralComparisonBy(IEnumerable properties, Func rightValueFactory) + { + var propertyCompare = properties.Select(p => + Expression.MakeBinary( + nodeType, + CreatePropertyAccessExpression(left, p), + rightValueFactory(p))) + .Aggregate((l, r) => nodeType == ExpressionType.Equal + ? Expression.AndAlso(l, r) + : Expression.OrElse(l, r)); + + if (compareReference.Type.IsNullableType() && compareReference is not SqlConstantExpression { Value: not null } && + (structuralReference.StructuralType is not IEntityType entityType1 || !entityType1.IsDocumentRoot())) // Document can never be null so don't compare document to null + { + var compareNullCompareReference = compareReference; + if (compareReference is SqlParameterExpression sqlParameterExpression) + { + var lambda = Expression.Lambda( + Expression.Condition( + Expression.Equal( + Expression.Call(ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string))), + Expression.Constant(null) + ), + Expression.Constant(null), + Expression.Constant(new object()) + ), + QueryCompilationContext.QueryContextParameter + ); + + var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}"; + var queryParam = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + compareNullCompareReference = new SqlParameterExpression(queryParam.Name, queryParam.Type, CosmosTypeMapping.Default); + } + + return Visit(Expression.OrElse( + Expression.AndAlso( + Expression.Equal(structuralReference, sqlExpressionFactory.Constant(null, typeof(object), null)!), + Expression.Equal(compareNullCompareReference, sqlExpressionFactory.Constant(null, typeof(object), null)!)) + , + Expression.AndAlso( + Expression.NotEqual(compareNullCompareReference, sqlExpressionFactory.Constant(null, typeof(object), null)!), + propertyCompare + ) + ) + ); + } + + return Visit(propertyCompare); + } + + result = null; + return false; } - private Expression CreatePropertyAccessExpression(Expression target, IProperty property) + private Expression CreatePropertyAccessExpression(Expression target, IPropertyBase property) { switch (target) { @@ -1154,10 +1248,10 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p case SqlParameterExpression sqlParameterExpression: var lambda = Expression.Lambda( Expression.Call( - ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string)), - Expression.Constant(property, typeof(IProperty))), + Expression.Constant(property, typeof(IPropertyBase))), QueryCompilationContext.QueryContextParameter); var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}"; @@ -1174,12 +1268,18 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == prope } } - private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) + private static T? ParameterPropertyValueExtractor(QueryContext context, string baseParameterName, IPropertyBase property) { var baseParameter = context.Parameters[baseParameterName]; return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); } + private static T? ParameterValueExtractor(QueryContext context, string baseParameterName) + { + var baseParameter = context.Parameters[baseParameterName]; + return (T?)baseParameter; + } + private static List? ParameterListValueExtractor( QueryContext context, string baseParameterName, @@ -1241,33 +1341,34 @@ private static bool TranslationFailed(Expression? original, Expression? translat } [DebuggerDisplay("{DebuggerDisplay(),nq}")] - private sealed class EntityReferenceExpression : Expression + private sealed class StructuralTypeReferenceExpression : Expression { - public EntityReferenceExpression(StructuralTypeShaperExpression parameter) + public StructuralTypeReferenceExpression(StructuralTypeShaperExpression parameter) { Parameter = parameter; - EntityType = (IEntityType)parameter.StructuralType; + StructuralType = parameter.StructuralType; } - public EntityReferenceExpression(ShapedQueryExpression subquery) + public StructuralTypeReferenceExpression(ShapedQueryExpression subquery) { Subquery = subquery; - EntityType = (IEntityType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType; + StructuralType = ((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType; } - private EntityReferenceExpression(EntityReferenceExpression typeReference, ITypeBase structuralType) + private StructuralTypeReferenceExpression(StructuralTypeReferenceExpression typeReference, ITypeBase structuralType) { Parameter = typeReference.Parameter; Subquery = typeReference.Subquery; - EntityType = (IEntityType)structuralType; + StructuralType = structuralType; } + public Expression Object => (Expression?)Parameter ?? Subquery ?? throw new UnreachableException(); public new StructuralTypeShaperExpression? Parameter { get; } public ShapedQueryExpression? Subquery { get; } - public IEntityType EntityType { get; } + public ITypeBase StructuralType { get; } public override Type Type - => EntityType.ClrType; + => StructuralType.ClrType; public override ExpressionType NodeType => ExpressionType.Extension; @@ -1280,9 +1381,9 @@ public Expression Convert(Type type) return this; } - return EntityType is { } entityType + return StructuralType is { } entityType && entityType.GetDerivedTypes().FirstOrDefault(et => et.ClrType == type) is { } derivedEntityType - ? new EntityReferenceExpression(this, derivedEntityType) + ? new StructuralTypeReferenceExpression(this, derivedEntityType) : QueryCompilationContext.NotTranslatedExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs index 007394d7119..41d3b1b522b 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs @@ -30,10 +30,33 @@ public ObjectAccessExpression(Expression @object, INavigation navigation) CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity( navigation.DeclaringEntityType.DisplayName(), navigation.Name)); - Navigation = navigation; + PropertyBase = navigation; + TypeBase = navigation.TargetEntityType; Object = @object; } + /// + /// 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 ObjectAccessExpression(Expression @object, IComplexProperty complexProperty) + { + PropertyBase = complexProperty; + PropertyName = complexProperty.Name; + Object = @object; + TypeBase = complexProperty.ComplexType; + } + + /// + /// 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 ITypeBase TypeBase { 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 @@ -50,7 +73,7 @@ public override ExpressionType NodeType /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Type Type - => Navigation.ClrType; + => PropertyBase.ClrType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -74,7 +97,7 @@ public override Type Type /// 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 INavigation Navigation { get; } + public virtual IPropertyBase PropertyBase { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -93,7 +116,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// public virtual ObjectAccessExpression Update(Expression outerExpression) => outerExpression != Object - ? new ObjectAccessExpression(outerExpression, Navigation) + ? PropertyBase is INavigation navigation + ? new ObjectAccessExpression(outerExpression, navigation) + : new ObjectAccessExpression(outerExpression, (IComplexProperty)PropertyBase) : this; /// @@ -127,7 +152,7 @@ public override bool Equals(object? obj) && Equals(objectAccessExpression)); private bool Equals(ObjectAccessExpression objectAccessExpression) - => Navigation == objectAccessExpression.Navigation + => PropertyBase == objectAccessExpression.PropertyBase && Object.Equals(objectAccessExpression.Object); /// @@ -137,5 +162,5 @@ private bool Equals(ObjectAccessExpression objectAccessExpression) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(Navigation, Object); + => HashCode.Combine(PropertyBase, Object); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs index 4eb61d119f3..737332e94e4 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs @@ -27,7 +27,7 @@ public class ObjectArrayAccessExpression : Expression, IPrintableExpression, IAc public ObjectArrayAccessExpression( Expression @object, INavigation navigation, - EntityProjectionExpression? innerProjection = null) + StructuralTypeProjectionExpression? innerProjection = null) { var targetType = navigation.TargetEntityType; Type = typeof(IEnumerable<>).MakeGenericType(targetType.ClrType); @@ -37,10 +37,31 @@ public ObjectArrayAccessExpression( CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity( navigation.DeclaringEntityType.DisplayName(), navigation.Name)); - Navigation = navigation; + PropertyBase = navigation; Object = @object; InnerProjection = innerProjection - ?? new EntityProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType); + ?? new StructuralTypeProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType); + } + + /// + /// 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 ObjectArrayAccessExpression( + Expression @object, + IComplexProperty complexProperty, + StructuralTypeProjectionExpression? innerProjection = null) + { + var targetType = complexProperty.ComplexType; + Type = typeof(IEnumerable<>).MakeGenericType(targetType.ClrType); + + PropertyName = complexProperty.Name; + PropertyBase = complexProperty; + Object = @object; + InnerProjection = innerProjection + ?? new StructuralTypeProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType); } /// @@ -82,7 +103,7 @@ public sealed override ExpressionType NodeType /// 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 INavigation Navigation { get; } + public virtual IPropertyBase PropertyBase { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -90,7 +111,7 @@ public sealed override ExpressionType NodeType /// 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 EntityProjectionExpression InnerProjection { get; } + public virtual StructuralTypeProjectionExpression InnerProjection { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -103,7 +124,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) var accessExpression = visitor.Visit(Object); var innerProjection = visitor.Visit(InnerProjection); - return Update(accessExpression, (EntityProjectionExpression)innerProjection); + return Update(accessExpression, (StructuralTypeProjectionExpression)innerProjection); } /// @@ -114,9 +135,11 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// public virtual ObjectArrayAccessExpression Update( Expression accessExpression, - EntityProjectionExpression innerProjection) + StructuralTypeProjectionExpression innerProjection) => accessExpression != Object || innerProjection != InnerProjection - ? new ObjectArrayAccessExpression(accessExpression, Navigation, innerProjection) + ? PropertyBase is INavigation navigation + ? new ObjectArrayAccessExpression(accessExpression, navigation, innerProjection) + : new ObjectArrayAccessExpression(accessExpression, (IComplexProperty)PropertyBase, innerProjection) : this; /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs index b2fee0d2605..50aee928f05 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 ObjectReferenceExpression(IEntityType entityType, string name) : Expression, IPrintableExpression, IAccessExpression +public class ObjectReferenceExpression(ITypeBase structuralType, string name) : Expression, IPrintableExpression, IAccessExpression { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,7 +33,7 @@ public sealed override ExpressionType NodeType /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Type Type - => EntityType.ClrType; + => StructuralType.ClrType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -45,7 +45,7 @@ public override Type Type // TODO: (CosmosProjectionBindingRemovingExpressionVisitorBase._projectionBindings has IAccessExpressions as keys, and so entity types // TODO: need to participate in the equality etc.). Long-term, this should be a server-side SQL expression that knows nothing about // TODO: the shaper side. - public virtual IEntityType EntityType { get; } = entityType; + public virtual ITypeBase StructuralType { get; } = structuralType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -105,7 +105,7 @@ public override bool Equals(object? obj) private bool Equals(ObjectReferenceExpression objectReferenceExpression) => Name == objectReferenceExpression.Name - && EntityType.Equals(objectReferenceExpression.EntityType); + && StructuralType.Equals(objectReferenceExpression.StructuralType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index 622f4a8c72f..a169168f2e8 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -280,7 +280,7 @@ public int AddToProjection(Expression sqlExpression) /// 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 int AddToProjection(EntityProjectionExpression entityProjection) + public int AddToProjection(StructuralTypeProjectionExpression entityProjection) => AddToProjection(entityProjection, null); private int AddToProjection(Expression expression, string? alias) @@ -504,7 +504,7 @@ public Expression AddJoin(ShapedQueryExpression inner, Expression outerShaper, C projectionToAdd = expression switch { SqlExpression e => new ScalarReferenceExpression(joinSource.Alias, e.Type, e.TypeMapping), - EntityProjectionExpression e => e.Update(new ObjectReferenceExpression(e.EntityType, joinSource.Alias)), + StructuralTypeProjectionExpression e => e.Update(new ObjectReferenceExpression(e.StructuralType, joinSource.Alias)), _ => throw new UnreachableException( $"Unexpected expression type in projection when adding join: {expression.GetType().Name}") diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs new file mode 100644 index 00000000000..4fe2f8c54ac --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs @@ -0,0 +1,90 @@ +// 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.Cosmos.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; + +/// +/// Represents an structural type object access on a CosmosJSON object +/// +/// +/// 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class SqlObjectAccessExpression(Expression @object) + : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression +{ + /// + /// 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 Expression Object { get; } = @object; + + /// + /// 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 string? PropertyName => null; + + /// + /// 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) + => Update(visitor.Visit(Object)); + + /// + /// 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 SqlObjectAccessExpression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new SqlObjectAccessExpression(@object); + + /// + /// 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.Visit(Object); + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlObjectAccessExpression expression + && Equals(expression)); + + private bool Equals(SqlObjectAccessExpression expression) + => base.Equals(expression) + && Object.Equals(expression.Object); + + /// + /// 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 override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Object); +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs similarity index 75% rename from src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs rename to src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs index 11824d8a4b1..6d2a238a51d 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs @@ -12,10 +12,11 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 EntityProjectionExpression : Expression, IPrintableExpression, IAccessExpression +public class StructuralTypeProjectionExpression : Expression, IPrintableExpression, IAccessExpression { private readonly Dictionary _propertyExpressionsMap = new(); private readonly Dictionary _navigationExpressionsMap = new(); + private readonly Dictionary _complexPropertyExpressionsMap = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -23,10 +24,10 @@ public class EntityProjectionExpression : Expression, IPrintableExpression, IAcc /// 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 EntityProjectionExpression(Expression @object, IEntityType entityType) + public StructuralTypeProjectionExpression(Expression @object, ITypeBase structuralType) { Object = @object; - EntityType = entityType; + StructuralType = structuralType; PropertyName = (@object as IAccessExpression)?.PropertyName; } @@ -46,7 +47,7 @@ public sealed override ExpressionType NodeType /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Type Type - => EntityType.ClrType; + => StructuralType.ClrType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -62,7 +63,7 @@ public override Type Type /// 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 IEntityType EntityType { get; } + public virtual ITypeBase StructuralType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -90,7 +91,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) public virtual Expression Update(Expression @object) => ReferenceEquals(@object, Object) ? this - : new EntityProjectionExpression(@object, EntityType); + : new StructuralTypeProjectionExpression(@object, StructuralType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -100,11 +101,11 @@ public virtual Expression Update(Expression @object) /// public virtual Expression BindProperty(IProperty property, bool clientEval) { - if (!EntityType.IsAssignableFrom(property.DeclaringType) - && !property.DeclaringType.IsAssignableFrom(EntityType)) + if (!StructuralType.IsAssignableFrom(property.DeclaringType) + && !property.DeclaringType.IsAssignableFrom(StructuralType)) { throw new InvalidOperationException( - CosmosStrings.UnableToBindMemberToEntityProjection("property", property.Name, EntityType.DisplayName())); + CosmosStrings.UnableToBindMemberToEntityProjection("property", property.Name, StructuralType.DisplayName())); } if (!_propertyExpressionsMap.TryGetValue(property, out var expression)) @@ -136,11 +137,11 @@ public virtual Expression BindProperty(IProperty property, bool clientEval) /// public virtual Expression BindNavigation(INavigation navigation, bool clientEval) { - if (!EntityType.IsAssignableFrom(navigation.DeclaringEntityType) - && !navigation.DeclaringEntityType.IsAssignableFrom(EntityType)) + if (!StructuralType.IsAssignableFrom(navigation.DeclaringEntityType) + && !navigation.DeclaringEntityType.IsAssignableFrom(StructuralType)) { throw new InvalidOperationException( - CosmosStrings.UnableToBindMemberToEntityProjection("navigation", navigation.Name, EntityType.DisplayName())); + CosmosStrings.UnableToBindMemberToEntityProjection("navigation", navigation.Name, StructuralType.DisplayName())); } if (!_navigationExpressionsMap.TryGetValue(navigation, out var expression)) @@ -153,7 +154,7 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval nullable: true) : new StructuralTypeShaperExpression( navigation.TargetEntityType, - new EntityProjectionExpression(new ObjectAccessExpression(Object, navigation), navigation.TargetEntityType), + new StructuralTypeProjectionExpression(new ObjectAccessExpression(Object, navigation), navigation.TargetEntityType), nullable: !navigation.ForeignKey.IsRequiredDependent); _navigationExpressionsMap[navigation] = expression; @@ -170,6 +171,40 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval return expression; } + /// + /// 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 Expression BindComplexProperty(IComplexProperty complexProperty, bool clientEval) + { + if (!StructuralType.IsAssignableFrom(complexProperty.DeclaringType) + && !complexProperty.DeclaringType.IsAssignableFrom(StructuralType)) + { + throw new InvalidOperationException( + CosmosStrings.UnableToBindMemberToEntityProjection("navigation", complexProperty.Name, StructuralType.DisplayName())); + } + + if (!_complexPropertyExpressionsMap.TryGetValue(complexProperty, out var expression)) + { + // TODO: Unify ObjectAccessExpression and ObjectArrayAccessExpression + expression = complexProperty.IsCollection + ? new StructuralTypeShaperExpression( + complexProperty.ComplexType, + new ObjectArrayAccessExpression(Object, complexProperty), + nullable: true) + : new StructuralTypeShaperExpression( + complexProperty.ComplexType, + new StructuralTypeProjectionExpression(new ObjectAccessExpression(Object, complexProperty), complexProperty.ComplexType), + nullable: complexProperty.IsNullable); + + _complexPropertyExpressionsMap[complexProperty] = expression; + } + + return expression; + } + /// /// 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 @@ -198,29 +233,41 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval private Expression? BindMember(MemberIdentity member, Type? entityClrType, bool clientEval, out IPropertyBase? propertyBase) { - var entityType = EntityType; + var structuralType = StructuralType; if (entityClrType != null - && !entityClrType.IsAssignableFrom(entityType.ClrType)) + && !entityClrType.IsAssignableFrom(structuralType.ClrType)) { - entityType = entityType.GetDerivedTypes().First(e => entityClrType.IsAssignableFrom(e.ClrType)); + structuralType = structuralType.GetDerivedTypes().First(e => entityClrType.IsAssignableFrom(e.ClrType)); } var property = member.MemberInfo == null - ? entityType.FindProperty(member.Name!) - : entityType.FindProperty(member.MemberInfo); + ? structuralType.FindProperty(member.Name!) + : structuralType.FindProperty(member.MemberInfo); if (property != null) { propertyBase = property; return BindProperty(property, clientEval); } - var navigation = member.MemberInfo == null + if (structuralType is IEntityType entityType) + { + var navigation = member.MemberInfo == null ? entityType.FindNavigation(member.Name!) : entityType.FindNavigation(member.MemberInfo); - if (navigation != null) + if (navigation != null) + { + propertyBase = navigation; + return BindNavigation(navigation, clientEval); + } + } + + var complex = member.MemberInfo == null + ? structuralType.FindComplexProperty(member.Name!) + : structuralType.FindComplexProperty(member.MemberInfo); + if (complex != null) { - propertyBase = navigation; - return BindNavigation(navigation, clientEval); + propertyBase = complex; + return BindComplexProperty(complex, clientEval); } // Entity member not found @@ -234,16 +281,16 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval /// 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 EntityProjectionExpression UpdateEntityType(IEntityType derivedType) + public virtual StructuralTypeProjectionExpression UpdateEntityType(IEntityType derivedType) { - if (!derivedType.GetAllBaseTypes().Contains(EntityType)) + if (!derivedType.GetAllBaseTypes().Contains(StructuralType)) { throw new InvalidOperationException( CosmosStrings.InvalidDerivedTypeInEntityProjection( - derivedType.DisplayName(), EntityType.DisplayName())); + derivedType.DisplayName(), StructuralType.DisplayName())); } - return new EntityProjectionExpression(Object, derivedType); + return new StructuralTypeProjectionExpression(Object, derivedType); } /// @@ -264,11 +311,11 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) public override bool Equals(object? obj) => obj != null && (ReferenceEquals(this, obj) - || obj is EntityProjectionExpression entityProjectionExpression + || obj is StructuralTypeProjectionExpression entityProjectionExpression && Equals(entityProjectionExpression)); - private bool Equals(EntityProjectionExpression entityProjectionExpression) - => Equals(EntityType, entityProjectionExpression.EntityType) + private bool Equals(StructuralTypeProjectionExpression entityProjectionExpression) + => Equals(StructuralType, entityProjectionExpression.StructuralType) && Object.Equals(entityProjectionExpression.Object); /// @@ -278,7 +325,7 @@ private bool Equals(EntityProjectionExpression entityProjectionExpression) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(EntityType, Object); + => HashCode.Combine(StructuralType, Object); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -287,5 +334,5 @@ public override int GetHashCode() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string ToString() - => $"EntityProjectionExpression: {EntityType.ShortName()}"; + => $"EntityProjectionExpression: {StructuralType.ShortName()}"; } diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs index 22d2b286bb1..235d07fea10 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs @@ -24,7 +24,7 @@ ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)), SelectExpression selectExpression => VisitSelect(selectExpression), ProjectionExpression projectionExpression => VisitProjection(projectionExpression), - EntityProjectionExpression entityProjectionExpression => VisitEntityProjection(entityProjectionExpression), + StructuralTypeProjectionExpression entityProjectionExpression => VisitEntityProjection(entityProjectionExpression), ObjectArrayAccessExpression arrayProjectionExpression => VisitObjectArrayAccess(arrayProjectionExpression), FromSqlExpression fromSqlExpression => VisitFromSql(fromSqlExpression), ObjectReferenceExpression objectReferenceExpression => VisitObjectReference(objectReferenceExpression), @@ -235,7 +235,7 @@ ShapedQueryExpression shapedQueryExpression /// 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 abstract Expression VisitEntityProjection(EntityProjectionExpression entityProjectionExpression); + protected abstract Expression VisitEntityProjection(StructuralTypeProjectionExpression entityProjectionExpression); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to From d9d91d2bb37b0aa07e2fdbeef676190cf641ac9b Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:05:29 +0100 Subject: [PATCH 03/28] Add some more tests --- .../Query/AdHocComplexTypeQueryCosmosTest.cs | 91 ++++ .../Query/ComplexTypeQueryCosmosTest.cs | 420 ++++++++++++++++++ .../Query/AdHocComplexTypeQueryTestBase.cs | 25 +- 3 files changed, 528 insertions(+), 8 deletions(-) create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs new file mode 100644 index 00000000000..832f5b0c8f4 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -0,0 +1,91 @@ +// 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; + +public class AdHocComplexTypeQueryCosmosTest(NonSharedFixture fixture) : AdHocComplexTypeQueryTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance; + + public override async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() + { + await base.Complex_type_equals_parameter_with_nested_types_with_property_of_same_name(); + + AssertSql( + """ +@entity_equality_container='{}' +@entity_equality_entity_equality_container_Containee1='{}' +@entity_equality_entity_equality_container_Containee1_Id='2' +@entity_equality_entity_equality_container_Containee2='{}' +@entity_equality_entity_equality_container_Containee2_Id='3' +@entity_equality_container_Id='1' + +SELECT VALUE c +FROM root c +WHERE (((c["ComplexContainer"] = null) AND (@entity_equality_container = null)) OR ((@entity_equality_container != null) AND (((((c["ComplexContainer"]["Containee1"] = null) AND (@entity_equality_entity_equality_container_Containee1 = null)) OR ((@entity_equality_entity_equality_container_Containee1 != null) AND (c["ComplexContainer"]["Containee1"]["Id"] = @entity_equality_entity_equality_container_Containee1_Id))) AND (((c["ComplexContainer"]["Containee2"] = null) AND (@entity_equality_entity_equality_container_Containee2 = null)) OR ((@entity_equality_entity_equality_container_Containee2 != null) AND (c["ComplexContainer"]["Containee2"]["Id"] = @entity_equality_entity_equality_container_Containee2_Id)))) AND (c["ComplexContainer"]["Id"] = @entity_equality_container_Id)))) +OFFSET 0 LIMIT 2 +"""); + } + + [ConditionalFact(Skip = "#34067: Cosmos: Projecting out nested documents retrieves the entire document")] + public override async Task Projecting_complex_property_does_not_auto_include_owned_types() + { + await base.Projecting_complex_property_does_not_auto_include_owned_types(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Optional_complex_type_with_discriminator() + { + await base.Optional_complex_type_with_discriminator(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["AllOptionalsComplexType"] = null) +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Non_optional_complex_type_with_all_nullable_properties() + { + await base.Non_optional_complex_type_with_all_nullable_properties(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + await base.Nullable_complex_type_with_discriminator_and_shadow_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder) + .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + private void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs new file mode 100644 index 00000000000..99ce4983c21 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -0,0 +1,420 @@ +// 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.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.TestModels.ComplexTypeModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexTypeQueryCosmosTest(ComplexTypeQueryCosmosTest.ComplexTypeQueryCosmosFixture fixture) : ComplexTypeQueryTestBase(fixture) +{ + public override Task Filter_on_property_inside_complex_type_after_subquery(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + public override Task Filter_on_property_inside_nested_complex_type_after_subquery(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_nested_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + public override Task Filter_on_required_property_inside_required_complex_type_on_optional_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_complex_type_on_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup))); + + public override Task Filter_on_required_property_inside_required_complex_type_on_required_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_complex_type_on_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup))); + + public override Task Project_complex_type_via_optional_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_complex_type_via_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup))); + + public override Task Project_complex_type_via_required_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_complex_type_via_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup))); + + public override Task Load_complex_type_after_subquery_on_entity_type(bool async) + => AssertTranslationFailedWithDetails(() => base.Load_complex_type_after_subquery_on_entity_type(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + public override Task Select_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Select_complex_type(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + }); + + public override Task Select_nested_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Select_nested_complex_type(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + }); + + public override Task Select_single_property_on_nested_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Select_single_property_on_nested_complex_type(async); + + AssertSql( + """ +SELECT VALUE c["ShippingAddress"]["Country"]["FullName"] +FROM root c +"""); + }); + + public override Task Select_complex_type_Where(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Select_complex_type_Where(async); + + AssertSql( + """ + +"""); + }); + + public override Task Select_complex_type_Distinct(bool async) // @TODO: Distinct should fail.. + => AssertTranslationFailed(() => base.Select_complex_type_Distinct(async)); + + public override Task Complex_type_equals_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Complex_type_equals_complex_type(async); + + AssertSql( + """ + + """); + }); + + public override Task Complex_type_equals_constant(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Complex_type_equals_constant(async); + + AssertSql( + """ + + """); + }); + + public override Task Complex_type_equals_parameter(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => + { + await base.Complex_type_equals_parameter(async); + + AssertSql( + """ + + """); + }); + + public override Task Subquery_over_complex_type(bool async) + => AssertTranslationFailedWithDetails(() => base.Subquery_over_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Contains_over_complex_type(bool async) + => AssertTranslationFailedWithDetails(() => base.Contains_over_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Concat_complex_type(bool async) + => AssertTranslationFailed(() => base.Concat_complex_type(async)); // Union not supported + + public override Task Concat_entity_type_containing_complex_property(bool async) + => AssertTranslationFailed(() => base.Concat_entity_type_containing_complex_property(async)); // Union not supported + + public override Task Union_entity_type_containing_complex_property(bool async) + => AssertTranslationFailed(() => base.Union_entity_type_containing_complex_property(async)); // Union not supported + + public override Task Union_complex_type(bool async) + => AssertTranslationFailed(() => base.Union_complex_type(async)); // Union not supported + + public override Task Concat_property_in_complex_type(bool async) + => AssertTranslationFailed(() => base.Concat_property_in_complex_type(async)); // Union not supported + + public override Task Union_property_in_complex_type(bool async) + => AssertTranslationFailed(() => base.Union_property_in_complex_type(async)); // Union not supported + + public override Task Concat_two_different_complex_type(bool async) + => AssertTranslationFailed(() => base.Concat_two_different_complex_type(async)); // Union not supported + + public override Task Union_two_different_complex_type(bool async) + => AssertTranslationFailed(() => base.Union_two_different_complex_type(async)); // Union not supported + + public override async Task Filter_on_property_inside_struct_complex_type(bool async) + { + await base.Filter_on_property_inside_struct_complex_type(async); + + AssertSql( + """ + +"""); + } + + public override async Task Filter_on_property_inside_nested_struct_complex_type(bool async) + { + await base.Filter_on_property_inside_nested_struct_complex_type(async); + + AssertSql( + """ + +"""); + } + + public override Task Filter_on_property_inside_struct_complex_type_after_subquery(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_struct_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + public override Task Filter_on_property_inside_nested_struct_complex_type_after_subquery(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_nested_struct_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + public override Task Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup))); + + public override Task Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup))); + + public override Task Project_struct_complex_type_via_optional_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_struct_complex_type_via_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup))); + + public override Task Project_nullable_struct_complex_type_via_optional_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_nullable_struct_complex_type_via_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup))); + + public override Task Project_struct_complex_type_via_required_navigation(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_struct_complex_type_via_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup))); + + public override Task Load_struct_complex_type_after_subquery_on_entity_type(bool async) + => AssertTranslationFailedWithDetails(() => base.Load_struct_complex_type_after_subquery_on_entity_type(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + public override async Task Select_struct_complex_type(bool async) + { + await base.Select_struct_complex_type(async); + + AssertSql( + """ + +"""); + } + + public override async Task Select_nested_struct_complex_type(bool async) + { + await base.Select_nested_struct_complex_type(async); + + AssertSql( + """ + +"""); + } + + public override async Task Select_single_property_on_nested_struct_complex_type(bool async) + { + await base.Select_single_property_on_nested_struct_complex_type(async); + + AssertSql( + """ + +"""); + } + + public override async Task Select_struct_complex_type_Where(bool async) + { + await base.Select_struct_complex_type_Where(async); + + AssertSql( + """ + +"""); + } + + public override Task Select_struct_complex_type_Distinct(bool async) + => AssertTranslationFailed(() => base.Select_struct_complex_type_Distinct(async)); + + public override async Task Struct_complex_type_equals_struct_complex_type(bool async) + { + await base.Struct_complex_type_equals_struct_complex_type(async); + + AssertSql( + """ + +"""); + } + + public override async Task Struct_complex_type_equals_constant(bool async) + { + await base.Struct_complex_type_equals_constant(async); + + AssertSql( + """ + +"""); + } + + public override async Task Struct_complex_type_equals_parameter(bool async) + { + await base.Struct_complex_type_equals_parameter(async); + + AssertSql( + """ + +"""); + } + + public override Task Subquery_over_struct_complex_type(bool async) + => AssertTranslationFailedWithDetails(() => base.Subquery_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Contains_over_struct_complex_type(bool async) + => AssertTranslationFailedWithDetails(() => base.Contains_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Concat_struct_complex_type(bool async) + => AssertTranslationFailed(() => base.Concat_struct_complex_type(async)); // Union not supported + + public override Task Concat_entity_type_containing_struct_complex_property(bool async) + => AssertTranslationFailed(() => base.Concat_entity_type_containing_struct_complex_property(async)); // Union not supported + + public override Task Union_entity_type_containing_struct_complex_property(bool async) + => AssertTranslationFailed(() => base.Union_entity_type_containing_struct_complex_property(async)); // Union not supported + + public override Task Union_struct_complex_type(bool async) + => AssertTranslationFailed(() => base.Union_struct_complex_type(async)); // Union not supported + + public override Task Concat_property_in_struct_complex_type(bool async) + => AssertTranslationFailed(() => base.Concat_property_in_struct_complex_type(async)); // Union not supported + + public override Task Union_property_in_struct_complex_type(bool async) + => AssertTranslationFailed(() => base.Union_property_in_struct_complex_type(async)); // Union not supported + + public override Task Concat_two_different_struct_complex_type(bool async) + => AssertTranslationFailed(() => base.Concat_two_different_struct_complex_type(async)); // Union not supported + + public override Task Union_two_different_struct_complex_type(bool async) + => AssertTranslationFailed(() => base.Union_two_different_struct_complex_type(async)); // Union not supported + + public override Task Project_same_entity_with_nested_complex_type_twice_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_nested_complex_type_twice_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_entity_with_nested_complex_type_twice_with_double_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_nested_complex_type_twice_with_double_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_entity_with_struct_nested_complex_type_twice_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_struct_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_struct_nested_complex_type_twice_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_struct_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_entity_with_struct_nested_complex_type_twice_with_double_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_struct_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Project_same_struct_nested_complex_type_twice_with_double_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_same_struct_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Union_of_same_entity_with_nested_complex_type_projected_twice_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Union_of_same_entity_with_nested_complex_type_projected_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Union_of_same_entity_with_nested_complex_type_projected_twice_with_double_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Union_of_same_entity_with_nested_complex_type_projected_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Union_of_same_nested_complex_type_projected_twice_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Union_of_same_nested_complex_type_projected_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) + => AssertTranslationFailedWithDetails(() => base.Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + public override Task Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) + => AssertTranslationFailedWithDetails(() => base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + + #region GroupBy + + [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")] + public override async Task GroupBy_over_property_in_nested_complex_type(bool async) + { + await base.GroupBy_over_property_in_nested_complex_type(async); + + AssertSql( + """ + +"""); + } + + [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")] + public override async Task GroupBy_over_complex_type(bool async) + { + await base.GroupBy_over_complex_type(async); + + AssertSql( + """ + +"""); + } + + [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")] + public override async Task GroupBy_over_nested_complex_type(bool async) + { + await base.GroupBy_over_nested_complex_type(async); + + AssertSql( + """ + +"""); + } + + [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")] + public override async Task Entity_with_complex_type_with_group_by_and_first(bool async) + { + await base.Entity_with_complex_type_with_group_by_and_first(async); + + AssertSql( + """ + +"""); + } + + #endregion GroupBy + + public override Task Projecting_property_of_complex_type_using_left_join_with_pushdown(bool async) + => AssertTranslationFailedWithDetails(() => base.Projecting_property_of_complex_type_using_left_join_with_pushdown(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup))); + + public override Task Projecting_complex_from_optional_navigation_using_conditional(bool async) + => AssertTranslationFailedWithDetails(() => base.Projecting_complex_from_optional_navigation_using_conditional(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup))); + + public override Task Project_entity_with_complex_type_pushdown_and_then_left_join(bool async) + => AssertTranslationFailedWithDetails(() => base.Project_entity_with_complex_type_pushdown_and_then_left_join(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class ComplexTypeQueryCosmosFixture : ComplexTypeQueryFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder) + .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined).Ignore(CoreEventId.MappedEntityTypeIgnoredWarning)); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + modelBuilder.Entity().ToContainer("Customers"); + modelBuilder.Entity().ToContainer("CustomerGroups"); + modelBuilder.Entity().ToContainer("ValuedCustomers"); + modelBuilder.Entity().ToContainer("ValuedCustomerGroups"); + } + } +} diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index e336dd34498..6bdaed35199 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -18,6 +18,7 @@ public virtual async Task Complex_type_equals_parameter_with_nested_types_with_p context.AddRange( new Context33449.EntityType { + Id = 1, ComplexContainer = new Context33449.ComplexContainer { Id = 1, @@ -132,14 +133,17 @@ public virtual async Task Optional_complex_type_with_discriminator() context.AddRange( new ContextShadowDiscriminator.EntityType { + Id = 1, AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "Non-null" } }, new ContextShadowDiscriminator.EntityType { + Id = 2, AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = null } }, new ContextShadowDiscriminator.EntityType { + Id = 3, AllOptionalsComplexType = null } ); @@ -186,6 +190,7 @@ public virtual async Task Non_optional_complex_type_with_all_nullable_properties context.Add( new Context37162.EntityType { + Id = 1, NonOptionalComplexType = new Context37162.ComplexTypeWithAllNulls { // All properties are null @@ -231,14 +236,15 @@ public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_pr var contextFactory = await InitializeAsync( seed: context => { - context.Add( - new Context37337.EntityType + var entity = new Context37337.EntityType + { + Prop = new Context37337.OptionalComplexProperty { - Prop = new Context37337.OptionalComplexProperty - { - OptionalValue = true - } - }); + OptionalValue = true + } + }; + context.Add(entity); + context.Entry(entity).Property("CreatedBy").CurrentValue = "Seeder"; return context.SaveChangesAsync(); }); @@ -250,9 +256,12 @@ public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_pr var entity = entities[0]; Assert.NotNull(entity.Prop); Assert.True(entity.Prop.OptionalValue); + + var entry = context.Entry(entity); + Assert.Equal("Seeder", entry.Property("CreatedBy").CurrentValue); } - private class Context37337(DbContextOptions options) : DbContext(options) + protected class Context37337(DbContextOptions options) : DbContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { From 003383d2767f0c1911c6823b9df671c596df8b76 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:38:53 +0100 Subject: [PATCH 04/28] Rewrite some baselines --- .../Query/ComplexTypeQueryCosmosTest.cs | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index 99ce4983c21..66ead04ae19 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -72,7 +72,9 @@ public override Task Select_complex_type_Where(bool async) AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (c["ShippingAddress"]["ZipCode"] = 7728) """); }); @@ -86,8 +88,10 @@ public override Task Complex_type_equals_complex_type(bool async) AssertSql( """ - - """); +SELECT VALUE c +FROM root c +WHERE (((c["ShippingAddress"] = null) AND (c["BillingAddress"] = null)) OR ((c["BillingAddress"] != null) AND (((((((c["ShippingAddress"]["Country"] = null) AND (c["BillingAddress"]["Country"] = null)) OR ((c["BillingAddress"]["Country"] != null) AND ((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"])))) AND (c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"])) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["Tags"] = c["BillingAddress"]["Tags"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])))) +"""); }); public override Task Complex_type_equals_constant(bool async) @@ -97,8 +101,10 @@ public override Task Complex_type_equals_constant(bool async) AssertSql( """ - - """); +SELECT VALUE c +FROM root c +WHERE ((((((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States")) AND (c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road")) AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["Tags"] = ["foo","bar"])) AND (c["ShippingAddress"]["ZipCode"] = 38654)) +"""); }); public override Task Complex_type_equals_parameter(bool async) @@ -108,8 +114,19 @@ public override Task Complex_type_equals_parameter(bool async) AssertSql( """ +@entity_equality_address='{}' +@entity_equality_entity_equality_address_Country='{}' +@entity_equality_entity_equality_address_Country_Code='US' +@entity_equality_entity_equality_address_Country_FullName='United States' +@entity_equality_address_AddressLine1='804 S. Lakeshore Road' +@entity_equality_address_AddressLine2=null +@entity_equality_address_Tags='["foo","bar"]' +@entity_equality_address_ZipCode='38654' - """); +SELECT VALUE c +FROM root c +WHERE (((c["ShippingAddress"] = null) AND (@entity_equality_address = null)) OR ((@entity_equality_address != null) AND (((((((c["ShippingAddress"]["Country"] = null) AND (@entity_equality_entity_equality_address_Country = null)) OR ((@entity_equality_entity_equality_address_Country != null) AND ((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName)))) AND (c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1)) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["Tags"] = @entity_equality_address_Tags)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)))) +"""); }); public override Task Subquery_over_complex_type(bool async) @@ -148,7 +165,9 @@ public override async Task Filter_on_property_inside_struct_complex_type(bool as AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (c["ShippingAddress"]["ZipCode"] = 7728) """); } @@ -158,7 +177,9 @@ public override async Task Filter_on_property_inside_nested_struct_complex_type( AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (c["ShippingAddress"]["Country"]["Code"] = "DE") """); } @@ -192,7 +213,8 @@ public override async Task Select_struct_complex_type(bool async) AssertSql( """ - +SELECT VALUE c +FROM root c """); } @@ -202,7 +224,8 @@ public override async Task Select_nested_struct_complex_type(bool async) AssertSql( """ - +SELECT VALUE c +FROM root c """); } @@ -212,7 +235,8 @@ public override async Task Select_single_property_on_nested_struct_complex_type( AssertSql( """ - +SELECT VALUE c["ShippingAddress"]["Country"]["FullName"] +FROM root c """); } @@ -222,7 +246,9 @@ public override async Task Select_struct_complex_type_Where(bool async) AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (c["ShippingAddress"]["ZipCode"] = 7728) """); } @@ -235,7 +261,9 @@ public override async Task Struct_complex_type_equals_struct_complex_type(bool a AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (((((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"])) AND (c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"])) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) """); } @@ -245,7 +273,9 @@ public override async Task Struct_complex_type_equals_constant(bool async) AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (((((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States")) AND (c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road")) AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["ZipCode"] = 38654)) """); } @@ -255,7 +285,15 @@ public override async Task Struct_complex_type_equals_parameter(bool async) AssertSql( """ +@entity_equality_entity_equality_address_Country_Code='US' +@entity_equality_entity_equality_address_Country_FullName='United States' +@entity_equality_address_AddressLine1='804 S. Lakeshore Road' +@entity_equality_address_AddressLine2=null +@entity_equality_address_ZipCode='38654' +SELECT VALUE c +FROM root c +WHERE (((((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName)) AND (c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1)) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) """); } From 7f7b564c0e478bbd6f799c62c17287907ca731f1 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:44:59 +0100 Subject: [PATCH 05/28] Fix async tests --- .../Query/ComplexTypeQueryCosmosTest.cs | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index 66ead04ae19..bed883ce78e 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -159,7 +159,8 @@ public override Task Concat_two_different_complex_type(bool async) public override Task Union_two_different_complex_type(bool async) => AssertTranslationFailed(() => base.Union_two_different_complex_type(async)); // Union not supported - public override async Task Filter_on_property_inside_struct_complex_type(bool async) + public override Task Filter_on_property_inside_struct_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Filter_on_property_inside_struct_complex_type(async); @@ -169,9 +170,10 @@ SELECT VALUE c FROM root c WHERE (c["ShippingAddress"]["ZipCode"] = 7728) """); - } + }); - public override async Task Filter_on_property_inside_nested_struct_complex_type(bool async) + public override Task Filter_on_property_inside_nested_struct_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Filter_on_property_inside_nested_struct_complex_type(async); @@ -181,7 +183,7 @@ SELECT VALUE c FROM root c WHERE (c["ShippingAddress"]["Country"]["Code"] = "DE") """); - } + }); public override Task Filter_on_property_inside_struct_complex_type_after_subquery(bool async) => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_struct_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); @@ -207,18 +209,20 @@ public override Task Project_struct_complex_type_via_required_navigation(bool as public override Task Load_struct_complex_type_after_subquery_on_entity_type(bool async) => AssertTranslationFailedWithDetails(() => base.Load_struct_complex_type_after_subquery_on_entity_type(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); - public override async Task Select_struct_complex_type(bool async) + public override Task Select_struct_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Select_struct_complex_type(async); AssertSql( - """ + """ SELECT VALUE c FROM root c """); - } + }); - public override async Task Select_nested_struct_complex_type(bool async) + public override Task Select_nested_struct_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Select_nested_struct_complex_type(async); @@ -227,9 +231,10 @@ public override async Task Select_nested_struct_complex_type(bool async) SELECT VALUE c FROM root c """); - } + }); - public override async Task Select_single_property_on_nested_struct_complex_type(bool async) + public override Task Select_single_property_on_nested_struct_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Select_single_property_on_nested_struct_complex_type(async); @@ -238,9 +243,10 @@ public override async Task Select_single_property_on_nested_struct_complex_type( SELECT VALUE c["ShippingAddress"]["Country"]["FullName"] FROM root c """); - } + }); - public override async Task Select_struct_complex_type_Where(bool async) + public override Task Select_struct_complex_type_Where(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Select_struct_complex_type_Where(async); @@ -250,12 +256,13 @@ SELECT VALUE c FROM root c WHERE (c["ShippingAddress"]["ZipCode"] = 7728) """); - } + }); public override Task Select_struct_complex_type_Distinct(bool async) => AssertTranslationFailed(() => base.Select_struct_complex_type_Distinct(async)); - public override async Task Struct_complex_type_equals_struct_complex_type(bool async) + public override Task Struct_complex_type_equals_struct_complex_type(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Struct_complex_type_equals_struct_complex_type(async); @@ -265,9 +272,10 @@ SELECT VALUE c FROM root c WHERE (((((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"])) AND (c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"])) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) """); - } + }); - public override async Task Struct_complex_type_equals_constant(bool async) + public override Task Struct_complex_type_equals_constant(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Struct_complex_type_equals_constant(async); @@ -277,9 +285,10 @@ SELECT VALUE c FROM root c WHERE (((((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States")) AND (c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road")) AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["ZipCode"] = 38654)) """); - } + }); - public override async Task Struct_complex_type_equals_parameter(bool async) + public override Task Struct_complex_type_equals_parameter(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => { await base.Struct_complex_type_equals_parameter(async); @@ -295,7 +304,7 @@ SELECT VALUE c FROM root c WHERE (((((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName)) AND (c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1)) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) """); - } + }); public override Task Subquery_over_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Subquery_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); From 1b4b5cdb67dbdc4dfc36980672fa6c22b8b98c9c Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:46:40 +0100 Subject: [PATCH 06/28] WIP: distinct, contains and null --- ...yableMethodTranslatingExpressionVisitor.cs | 32 ++++++++-- .../CosmosSqlTranslatingExpressionVisitor.cs | 27 ++++---- .../ComplexPropertiesCollectionCosmosTest.cs | 19 ++++-- ...xPropertiesStructuralEqualityCosmosTest.cs | 64 +++++++++++++++---- ...NavigationsStructuralEqualityCosmosTest.cs | 2 +- .../Query/ComplexTypeQueryCosmosTest.cs | 56 ++++++++++------ ...thwindAggregateOperatorsQueryCosmosTest.cs | 6 +- 7 files changed, 148 insertions(+), 58 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 4cebd8c8c5a..8728979516e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Numerics; -using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; @@ -535,9 +533,11 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return null; } - // @TODO: Without this check, this will generate a query with DISTINCT over a complex type. This does work, but it is a bitwise comparison. Meaning if properties are in different order, or there are extra properties, this will not work as expected for EF Distinct. Currently for owned types this will create a subquery because owned types are translated to Include's. The subquery will be checked for isdistinct and return null due to sub query pushdown not implement. - // Are we ok with a bitwise comparison for complex types? If not we need to implement GROUP BY { properties}. However, that could be out of scope for now? Have to discuss - if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IComplexType }) + // DISTINCT applies to the SQL projection. If the shaper extracts a subset of the projection + // (e.g., a property from an entity), DISTINCT would operate on the wrong columns. + // This is valid when the shaper directly binds to projection members without further extraction. + // Cosmos: Projecting out nested documents retrieves the entire document #34067 + if (!IsProjectionCompatibleWithDistinct(source.ShaperExpression)) { return null; } @@ -547,6 +547,28 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return source; } + private static bool IsProjectionCompatibleWithDistinct(Expression shaperExpression) // @TODO: Is there a better way to do this? // @TODO: Check if binding is done on client eval..? + => shaperExpression switch + { + // Structural type shaper binding to the root projection + StructuralTypeShaperExpression { ValueBufferExpression: ProjectionBindingExpression } => true, + + // Direct scalar projection binding + ProjectionBindingExpression => true, + + // Convert wrapping a valid projection + UnaryExpression { NodeType: ExpressionType.Convert, Operand: var operand } => + IsProjectionCompatibleWithDistinct(operand), + + // Anonymous types / DTOs + NewExpression newExpr => newExpr.Arguments.All(IsProjectionCompatibleWithDistinct), + MemberInitExpression memberInit => + IsProjectionCompatibleWithDistinct(memberInit.NewExpression) + && memberInit.Bindings.All(b => b is MemberAssignment ma && IsProjectionCompatibleWithDistinct(ma.Expression)), + + _ => false + }; + /// /// 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 diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 37dd802b860..8fa9d3354d7 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1007,11 +1007,11 @@ private static Expression TryRemoveImplicitConvert(Expression expression) return expression; } + // This is for list.Contains(entity) private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result) { result = null; - // @TODO: support composite keys? Then we can support complex types as well. if (item is not StructuralTypeReferenceExpression itemEntityReference || itemEntityReference.StructuralType is not IEntityType entityType) { return false; @@ -1104,7 +1104,15 @@ private bool TryRewriteEntityEquality( } // Treat type as object for null comparison - var access = new SqlObjectAccessExpression(structuralReference.Object); + + var obj = structuralReference.Object; + if (obj is StructuralTypeShaperExpression { ValueBufferExpression: ProjectionBindingExpression { QueryExpression: SelectExpression select } }) // @TODO: Is this the right way to check if this is a query object reference? + { + obj = new ObjectReferenceExpression(structuralType, select.Sources.Single().Alias); // @TODO: Probably a better way to get the alais we need here.. + } + + var access = new SqlObjectAccessExpression(obj); + // There is an ValueBufferExpression: ProjectionBindingExpression: EmptyProjectionMember as oppose to ValueBufferExpression: c["OptionalAssociate"] (ObjectAccessExpression?) result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object), null)!, typeMappingSource.FindMapping(typeof(bool)))!; return true; } @@ -1142,17 +1150,12 @@ private bool TryRewriteEntityEquality( // Complex type equality else if (structuralType is IComplexType complexType) { - if (complexType.ComplexProperty.IsCollection) + // We need to know here the difference between + // x.Collection == x.Collection (should return null) + // x.Collection[1] == x.Collection[1] (should run below) + // How can we determine the difference? The structuralReference represents the entire complex type in both scenarios, wich is always of type ComplexType and IsCollection true.. + if (structuralReference.Parameter?.ValueBufferExpression is ObjectArrayAccessExpression) // @TODO: Is there a better way to do this? It feels like this might not be the right place. What about CosmosQueryableMethodTranslatingExpressionVisitor? What is the difference again? { - // @TODO: We could compare by: - /* - WHERE ARRAY_LENGTH(c.items) = ARRAY_LENGTH(@items) - AND NOT EXISTS ( - SELECT VALUE i - FROM i IN c.items - WHERE NOT ARRAY_CONTAINS(@items, i, true) - ) - * */ result = null; return false; } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs index 7e5d1b5e165..3d203e83166 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -62,14 +62,25 @@ FROM a IN c["AssociateCollection"] public override Task Distinct() => AssertTranslationFailed(base.Distinct); - public override Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) - => AssertTranslationFailed(() => base.Distinct_projected(queryTrackingBehavior)); + 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_over_projected_nested_collection() - => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); + => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); // Cosmos: Projecting out nested documents retrieves the entire document #34067 public override Task Distinct_over_projected_filtered_nested_collection() - => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); + => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); // Cosmos: Projecting out nested documents retrieves the entire document #34067 #endregion Distinct diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs index d849872b710..77870942ee4 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -27,7 +27,7 @@ FROM root c } public override Task Not_equals() - => AssertTranslationFailed(base.Not_equals); + => AssertTranslationFailed(base.Not_equals); // Complex collection equality... Need ALL support public override async Task Associate_with_inline_null() { @@ -42,7 +42,7 @@ FROM root c } public override Task Associate_with_parameter_null() - => AssertTranslationFailed(base.Associate_with_parameter_null); + => AssertTranslationFailed(base.Associate_with_parameter_null); // Complex collection equality... Need ALL support public override async Task Nested_associate_with_inline_null() { @@ -111,13 +111,13 @@ FROM root c } public override Task Two_nested_collections() - => AssertTranslationFailed(base.Two_nested_collections); + => AssertTranslationFailed(base.Two_nested_collections); // Complex collection equality... Need ALL support public override Task Nested_collection_with_inline() - => AssertTranslationFailed(base.Nested_collection_with_inline); + => AssertTranslationFailed(base.Nested_collection_with_inline); // Complex collection equality... Need ALL support public override Task Nested_collection_with_parameter() - => AssertTranslationFailed(base.Nested_collection_with_parameter); + => AssertTranslationFailed(base.Nested_collection_with_parameter); // Complex collection equality... Need ALL support [ConditionalFact] public override async Task Nullable_value_type_with_null() @@ -136,28 +136,66 @@ FROM root c public override async Task Contains_with_inline() { - // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. - await Assert.ThrowsAsync(() => base.Contains_with_inline()); + await base.Contains_with_inline(); - AssertSql(); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE EXISTS ( + SELECT 1 + FROM n IN c["RequiredAssociate"]["NestedCollection"] + WHERE (((((n["Id"] = 1002) AND (n["Int"] = 8)) AND (n["Ints"] = [1,2,3])) AND (n["Name"] = "Root1_RequiredAssociate_NestedCollection_1")) AND (n["String"] = "foo"))) +"""); } public override async Task Contains_with_parameter() { + await base.Contains_with_parameter(); + + AssertSql( + """ +@entity_equality_nested='{}' +@entity_equality_nested_Id='1002' +@entity_equality_nested_Int='8' +@entity_equality_nested_Ints='[1,2,3]' +@entity_equality_nested_Name='Root1_RequiredAssociate_NestedCollection_1' +@entity_equality_nested_String='foo' - await Assert.ThrowsAsync(base.Contains_with_parameter); +SELECT VALUE c +FROM root c +WHERE EXISTS ( + SELECT 1 + FROM n IN c["RequiredAssociate"]["NestedCollection"] + WHERE (((n = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((n["Id"] = @entity_equality_nested_Id) AND (n["Int"] = @entity_equality_nested_Int)) AND (n["Ints"] = @entity_equality_nested_Ints)) AND (n["Name"] = @entity_equality_nested_Name)) AND (n["String"] = @entity_equality_nested_String))))) +"""); } public override async Task Contains_with_operators_composed_on_the_collection() { + await base.Contains_with_operators_composed_on_the_collection(); + + AssertSql( + """ +@get_Item_Int='106' +@entity_equality_get_Item='{}' +@entity_equality_get_Item_Id='3003' +@entity_equality_get_Item_Int='108' +@entity_equality_get_Item_Ints='[8,9,109]' +@entity_equality_get_Item_Name='Root3_RequiredAssociate_NestedCollection_2' +@entity_equality_get_Item_String='foo104' - await Assert.ThrowsAsync(base.Contains_with_operators_composed_on_the_collection); +SELECT VALUE c +FROM root c +WHERE EXISTS ( + SELECT 1 + FROM n IN c["RequiredAssociate"]["NestedCollection"] + WHERE ((n["Int"] > @get_Item_Int) AND (((n = null) AND (@entity_equality_get_Item = null)) OR ((@entity_equality_get_Item != null) AND (((((n["Id"] = @entity_equality_get_Item_Id) AND (n["Int"] = @entity_equality_get_Item_Int)) AND (n["Ints"] = @entity_equality_get_Item_Ints)) AND (n["Name"] = @entity_equality_get_Item_Name)) AND (n["String"] = @entity_equality_get_Item_String)))))) +"""); } public override async Task Contains_with_nested_and_composed_operators() - { - await Assert.ThrowsAsync(base.Contains_with_nested_and_composed_operators); - } + => await AssertTranslationFailed(base.Contains_with_nested_and_composed_operators); // Complex collection equality... Need ALL support #endregion Contains diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index b19b56da69b..2bd23b53a8b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -49,7 +49,7 @@ WHERE false } public override Task Associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Associate_with_inline_null()); + => Assert.ThrowsAsync(() => base.Associate_with_inline_null()); public override Task Associate_with_parameter_null() => Assert.ThrowsAsync(() => base.Associate_with_parameter_null()); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index bed883ce78e..ae2a5e4a662 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -78,8 +78,8 @@ FROM root c """); }); - public override Task Select_complex_type_Distinct(bool async) // @TODO: Distinct should fail.. - => AssertTranslationFailed(() => base.Select_complex_type_Distinct(async)); + public override Task Select_complex_type_Distinct(bool async) + => AssertTranslationFailed(() => base.Select_complex_type_Distinct(async)); // #34067 public override Task Complex_type_equals_complex_type(bool async) => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => @@ -135,29 +135,37 @@ public override Task Subquery_over_complex_type(bool async) public override Task Contains_over_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Contains_over_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_complex_type(bool async) - => AssertTranslationFailed(() => base.Concat_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_entity_type_containing_complex_property(bool async) - => AssertTranslationFailed(() => base.Concat_entity_type_containing_complex_property(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_entity_type_containing_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_entity_type_containing_complex_property(bool async) - => AssertTranslationFailed(() => base.Union_entity_type_containing_complex_property(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_entity_type_containing_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_complex_type(bool async) - => AssertTranslationFailed(() => base.Union_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_property_in_complex_type(bool async) - => AssertTranslationFailed(() => base.Concat_property_in_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_property_in_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_property_in_complex_type(bool async) - => AssertTranslationFailed(() => base.Union_property_in_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_property_in_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_two_different_complex_type(bool async) - => AssertTranslationFailed(() => base.Concat_two_different_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_two_different_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_two_different_complex_type(bool async) - => AssertTranslationFailed(() => base.Union_two_different_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_two_different_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); public override Task Filter_on_property_inside_struct_complex_type(bool async) => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => @@ -259,7 +267,7 @@ FROM root c }); public override Task Select_struct_complex_type_Distinct(bool async) - => AssertTranslationFailed(() => base.Select_struct_complex_type_Distinct(async)); + => AssertTranslationFailed(() => base.Select_struct_complex_type_Distinct(async)); // #34067 public override Task Struct_complex_type_equals_struct_complex_type(bool async) => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => @@ -312,29 +320,37 @@ public override Task Subquery_over_struct_complex_type(bool async) public override Task Contains_over_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Contains_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_struct_complex_type(bool async) - => AssertTranslationFailed(() => base.Concat_struct_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_entity_type_containing_struct_complex_property(bool async) - => AssertTranslationFailed(() => base.Concat_entity_type_containing_struct_complex_property(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_entity_type_containing_struct_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_entity_type_containing_struct_complex_property(bool async) - => AssertTranslationFailed(() => base.Union_entity_type_containing_struct_complex_property(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_entity_type_containing_struct_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_struct_complex_type(bool async) - => AssertTranslationFailed(() => base.Union_struct_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_property_in_struct_complex_type(bool async) - => AssertTranslationFailed(() => base.Concat_property_in_struct_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_property_in_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_property_in_struct_complex_type(bool async) - => AssertTranslationFailed(() => base.Union_property_in_struct_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_property_in_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_two_different_struct_complex_type(bool async) - => AssertTranslationFailed(() => base.Concat_two_different_struct_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Concat_two_different_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_two_different_struct_complex_type(bool async) - => AssertTranslationFailed(() => base.Union_two_different_struct_complex_type(async)); // Union not supported + => AssertTranslationFailedWithDetails(() => base.Union_two_different_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); public override Task Project_same_entity_with_nested_complex_type_twice_with_pushdown(bool async) => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); @@ -373,7 +389,7 @@ public override Task Union_of_same_nested_complex_type_projected_twice_with_doub => AssertTranslationFailedWithDetails(() => base.Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); public override Task Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) - => AssertTranslationFailedWithDetails(() => base.Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); + => AssertTranslationFailed(() => base.Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async)); public override Task Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) => AssertTranslationFailedWithDetails(() => base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs index 62db3a71ee3..42779b53ebd 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs @@ -5,6 +5,7 @@ using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; namespace Microsoft.EntityFrameworkCore.Query; @@ -1161,7 +1162,7 @@ FROM root c """); }); - [ConditionalTheory(Skip = "Fails on CI #27688")] + [SkipOnCiCondition(SkipReason = "Fails on CI #27688")] public override Task Distinct_Scalar(bool async) => Fixture.NoSyncTest( async, async a => @@ -1170,9 +1171,8 @@ public override Task Distinct_Scalar(bool async) AssertSql( """ -SELECT DISTINCT c[""City""] +SELECT DISTINCT VALUE c["City"] FROM root c -WHERE (c[""$type""] = ""Customer"") """); }); From bbc34776fc45bd18ecbdae5082956ebb6683eab7 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:59:18 +0100 Subject: [PATCH 07/28] Fix test do not explicitly set int ids in base test but use value generator for cosmos --- .../Query/AdHocComplexTypeQueryCosmosTest.cs | 26 ++++++++++++++++++- .../Query/AdHocCosmosTestHelpers.cs | 24 +++++++++++++++++ ...omplexPropertiesMiscellaneousCosmosTest.cs | 3 --- .../Query/AdHocComplexTypeQueryTestBase.cs | 5 ---- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs index 832f5b0c8f4..168dee2c8c7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -27,11 +27,11 @@ OFFSET 0 LIMIT 2 """); } - [ConditionalFact(Skip = "#34067: Cosmos: Projecting out nested documents retrieves the entire document")] public override async Task Projecting_complex_property_does_not_auto_include_owned_types() { await base.Projecting_complex_property_does_not_auto_include_owned_types(); + // #34067: Cosmos: Projecting out nested documents retrieves the entire document AssertSql( """ SELECT VALUE c @@ -88,4 +88,28 @@ protected TestSqlLoggerFactory TestSqlLoggerFactory private void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); + + protected override Task> InitializeAsync( + Action? onModelCreating = null, + Action? onConfiguring = null, + Func? addServices = null, + Action? configureConventions = null, + Func? seed = null, + Func? shouldLogCategory = null, + Func? createTestStore = null, + bool usePooling = true, + bool useServiceProvider = true) + => base.InitializeAsync(model => + { + onModelCreating?.Invoke(model); + AdHocCosmosTestHelpers.UseTestAutoIncrementIntIds(model); + }, + onConfiguring, + addServices, + configureConventions, + seed, + shouldLogCategory, + createTestStore, + usePooling, + useServiceProvider); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs index d308f79f4dc..7af7cd789f2 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs @@ -3,6 +3,7 @@ using System.Net; using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -11,6 +12,29 @@ namespace Microsoft.EntityFrameworkCore.Query; public class AdHocCosmosTestHelpers { + public static void UseTestAutoIncrementIntIds(ModelBuilder modelBuilder) + { + foreach (var rootDocument in modelBuilder.Model.GetEntityTypes().Where(x => x.IsDocumentRoot())) + { + var primaryKey = rootDocument.FindPrimaryKey(); + + if (primaryKey != null && primaryKey.Properties.Count == 1 && primaryKey.Properties[0].ClrType == typeof(int)) + { + var valueGenerator = new TestAutoIncerementIntValueGenerator(); + primaryKey.Properties[0].SetValueGeneratorFactory((_, _) => valueGenerator); + } + } + } + + private class TestAutoIncerementIntValueGenerator : ValueGenerator + { + private int i; + + public override bool GeneratesTemporaryValues => false; + + public override int Next(EntityEntry entry) => Interlocked.Increment(ref i); + } + public static async Task CreateCustomEntityHelperAsync( Container container, string json, diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs index ed72d52fdc4..d625d726dac 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs @@ -75,7 +75,6 @@ FROM root c public override async Task Where_HasValue_on_nullable_value_type() { - // @TODO: Structural equality. await base.Where_HasValue_on_nullable_value_type(); AssertSql(""" @@ -83,8 +82,6 @@ SELECT VALUE c FROM root c WHERE (c["OptionalAssociate"] != null) """); - //var ex = await Assert.ThrowsAsync(() => base.Where_HasValue_on_nullable_value_type()); - //Assert.Equal(CoreStrings.EntityEqualityOnKeylessEntityNotSupported("!=", "ValueRootEntity.OptionalAssociate#ValueAssociateType"), ex.Message); } #endregion Value types diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index 6bdaed35199..72f8e94a72e 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -18,7 +18,6 @@ public virtual async Task Complex_type_equals_parameter_with_nested_types_with_p context.AddRange( new Context33449.EntityType { - Id = 1, ComplexContainer = new Context33449.ComplexContainer { Id = 1, @@ -133,17 +132,14 @@ public virtual async Task Optional_complex_type_with_discriminator() context.AddRange( new ContextShadowDiscriminator.EntityType { - Id = 1, AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "Non-null" } }, new ContextShadowDiscriminator.EntityType { - Id = 2, AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = null } }, new ContextShadowDiscriminator.EntityType { - Id = 3, AllOptionalsComplexType = null } ); @@ -190,7 +186,6 @@ public virtual async Task Non_optional_complex_type_with_all_nullable_properties context.Add( new Context37162.EntityType { - Id = 1, NonOptionalComplexType = new Context37162.ComplexTypeWithAllNulls { // All properties are null From de6328fc65d88293c89c5e6a747fbd7c999227f1 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:09:57 +0100 Subject: [PATCH 08/28] Improve object retrieval for null comparison --- ...yableMethodTranslatingExpressionVisitor.cs | 4 ++-- .../CosmosSqlTranslatingExpressionVisitor.cs | 20 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 8728979516e..87465794d4b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -534,8 +534,8 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou } // DISTINCT applies to the SQL projection. If the shaper extracts a subset of the projection - // (e.g., a property from an entity), DISTINCT would operate on the wrong columns. - // This is valid when the shaper directly binds to projection members without further extraction. + // (e.g., a property from an entity), DISTINCT would operate on the wrong type. + // Thus we can only apply distinct when the shaper directly binds to projection members without further extraction. // Cosmos: Projecting out nested documents retrieves the entire document #34067 if (!IsProjectionCompatibleWithDistinct(source.ShaperExpression)) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 8fa9d3354d7..0b0fb41d334 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1007,12 +1007,12 @@ private static Expression TryRemoveImplicitConvert(Expression expression) return expression; } - // This is for list.Contains(entity) private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result) { result = null; - if (item is not StructuralTypeReferenceExpression itemEntityReference || itemEntityReference.StructuralType is not IEntityType entityType) + if (item is not StructuralTypeReferenceExpression itemEntityReference || + itemEntityReference.StructuralType is not IEntityType entityType) // #36468 ? { return false; } @@ -1096,23 +1096,21 @@ private bool TryRewriteEntityEquality( // Null equality if (IsNullSqlConstantExpression(compareReference)) { - if (structuralType is IEntityType entityType1 && entityType1.IsDocumentRoot() && structuralReference.Subquery == null) + if (structuralType is IEntityType entityType1 && entityType1.IsDocumentRoot()) { // Document root can never be be null result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); return true; } - // Treat type as object for null comparison - - var obj = structuralReference.Object; - if (obj is StructuralTypeShaperExpression { ValueBufferExpression: ProjectionBindingExpression { QueryExpression: SelectExpression select } }) // @TODO: Is this the right way to check if this is a query object reference? + var obj = structuralReference switch { - obj = new ObjectReferenceExpression(structuralType, select.Sources.Single().Alias); // @TODO: Probably a better way to get the alais we need here.. - } + { Parameter: { } shaper } => Visit(shaper.ValueBufferExpression), + { Subquery: not null } => throw new NotImplementedException("Null comparison on structural type coming out of subquery"), + _ => throw new UnreachableException(), + }; var access = new SqlObjectAccessExpression(obj); - // There is an ValueBufferExpression: ProjectionBindingExpression: EmptyProjectionMember as oppose to ValueBufferExpression: c["OptionalAssociate"] (ObjectAccessExpression?) result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object), null)!, typeMappingSource.FindMapping(typeof(bool)))!; return true; } @@ -1197,6 +1195,7 @@ private bool TryRewriteEntityEquality( ? Expression.AndAlso(l, r) : Expression.OrElse(l, r)); + // Maybe only if structuralReference.StructuralType is nullable? But we can't really determine that, unless collections can not contain null. Then retrieving alias in subquery wouldn't be needed for null comparison translation above if (compareReference.Type.IsNullableType() && compareReference is not SqlConstantExpression { Value: not null } && (structuralReference.StructuralType is not IEntityType entityType1 || !entityType1.IsDocumentRoot())) // Document can never be null so don't compare document to null { @@ -1365,7 +1364,6 @@ private StructuralTypeReferenceExpression(StructuralTypeReferenceExpression type StructuralType = structuralType; } - public Expression Object => (Expression?)Parameter ?? Subquery ?? throw new UnreachableException(); public new StructuralTypeShaperExpression? Parameter { get; } public ShapedQueryExpression? Subquery { get; } public ITypeBase StructuralType { get; } From 607a921097f8316499cc2b5705651a27602e00b8 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:27:04 +0100 Subject: [PATCH 09/28] Distinct detection --- ...osmosProjectionBindingExpressionVisitor.cs | 1 + ...yableMethodTranslatingExpressionVisitor.cs | 30 +++---------------- .../Internal/Expressions/SelectExpression.cs | 17 +++++++++++ .../ComplexPropertiesCollectionCosmosTest.cs | 4 +-- .../OwnedNavigationsProjectionCosmosTest.cs | 6 ++++ .../Query/ComplexTypeQueryCosmosTest.cs | 4 +-- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 04ce3273e59..e56e3601df4 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -133,6 +133,7 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio var translation = _sqlTranslator.Translate(expression); if (translation == null) { + _selectExpression.IndicateClientProjection(); return base.Visit(expression); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 87465794d4b..81d1f2964d1 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -533,11 +533,11 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return null; } - // DISTINCT applies to the SQL projection. If the shaper extracts a subset of the projection - // (e.g., a property from an entity), DISTINCT would operate on the wrong type. - // Thus we can only apply distinct when the shaper directly binds to projection members without further extraction. + // We're extracting a property from a materialized structural type. + // If DISTINCT was applied, this is incorrect because SQL DISTINCT operates on the full + // structural type, but the shaper extracts only a subset of that data. // Cosmos: Projecting out nested documents retrieves the entire document #34067 - if (!IsProjectionCompatibleWithDistinct(source.ShaperExpression)) + if (select.UsesClientProjection) { return null; } @@ -547,28 +547,6 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return source; } - private static bool IsProjectionCompatibleWithDistinct(Expression shaperExpression) // @TODO: Is there a better way to do this? // @TODO: Check if binding is done on client eval..? - => shaperExpression switch - { - // Structural type shaper binding to the root projection - StructuralTypeShaperExpression { ValueBufferExpression: ProjectionBindingExpression } => true, - - // Direct scalar projection binding - ProjectionBindingExpression => true, - - // Convert wrapping a valid projection - UnaryExpression { NodeType: ExpressionType.Convert, Operand: var operand } => - IsProjectionCompatibleWithDistinct(operand), - - // Anonymous types / DTOs - NewExpression newExpr => newExpr.Arguments.All(IsProjectionCompatibleWithDistinct), - MemberInitExpression memberInit => - IsProjectionCompatibleWithDistinct(memberInit.NewExpression) - && memberInit.Bindings.All(b => b is MemberAssignment ma && IsProjectionCompatibleWithDistinct(ma.Expression)), - - _ => false - }; - /// /// 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 diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index a169168f2e8..b8069a7678e 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -167,6 +167,14 @@ public IReadOnlyList Orderings /// public bool IsDistinct { get; private set; } + /// + /// 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 bool UsesClientProjection { get; private set; } + /// /// 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 @@ -323,6 +331,15 @@ private int AddToProjection(Expression expression, string? alias) public void ApplyDistinct() => IsDistinct = true; + /// + /// 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 void IndicateClientProjection() + => UsesClientProjection = true; + /// /// 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 diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs index 3d203e83166..b0cd0c3c51d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -77,10 +77,10 @@ ORDER BY c["Id"] } public override Task Distinct_over_projected_nested_collection() - => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); // Cosmos: Projecting out nested documents retrieves the entire document #34067 + => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); public override Task Distinct_over_projected_filtered_nested_collection() - => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); // Cosmos: Projecting out nested documents retrieves the entire document #34067 + => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); #endregion Distinct diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs index 1ca8de4b054..c08f9f90024 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs @@ -102,6 +102,12 @@ FROM root c } } + [ConditionalFact] + public Task Select_distinct_associate() + => AssertTranslationFailed(() => AssertQuery( + ss => ss.Set().Select(x => x.RequiredAssociate).Distinct(), + queryTrackingBehavior: QueryTrackingBehavior.NoTracking)); + public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) { await base.Select_optional_associate(queryTrackingBehavior); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index ae2a5e4a662..f54f17271c8 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -78,8 +78,8 @@ FROM root c """); }); - public override Task Select_complex_type_Distinct(bool async) - => AssertTranslationFailed(() => base.Select_complex_type_Distinct(async)); // #34067 + public override async Task Select_complex_type_Distinct(bool async) + => await AssertTranslationFailed(async () => await base.Select_complex_type_Distinct(async)); // Cosmos: Projecting out nested documents retrieves the entire document #34067 public override Task Complex_type_equals_complex_type(bool async) => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) => From 04571436bec48637025a8df00cc6c8c694286b78 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:16:52 +0100 Subject: [PATCH 10/28] WIP some docs for question --- .../Internal/CosmosSqlTranslatingExpressionVisitor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 0b0fb41d334..d47d74d152f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1151,8 +1151,11 @@ private bool TryRewriteEntityEquality( // We need to know here the difference between // x.Collection == x.Collection (should return null) // x.Collection[1] == x.Collection[1] (should run below) - // How can we determine the difference? The structuralReference represents the entire complex type in both scenarios, wich is always of type ComplexType and IsCollection true.. - if (structuralReference.Parameter?.ValueBufferExpression is ObjectArrayAccessExpression) // @TODO: Is there a better way to do this? It feels like this might not be the right place. What about CosmosQueryableMethodTranslatingExpressionVisitor? What is the difference again? + // I think the problem is that this would need to be translated into an All expression, + // which would be handled by CosmosQueryableMethodTranslatingExpressionVisitor, so the check has to live there kinda. + if ((structuralReference.Parameter ?? + structuralReference.Parameter ?? + throw new UnreachableException()).ValueBufferExpression is ObjectArrayAccessExpression) // @TODO: Is there a better way to do this? It feels like this might not be the right place. { result = null; return false; From 5ac80e0e684af4cf6178aea3458a2fafcc47be42 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:24:54 +0100 Subject: [PATCH 11/28] WIP: Match code structure of relational where possible --- ...ingExpressionVisitor.StructuralEquality.cs | 369 ++++++++++++++++++ .../CosmosSqlTranslatingExpressionVisitor.cs | 317 +-------------- .../Query/AdHocComplexTypeQueryCosmosTest.cs | 7 +- ...xPropertiesStructuralEqualityCosmosTest.cs | 36 +- ...NavigationsStructuralEqualityCosmosTest.cs | 26 +- .../Query/ComplexTypeQueryCosmosTest.cs | 22 +- .../NorthwindMiscellaneousQueryCosmosTest.cs | 20 +- 7 files changed, 446 insertions(+), 351 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs new file mode 100644 index 00000000000..6dc80e1a963 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -0,0 +1,369 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +public partial class CosmosSqlTranslatingExpressionVisitor +{ + private const string RuntimeParameterPrefix = "entity_equality_"; + + private static readonly MethodInfo ParameterPropertyValueExtractorMethod = + typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!; + + private static readonly MethodInfo ParameterValueExtractorMethod = + typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; + + private static readonly MethodInfo ParameterListValueExtractorMethod = + typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor))!; + + private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result) + { + result = null; + + if (item is not StructuralTypeReferenceExpression itemEntityReference || + itemEntityReference.StructuralType is not IEntityType entityType) // #36468 + { + return false; + } + + var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; + + switch (primaryKeyProperties) + { + case null: + throw new InvalidOperationException( + CoreStrings.EntityEqualityOnKeylessEntityNotSupported( + nameof(Queryable.Contains), entityType.DisplayName())); + + case { Count: > 1 }: + throw new InvalidOperationException( + CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported( + nameof(Queryable.Contains), entityType.DisplayName())); + } + + var property = primaryKeyProperties[0]; + Expression rewrittenSource; + switch (source) + { + case SqlConstantExpression sqlConstantExpression: + var values = (IEnumerable)sqlConstantExpression.Value!; + var propertyValueList = + (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(property.ClrType.MakeNullable()))!; + var propertyGetter = property.GetGetter(); + foreach (var value in values) + { + propertyValueList.Add(propertyGetter.GetClrValue(value)); + } + + rewrittenSource = Expression.Constant(propertyValueList); + break; + + case SqlParameterExpression sqlParameterExpression: + var lambda = Expression.Lambda( + Expression.Call( + ParameterListValueExtractorMethod.MakeGenericMethod(entityType.ClrType, property.ClrType.MakeNullable()), + QueryCompilationContext.QueryContextParameter, + Expression.Constant(sqlParameterExpression.Name, typeof(string)), + Expression.Constant(property, typeof(IProperty))), + QueryCompilationContext.QueryContextParameter + ); + + var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}"; + + rewrittenSource = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + break; + + default: + return false; + } + + result = Visit( + Expression.Call( + EnumerableMethods.Contains.MakeGenericMethod(property.ClrType.MakeNullable()), + rewrittenSource, + CreatePropertyAccessExpression(item, property))); + + return true; + } + + private bool TryRewriteStructuralTypeEquality( + ExpressionType nodeType, + Expression left, + Expression right, + bool equalsMethod, + [NotNullWhen(true)] out SqlExpression? result) + { + var leftReference = left as StructuralTypeReferenceExpression; + var rightReference = right as StructuralTypeReferenceExpression; + var reference = leftReference ?? rightReference; + if (reference == null) + { + result = null; + return false; + } + + var leftShaper = leftReference?.Parameter + ?? (StructuralTypeShaperExpression?)(leftReference?.Subquery)?.ShaperExpression; + var rightShaper = rightReference?.Parameter + ?? (StructuralTypeShaperExpression?)(rightReference?.Subquery)?.ShaperExpression; + var shaper = leftShaper ?? rightShaper ?? throw new UnreachableException(); + + if (IsNullSqlConstantExpression(left) + || IsNullSqlConstantExpression(right)) + { + var nullComparedStructuralType = reference.StructuralType; + if (nullComparedStructuralType is IEntityType entityType1 && entityType1.IsDocumentRoot()) + { + // Document root can never be be null + result = sqlExpressionFactory.Constant(nodeType != ExpressionType.Equal); + return true; + } + + if (!shaper.IsNullable) + { + result = (SqlExpression)Visit(Expression.Constant(nodeType != ExpressionType.Equal)); + return true; + } + + var access = new SqlObjectAccessExpression(Visit(shaper.ValueBufferExpression)); // @TODO + result = sqlExpressionFactory.MakeBinary( + nodeType, + access, + sqlExpressionFactory.Constant(null, typeof(object))!, + typeMappingSource.FindMapping(typeof(bool)))!; + return true; + } + + var leftStructuralType = leftReference?.StructuralType; + var rightStructuralType = rightReference?.StructuralType; + var structuralType = reference.StructuralType; + + Check.DebugAssert(structuralType != null, "We checked that at least one side is an entity type"); + + switch (structuralType) + { + case IEntityType entityType: + return TryRewriteEntityEquality(entityType, out result); + case IComplexType complexType: + return TryRewriteComplexTypeEquality( + complexType, out result); + } + + result = null; + return false; + + bool TryRewriteEntityEquality(IEntityType entityType, [NotNullWhen(true)] out SqlExpression? result) + { + if (leftStructuralType != null + && rightStructuralType != null + && leftStructuralType.GetRootType() != rightStructuralType.GetRootType()) + { + result = sqlExpressionFactory.Constant(false); + return true; + } + + var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; + if (primaryKeyProperties == null) + { + throw new InvalidOperationException( + CoreStrings.EntityEqualityOnKeylessEntityNotSupported( + nodeType == ExpressionType.Equal + ? equalsMethod ? nameof(object.Equals) : "==" + : equalsMethod + ? "!" + nameof(object.Equals) + : "!=", + structuralType.DisplayName())); + } + + if (primaryKeyProperties.Count > 1 + && (leftReference?.Subquery != null + || rightReference?.Subquery != null)) + { + throw new InvalidOperationException( + CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported( + nodeType == ExpressionType.Equal + ? equalsMethod ? nameof(object.Equals) : "==" + : equalsMethod + ? "!" + nameof(object.Equals) + : "!=", + structuralType.DisplayName())); + } + + result = Visit( + primaryKeyProperties.Select(p => Expression.MakeBinary(nodeType, + CreatePropertyAccessExpression(left, p), + CreatePropertyAccessExpression(right, p))) + .Aggregate((l, r) => nodeType == ExpressionType.Equal + ? Expression.AndAlso(l, r) + : Expression.OrElse(l, r))) as SqlExpression; + + return result is not null; + } + + bool TryRewriteComplexTypeEquality(IComplexType complexType, [NotNullWhen(true)] out SqlExpression? result) + { + if (leftStructuralType is not null + && rightStructuralType is not null + && leftStructuralType.ClrType != rightStructuralType.ClrType) + { + // Currently only support comparing complex types of the same CLR type. + // We could allow any case where the complex types have the same properties (some may be shadow). + result = null; + return false; + } + + // Generate an expression that compares each property on the left to the same property on the right; this needs to recursively + // include all properties in nested complex types. + var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!; + SqlExpression? comparisons = null; + + if (!TryGeneratePropertyComparisons(ref comparisons)) + { + result = null; + return false; + } + + result = comparisons; + return true; + + bool TryGeneratePropertyComparisons([NotNullWhen(true)] ref SqlExpression? comparisons) + { + // We need to know here the difference between + // x.Collection == x.Collection (should return null, as we need All support) + // x.Collection[1] == x.Collection[1] (should run below) + // @TODO: Is there a better way to do this? It feels like this might not be the right place. + // In relational, this wouldn't have come from a StructuralTypeReferenceExpression, but a CollectionResultExpression + if ((leftReference?.Parameter ?? leftReference?.Subquery?.ShaperExpression as StructuralTypeShaperExpression) + ?.ValueBufferExpression is ObjectArrayAccessExpression + || + (rightReference?.Parameter ?? rightReference?.Subquery?.ShaperExpression as StructuralTypeShaperExpression) + ?.ValueBufferExpression is ObjectArrayAccessExpression) + { + return false; + } + + foreach (var property in complexType.GetProperties().Cast().Concat(complexType.GetComplexProperties())) + { + var leftAccess = CreatePropertyAccessExpression(left, property); + var rightAccess = CreatePropertyAccessExpression(right, property); + + var comparison = Visit(Expression.MakeBinary(nodeType, leftAccess, rightAccess)) as SqlExpression; + if (comparison == null || comparison == QueryCompilationContext.NotTranslatedExpression) + { + return false; + } + + if (comparison is SqlConstantExpression { Value: false } && nodeType == ExpressionType.Equal) + { + comparisons = comparison; + return true; + } + + comparisons = comparisons is null + ? comparison + : nodeType == ExpressionType.Equal + ? sqlExpressionFactory.AndAlso(comparisons, comparison) + : sqlExpressionFactory.OrElse(comparisons, comparison); + } + + var compare = reference == rightReference ? left : right; + if (comparisons != null && (leftShaper?.IsNullable == true || rightShaper?.IsNullable == true)) + { + var nullCompare = compare; + if (nullCompare is SqlParameterExpression sqlParameterExpression) + { + var lambda = Expression.Lambda( + Expression.Condition( + Expression.Equal( + Expression.Call(ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string))), + Expression.Constant(null)), + Expression.Constant(null), + Expression.Constant(new object())), + QueryCompilationContext.QueryContextParameter); + + var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}"; + var queryParam = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + nullCompare = new SqlParameterExpression(queryParam.Name, queryParam.Type, CosmosTypeMapping.Default); + } + + comparisons = (SqlExpression)Visit( + Expression.OrElse( + Expression.AndAlso( + Expression.MakeBinary(nodeType, reference, Expression.Constant(null)), + Expression.MakeBinary(nodeType, nullCompare, Expression.Constant(null))), + Expression.OrElse( + Expression.NotEqual(nullCompare, Expression.Constant(null)), + comparisons))); + } + + return comparisons is not null; + } + } + } + + private Expression CreatePropertyAccessExpression(Expression target, IPropertyBase property) + { + switch (target) + { + case SqlConstantExpression sqlConstantExpression: + return Expression.Constant( + property.GetGetter().GetClrValue(sqlConstantExpression.Value!), property.ClrType.MakeNullable()); + + case SqlParameterExpression sqlParameterExpression: + var lambda = Expression.Lambda( + Expression.Call( + ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + QueryCompilationContext.QueryContextParameter, + Expression.Constant(sqlParameterExpression.Name, typeof(string)), + Expression.Constant(property, typeof(IPropertyBase))), + QueryCompilationContext.QueryContextParameter); + + var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}"; + + return queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + + case MemberInitExpression memberInitExpression + when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == property.Name) is MemberAssignment + memberAssignment: + return memberAssignment.Expression; + + default: + return target.CreateEFPropertyExpression(property); + } + } + + private static T? ParameterPropertyValueExtractor(QueryContext context, string baseParameterName, IPropertyBase property) + { + var baseParameter = context.Parameters[baseParameterName]; + return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); + } + + private static T? ParameterValueExtractor(QueryContext context, string baseParameterName) + { + var baseParameter = context.Parameters[baseParameterName]; + return (T?)baseParameter; + } + + private static List? ParameterListValueExtractor( + QueryContext context, + string baseParameterName, + IProperty property) + { + if (context.Parameters[baseParameterName] is not IEnumerable baseListParameter) + { + return null; + } + + var getter = property.GetGetter(); + return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetClrValue(e) : (TProperty?)(object?)null).ToList(); + } + + private static bool IsNullSqlConstantExpression(Expression expression) + => expression is SqlConstantExpression { Value: null }; +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index d47d74d152f..41ceed44e2e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,9 +4,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -18,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 CosmosSqlTranslatingExpressionVisitor( +public partial class CosmosSqlTranslatingExpressionVisitor( QueryCompilationContext queryCompilationContext, ISqlExpressionFactory sqlExpressionFactory, ITypeMappingSource typeMappingSource, @@ -27,17 +24,6 @@ public class CosmosSqlTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) : ExpressionVisitor { - private const string RuntimeParameterPrefix = "entity_equality_"; - - private static readonly MethodInfo ParameterPropertyValueExtractorMethod = - typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!; - - private static readonly MethodInfo ParameterValueExtractorMethod = - typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; - - private static readonly MethodInfo ParameterListValueExtractorMethod = - typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor))!; - private static readonly MethodInfo ConcatMethodInfo = typeof(string).GetRuntimeMethod(nameof(string.Concat), [typeof(object), typeof(object)])!; @@ -184,7 +170,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) { // Visited expression could be null, We need to pass MemberInitExpression case { NodeType: ExpressionType.Equal or ExpressionType.NotEqual } - when TryRewriteEntityEquality( + when TryRewriteStructuralTypeEquality( binaryExpression.NodeType, visitedLeft == QueryCompilationContext.NotTranslatedExpression ? left : visitedLeft, visitedRight == QueryCompilationContext.NotTranslatedExpression ? right : visitedRight, @@ -563,7 +549,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var left = Visit(methodCallExpression.Object); var right = Visit(RemoveObjectConvert(methodCallExpression.Arguments[0])); - if (TryRewriteEntityEquality( + if (TryRewriteStructuralTypeEquality( ExpressionType.Equal, left == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Object : left, right == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Arguments[0] : right, @@ -597,7 +583,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var left = Visit(RemoveObjectConvert(methodCallExpression.Arguments[0])); var right = Visit(RemoveObjectConvert(methodCallExpression.Arguments[1])); - if (TryRewriteEntityEquality( + if (TryRewriteStructuralTypeEquality( ExpressionType.Equal, left == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Arguments[0] : left, right == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Arguments[1] : right, @@ -1007,301 +993,6 @@ private static Expression TryRemoveImplicitConvert(Expression expression) return expression; } - private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result) - { - result = null; - - if (item is not StructuralTypeReferenceExpression itemEntityReference || - itemEntityReference.StructuralType is not IEntityType entityType) // #36468 ? - { - return false; - } - - var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; - - switch (primaryKeyProperties) - { - case null: - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nameof(Queryable.Contains), entityType.DisplayName())); - - case { Count: > 1 }: - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported( - nameof(Queryable.Contains), entityType.DisplayName())); - } - - var property = primaryKeyProperties[0]; - Expression rewrittenSource; - switch (source) - { - case SqlConstantExpression sqlConstantExpression: - var values = (IEnumerable)sqlConstantExpression.Value!; - var propertyValueList = - (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(property.ClrType.MakeNullable()))!; - var propertyGetter = property.GetGetter(); - foreach (var value in values) - { - propertyValueList.Add(propertyGetter.GetClrValue(value)); - } - - rewrittenSource = Expression.Constant(propertyValueList); - break; - - case SqlParameterExpression sqlParameterExpression: - var lambda = Expression.Lambda( - Expression.Call( - ParameterListValueExtractorMethod.MakeGenericMethod(entityType.ClrType, property.ClrType.MakeNullable()), - QueryCompilationContext.QueryContextParameter, - Expression.Constant(sqlParameterExpression.Name, typeof(string)), - Expression.Constant(property, typeof(IProperty))), - QueryCompilationContext.QueryContextParameter - ); - - var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}"; - - rewrittenSource = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); - break; - - default: - return false; - } - - result = Visit( - Expression.Call( - EnumerableMethods.Contains.MakeGenericMethod(property.ClrType.MakeNullable()), - rewrittenSource, - CreatePropertyAccessExpression(item, property))); - - return true; - } - - private bool TryRewriteEntityEquality( - ExpressionType nodeType, - Expression left, - Expression right, - bool equalsMethod, - [NotNullWhen(true)] out Expression? result) - { - var structuralReference = left as StructuralTypeReferenceExpression ?? right as StructuralTypeReferenceExpression; - if (structuralReference == null) - { - result = null; - return false; - } - var structuralType = structuralReference.StructuralType; - var compareReference = structuralReference == left ? right : left; - - // Null equality - if (IsNullSqlConstantExpression(compareReference)) - { - if (structuralType is IEntityType entityType1 && entityType1.IsDocumentRoot()) - { - // Document root can never be be null - result = Visit(Expression.Constant(nodeType != ExpressionType.Equal)); - return true; - } - - var obj = structuralReference switch - { - { Parameter: { } shaper } => Visit(shaper.ValueBufferExpression), - { Subquery: not null } => throw new NotImplementedException("Null comparison on structural type coming out of subquery"), - _ => throw new UnreachableException(), - }; - - var access = new SqlObjectAccessExpression(obj); - result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object), null)!, typeMappingSource.FindMapping(typeof(bool)))!; - return true; - } - - // IEntityType type comparison - if (structuralType is IEntityType entityType) - { - if (entityType.FindPrimaryKey()?.Properties is not { } primaryKeyProperties) - { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType.DisplayName())); - } - - if (compareReference is StructuralTypeReferenceExpression compareStructuralTypeReference) - { - // Comparing of 2 different entity types is always false. - if (structuralType.GetRootType() != compareStructuralTypeReference.StructuralType.GetRootType()) - { - result = Visit(Expression.Constant(false)); - return true; - } - } - - // Compare primary keys of entity type - result = CreateStructuralComparison(primaryKeyProperties); - - return result is not null; - } - // Complex type equality - else if (structuralType is IComplexType complexType) - { - // We need to know here the difference between - // x.Collection == x.Collection (should return null) - // x.Collection[1] == x.Collection[1] (should run below) - // I think the problem is that this would need to be translated into an All expression, - // which would be handled by CosmosQueryableMethodTranslatingExpressionVisitor, so the check has to live there kinda. - if ((structuralReference.Parameter ?? - structuralReference.Parameter ?? - throw new UnreachableException()).ValueBufferExpression is ObjectArrayAccessExpression) // @TODO: Is there a better way to do this? It feels like this might not be the right place. - { - result = null; - return false; - } - - // Compare to another structural type reference x => x.ComplexProp1 == x.ComplexProp2 || - // Compare to constant complex type x => x.ComplexProp1 == new ComplexType() - // Compare to parameter complex type x => x.ComplexProp1 == param - if (compareReference is StructuralTypeReferenceExpression compareStructuralTypeReference && compareStructuralTypeReference.StructuralType.ClrType == structuralType.ClrType || - compareReference is SqlConstantExpression constant && constant.Type.MakeNullable() == structuralType.ClrType.MakeNullable() || - compareReference is SqlParameterExpression parameter && parameter.Type.MakeNullable() == structuralType.ClrType.MakeNullable()) - { - if (compareReference is SqlParameterExpression p) - { - compareReference = new SqlParameterExpression( - p.Name, - structuralType.ClrType, - new CosmosTypeMapping(typeof(object), null, null, null, null) - ); - } - - var allProperties = complexType.GetComplexProperties().Cast().Concat(complexType.GetProperties()); - result = CreateStructuralComparison(allProperties); - - return result is not null; - } - } - - Expression? CreateStructuralComparison(IEnumerable properties) - => CreateStructuralComparisonBy(properties, p => CreatePropertyAccessExpression(right, p)); - - Expression? CreateStructuralComparisonBy(IEnumerable properties, Func rightValueFactory) - { - var propertyCompare = properties.Select(p => - Expression.MakeBinary( - nodeType, - CreatePropertyAccessExpression(left, p), - rightValueFactory(p))) - .Aggregate((l, r) => nodeType == ExpressionType.Equal - ? Expression.AndAlso(l, r) - : Expression.OrElse(l, r)); - - // Maybe only if structuralReference.StructuralType is nullable? But we can't really determine that, unless collections can not contain null. Then retrieving alias in subquery wouldn't be needed for null comparison translation above - if (compareReference.Type.IsNullableType() && compareReference is not SqlConstantExpression { Value: not null } && - (structuralReference.StructuralType is not IEntityType entityType1 || !entityType1.IsDocumentRoot())) // Document can never be null so don't compare document to null - { - var compareNullCompareReference = compareReference; - if (compareReference is SqlParameterExpression sqlParameterExpression) - { - var lambda = Expression.Lambda( - Expression.Condition( - Expression.Equal( - Expression.Call(ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string))), - Expression.Constant(null) - ), - Expression.Constant(null), - Expression.Constant(new object()) - ), - QueryCompilationContext.QueryContextParameter - ); - - var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}"; - var queryParam = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); - compareNullCompareReference = new SqlParameterExpression(queryParam.Name, queryParam.Type, CosmosTypeMapping.Default); - } - - return Visit(Expression.OrElse( - Expression.AndAlso( - Expression.Equal(structuralReference, sqlExpressionFactory.Constant(null, typeof(object), null)!), - Expression.Equal(compareNullCompareReference, sqlExpressionFactory.Constant(null, typeof(object), null)!)) - , - Expression.AndAlso( - Expression.NotEqual(compareNullCompareReference, sqlExpressionFactory.Constant(null, typeof(object), null)!), - propertyCompare - ) - ) - ); - } - - return Visit(propertyCompare); - } - - result = null; - return false; - } - - private Expression CreatePropertyAccessExpression(Expression target, IPropertyBase property) - { - switch (target) - { - case SqlConstantExpression sqlConstantExpression: - return Expression.Constant( - property.GetGetter().GetClrValue(sqlConstantExpression.Value!), property.ClrType.MakeNullable()); - - case SqlParameterExpression sqlParameterExpression: - var lambda = Expression.Lambda( - Expression.Call( - ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), - QueryCompilationContext.QueryContextParameter, - Expression.Constant(sqlParameterExpression.Name, typeof(string)), - Expression.Constant(property, typeof(IPropertyBase))), - QueryCompilationContext.QueryContextParameter); - - var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}"; - - return queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); - - case MemberInitExpression memberInitExpression - when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == property.Name) is MemberAssignment - memberAssignment: - return memberAssignment.Expression; - - default: - return target.CreateEFPropertyExpression(property); - } - } - - private static T? ParameterPropertyValueExtractor(QueryContext context, string baseParameterName, IPropertyBase property) - { - var baseParameter = context.Parameters[baseParameterName]; - return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); - } - - private static T? ParameterValueExtractor(QueryContext context, string baseParameterName) - { - var baseParameter = context.Parameters[baseParameterName]; - return (T?)baseParameter; - } - - private static List? ParameterListValueExtractor( - QueryContext context, - string baseParameterName, - IProperty property) - { - if (context.Parameters[baseParameterName] is not IEnumerable baseListParameter) - { - return null; - } - - var getter = property.GetGetter(); - return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetClrValue(e) : (TProperty?)(object?)null).ToList(); - } - - private static bool IsNullSqlConstantExpression(Expression expression) - => expression is SqlConstantExpression { Value: null }; - private static bool TryEvaluateToConstant(Expression expression, [NotNullWhen(true)] out SqlConstantExpression? sqlConstantExpression) { if (CanEvaluate(expression)) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs index 168dee2c8c7..c8d84c81d23 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -13,16 +13,13 @@ public override async Task Complex_type_equals_parameter_with_nested_types_with_ AssertSql( """ -@entity_equality_container='{}' -@entity_equality_entity_equality_container_Containee1='{}' +@entity_equality_container_Id='1' @entity_equality_entity_equality_container_Containee1_Id='2' -@entity_equality_entity_equality_container_Containee2='{}' @entity_equality_entity_equality_container_Containee2_Id='3' -@entity_equality_container_Id='1' SELECT VALUE c FROM root c -WHERE (((c["ComplexContainer"] = null) AND (@entity_equality_container = null)) OR ((@entity_equality_container != null) AND (((((c["ComplexContainer"]["Containee1"] = null) AND (@entity_equality_entity_equality_container_Containee1 = null)) OR ((@entity_equality_entity_equality_container_Containee1 != null) AND (c["ComplexContainer"]["Containee1"]["Id"] = @entity_equality_entity_equality_container_Containee1_Id))) AND (((c["ComplexContainer"]["Containee2"] = null) AND (@entity_equality_entity_equality_container_Containee2 = null)) OR ((@entity_equality_entity_equality_container_Containee2 != null) AND (c["ComplexContainer"]["Containee2"]["Id"] = @entity_equality_entity_equality_container_Containee2_Id)))) AND (c["ComplexContainer"]["Id"] = @entity_equality_container_Id)))) +WHERE (((c["ComplexContainer"]["Id"] = @entity_equality_container_Id) AND (c["ComplexContainer"]["Containee1"]["Id"] = @entity_equality_entity_equality_container_Containee1_Id)) AND (c["ComplexContainer"]["Containee2"]["Id"] = @entity_equality_entity_equality_container_Containee2_Id)) OFFSET 0 LIMIT 2 """); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs index 77870942ee4..caef779990b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -22,7 +22,7 @@ public override async Task Two_nested_associates() """ SELECT VALUE c FROM root c -WHERE (((c["RequiredAssociate"]["RequiredNestedAssociate"] = null) AND (c["OptionalAssociate"]["RequiredNestedAssociate"] = null)) OR ((c["OptionalAssociate"]["RequiredNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["String"])))) +WHERE (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["String"])) """); } @@ -74,7 +74,6 @@ public override async Task Nested_associate_with_parameter() AssertSql( """ -@entity_equality_nested='{}' @entity_equality_nested_Id='1000' @entity_equality_nested_Int='8' @entity_equality_nested_Ints='[1,2,3]' @@ -83,7 +82,7 @@ public override async Task Nested_associate_with_parameter() SELECT VALUE c FROM root c -WHERE (((c["RequiredAssociate"]["RequiredNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = @entity_equality_nested_String)))) +WHERE (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = @entity_equality_nested_String)) """); } @@ -92,7 +91,6 @@ public async Task Nested_associate_with_parameter_null() { NestedAssociateType? nested = null; await AssertQuery( - ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested), ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested)); AssertSql( @@ -106,7 +104,29 @@ await AssertQuery( SELECT VALUE c FROM root c -WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) +WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) OR (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) +"""); + } + + [ConditionalFact] + public async Task Nested_associate_with_parameter_not_null() + { + NestedAssociateType? nested = null; + await AssertQuery( + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate != nested)); + + AssertSql( + """ +@entity_equality_nested=null +@entity_equality_nested_Id=null +@entity_equality_nested_Int=null +@entity_equality_nested_Ints=null +@entity_equality_nested_Name=null +@entity_equality_nested_String=null + +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested != null)) OR ((@entity_equality_nested != null) OR (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != @entity_equality_nested_Id) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != @entity_equality_nested_Int)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != @entity_equality_nested_Ints)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != @entity_equality_nested_Name)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != @entity_equality_nested_String)))) """); } @@ -155,7 +175,6 @@ public override async Task Contains_with_parameter() AssertSql( """ -@entity_equality_nested='{}' @entity_equality_nested_Id='1002' @entity_equality_nested_Int='8' @entity_equality_nested_Ints='[1,2,3]' @@ -167,7 +186,7 @@ FROM root c WHERE EXISTS ( SELECT 1 FROM n IN c["RequiredAssociate"]["NestedCollection"] - WHERE (((n = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((n["Id"] = @entity_equality_nested_Id) AND (n["Int"] = @entity_equality_nested_Int)) AND (n["Ints"] = @entity_equality_nested_Ints)) AND (n["Name"] = @entity_equality_nested_Name)) AND (n["String"] = @entity_equality_nested_String))))) + WHERE (((((n["Id"] = @entity_equality_nested_Id) AND (n["Int"] = @entity_equality_nested_Int)) AND (n["Ints"] = @entity_equality_nested_Ints)) AND (n["Name"] = @entity_equality_nested_Name)) AND (n["String"] = @entity_equality_nested_String))) """); } @@ -178,7 +197,6 @@ public override async Task Contains_with_operators_composed_on_the_collection() AssertSql( """ @get_Item_Int='106' -@entity_equality_get_Item='{}' @entity_equality_get_Item_Id='3003' @entity_equality_get_Item_Int='108' @entity_equality_get_Item_Ints='[8,9,109]' @@ -190,7 +208,7 @@ FROM root c WHERE EXISTS ( SELECT 1 FROM n IN c["RequiredAssociate"]["NestedCollection"] - WHERE ((n["Int"] > @get_Item_Int) AND (((n = null) AND (@entity_equality_get_Item = null)) OR ((@entity_equality_get_Item != null) AND (((((n["Id"] = @entity_equality_get_Item_Id) AND (n["Int"] = @entity_equality_get_Item_Int)) AND (n["Ints"] = @entity_equality_get_Item_Ints)) AND (n["Name"] = @entity_equality_get_Item_Name)) AND (n["String"] = @entity_equality_get_Item_String)))))) + WHERE ((n["Int"] > @get_Item_Int) AND (((((n["Id"] = @entity_equality_get_Item_Id) AND (n["Int"] = @entity_equality_get_Item_Int)) AND (n["Ints"] = @entity_equality_get_Item_Ints)) AND (n["Name"] = @entity_equality_get_Item_Name)) AND (n["String"] = @entity_equality_get_Item_String)))) """); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index 2bd23b53a8b..2af99482851 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -48,14 +48,32 @@ WHERE false """); } - public override Task Associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Associate_with_inline_null()); + public override async Task Associate_with_inline_null() + { + await base.Associate_with_inline_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"] = null) +"""); + } public override Task Associate_with_parameter_null() => Assert.ThrowsAsync(() => base.Associate_with_parameter_null()); - public override Task Nested_associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Nested_associate_with_inline_null()); + public override async Task Nested_associate_with_inline_null() + { + await base.Nested_associate_with_inline_null(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = null) +"""); + } public override async Task Nested_associate_with_inline() { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index f54f17271c8..156efabfd1a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -90,7 +90,7 @@ public override Task Complex_type_equals_complex_type(bool async) """ SELECT VALUE c FROM root c -WHERE (((c["ShippingAddress"] = null) AND (c["BillingAddress"] = null)) OR ((c["BillingAddress"] != null) AND (((((((c["ShippingAddress"]["Country"] = null) AND (c["BillingAddress"]["Country"] = null)) OR ((c["BillingAddress"]["Country"] != null) AND ((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"])))) AND (c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"])) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["Tags"] = c["BillingAddress"]["Tags"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])))) +WHERE (((((c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"]) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["Tags"] = c["BillingAddress"]["Tags"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) AND ((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"]))) """); }); @@ -103,7 +103,7 @@ public override Task Complex_type_equals_constant(bool async) """ SELECT VALUE c FROM root c -WHERE ((((((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States")) AND (c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road")) AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["Tags"] = ["foo","bar"])) AND (c["ShippingAddress"]["ZipCode"] = 38654)) +WHERE (((((c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road") AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["Tags"] = ["foo","bar"])) AND (c["ShippingAddress"]["ZipCode"] = 38654)) AND ((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States"))) """); }); @@ -114,18 +114,16 @@ public override Task Complex_type_equals_parameter(bool async) AssertSql( """ -@entity_equality_address='{}' -@entity_equality_entity_equality_address_Country='{}' -@entity_equality_entity_equality_address_Country_Code='US' -@entity_equality_entity_equality_address_Country_FullName='United States' @entity_equality_address_AddressLine1='804 S. Lakeshore Road' @entity_equality_address_AddressLine2=null @entity_equality_address_Tags='["foo","bar"]' @entity_equality_address_ZipCode='38654' +@entity_equality_entity_equality_address_Country_Code='US' +@entity_equality_entity_equality_address_Country_FullName='United States' SELECT VALUE c FROM root c -WHERE (((c["ShippingAddress"] = null) AND (@entity_equality_address = null)) OR ((@entity_equality_address != null) AND (((((((c["ShippingAddress"]["Country"] = null) AND (@entity_equality_entity_equality_address_Country = null)) OR ((@entity_equality_entity_equality_address_Country != null) AND ((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName)))) AND (c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1)) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["Tags"] = @entity_equality_address_Tags)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)))) +WHERE (((((c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["Tags"] = @entity_equality_address_Tags)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) AND ((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName))) """); }); @@ -278,7 +276,7 @@ public override Task Struct_complex_type_equals_struct_complex_type(bool async) """ SELECT VALUE c FROM root c -WHERE (((((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"])) AND (c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"])) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) +WHERE ((((c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"]) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) AND ((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"]))) """); }); @@ -291,7 +289,7 @@ public override Task Struct_complex_type_equals_constant(bool async) """ SELECT VALUE c FROM root c -WHERE (((((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States")) AND (c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road")) AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["ZipCode"] = 38654)) +WHERE ((((c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road") AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["ZipCode"] = 38654)) AND ((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States"))) """); }); @@ -302,15 +300,15 @@ public override Task Struct_complex_type_equals_parameter(bool async) AssertSql( """ -@entity_equality_entity_equality_address_Country_Code='US' -@entity_equality_entity_equality_address_Country_FullName='United States' @entity_equality_address_AddressLine1='804 S. Lakeshore Road' @entity_equality_address_AddressLine2=null @entity_equality_address_ZipCode='38654' +@entity_equality_entity_equality_address_Country_Code='US' +@entity_equality_entity_equality_address_Country_FullName='United States' SELECT VALUE c FROM root c -WHERE (((((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName)) AND (c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1)) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) +WHERE ((((c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) AND ((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName))) """); }); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 9a713bfda47..fa673f688e1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -167,7 +167,7 @@ public override Task Entity_equality_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -181,7 +181,6 @@ public override Task Entity_equality_not_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] != null) """); }); @@ -2895,7 +2894,7 @@ public override Task Comparing_entity_to_null_using_Equals(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (STARTSWITH(c["id"], "A") AND NOT((c["id"] = null))) +WHERE (STARTSWITH(c["id"], "A") AND NOT(false)) ORDER BY c["id"] """); }); @@ -2941,7 +2940,7 @@ public override Task Comparing_collection_navigation_to_null(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -4016,7 +4015,7 @@ public override Task Entity_equality_through_include(bool async) """ SELECT VALUE c["id"] FROM root c -WHERE (c["id"] = null) +WHERE false """); }); @@ -4125,7 +4124,7 @@ public override Task Entity_equality_not_null_composite_key(bool async) """ SELECT VALUE c FROM root c -WHERE ((c["$type"] = "OrderDetail") AND ((c["OrderID"] != null) AND (c["ProductID"] != null))) +WHERE (c["$type"] = "OrderDetail") """); }); @@ -4195,7 +4194,12 @@ public override Task Null_parameter_name_works(bool async) { await base.Null_parameter_name_works(a); - AssertSql("ReadItem(None, null)"); + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE false +"""); }); public override Task Where_Property_shadow_closure(bool async) @@ -4288,7 +4292,7 @@ public override Task Entity_equality_null_composite_key(bool async) """ SELECT VALUE c FROM root c -WHERE ((c["$type"] = "OrderDetail") AND ((c["OrderID"] = null) OR (c["ProductID"] = null))) +WHERE ((c["$type"] = "OrderDetail") AND false) """); }); From 753f31ed5340d7e627136de489098a13f71ed73a Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:56:02 +0100 Subject: [PATCH 12/28] Cleanup --- .../CosmosQueryableMethodTranslatingExpressionVisitor.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 3da0da887f9..c682934569a 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Numerics; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; @@ -533,8 +532,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return null; } - // We're extracting a property from a materialized structural type. - // If DISTINCT was applied, this is incorrect because SQL DISTINCT operates on the full + // We can not apply distinct because SQL DISTINCT operates on the full // structural type, but the shaper extracts only a subset of that data. // Cosmos: Projecting out nested documents retrieves the entire document #34067 if (select.UsesClientProjection) From 88c84b6ab7caf8e1b984dca86f8c5be72ad9ee26 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:44:29 +0100 Subject: [PATCH 13/28] Rename TypeBase to StructuralType --- .../Query/Internal/Expressions/ObjectAccessExpression.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs index 41d3b1b522b..db210ebc9bd 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs @@ -31,7 +31,7 @@ public ObjectAccessExpression(Expression @object, INavigation navigation) navigation.DeclaringEntityType.DisplayName(), navigation.Name)); PropertyBase = navigation; - TypeBase = navigation.TargetEntityType; + StructuralType = navigation.TargetEntityType; Object = @object; } @@ -46,7 +46,7 @@ public ObjectAccessExpression(Expression @object, IComplexProperty complexProper PropertyBase = complexProperty; PropertyName = complexProperty.Name; Object = @object; - TypeBase = complexProperty.ComplexType; + StructuralType = complexProperty.ComplexType; } /// @@ -55,7 +55,7 @@ public ObjectAccessExpression(Expression @object, IComplexProperty complexProper /// 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 ITypeBase TypeBase { get; } + public virtual ITypeBase StructuralType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to From 19c1de15728e31db2be35183ad576f819ac2fd6a Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:17:35 +0100 Subject: [PATCH 14/28] Cleanup and add todo --- ...ingExpressionVisitor.StructuralEquality.cs | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs index 11ef22879c7..919516cf2a3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; @@ -117,17 +116,9 @@ private bool TryRewriteStructuralTypeEquality( if (left is SqlConstantExpression { Value: null } || right is SqlConstantExpression { Value: null }) { - var nullComparedStructuralType = reference.StructuralType; - if (nullComparedStructuralType is IEntityType entityType1 && entityType1.IsDocumentRoot()) - { - // Document root can never be be null - result = sqlExpressionFactory.Constant(nodeType != ExpressionType.Equal); - return true; - } - if (!shaper.IsNullable) { - result = (SqlExpression)Visit(Expression.Constant(nodeType != ExpressionType.Equal)); + result = sqlExpressionFactory.Constant(nodeType != ExpressionType.Equal); return true; } @@ -181,20 +172,6 @@ bool TryRewriteEntityEquality(IEntityType entityType, [NotNullWhen(true)] out Sq structuralType.DisplayName())); } - if (primaryKeyProperties.Count > 1 - && (leftReference?.Subquery != null - || rightReference?.Subquery != null)) - { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - structuralType.DisplayName())); - } - result = Visit( primaryKeyProperties.Select(p => Expression.MakeBinary(nodeType, CreatePropertyAccessExpression(left, p), @@ -218,6 +195,8 @@ bool TryRewriteComplexTypeEquality(IComplexType complexType, [NotNullWhen(true)] return false; } + // @TODO: Alternative would be a bitwise comparison... But structure and order of properties would matter then. + // Generate an expression that compares each property on the left to the same property on the right; this needs to recursively // include all properties in nested complex types. var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!; @@ -238,7 +217,8 @@ bool TryGeneratePropertyComparisons([NotNullWhen(true)] ref SqlExpression? compa // x.Collection == x.Collection (should return null, as we need All support) // x.Collection[1] == x.Collection[1] (should run below) // @TODO: Is there a better way to do this? It feels like this might not be the right place. - // In relational, this wouldn't have come from a StructuralTypeReferenceExpression, but a CollectionResultExpression + // In relational, this wouldn't have come from a StructuralTypeReferenceExpression, but a CollectionResultExpression? At-least if it's json.. + // See BindMember on StructuralTypeProjectionExpression if ((leftReference?.Parameter ?? leftReference?.Subquery?.ShaperExpression as StructuralTypeShaperExpression) ?.ValueBufferExpression is ObjectArrayAccessExpression || @@ -276,8 +256,14 @@ bool TryGeneratePropertyComparisons([NotNullWhen(true)] ref SqlExpression? compa if (comparisons != null && (leftShaper?.IsNullable == true || rightShaper?.IsNullable == true)) { var nullCompare = compare; + if (nullCompare is SqlParameterExpression sqlParameterExpression) { + // @TODO: Can we optimize this instead in CosmosQuerySqlGenerator? + // My idea would be to create a SqlParameterComparisonExpression that will hold the comparisons below + // But also hold a sql == null or != null expression + // The CosmosQuerySqlGenerator will check the value of the parameter and chooses which tree to visit at sql generation time + // (or just call a method on SqlParameterComparisonExpression to get which tree to visit, so logic can live there...) var lambda = Expression.Lambda( Expression.Condition( Expression.Equal( From a5a8bd4cd95274e92c27e47273d7205c582c17f0 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:45:42 +0100 Subject: [PATCH 15/28] Implement test --- .../CosmosComplexTypesTrackingTest.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs index c62d5eeccc1..fbfe3a30ce7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs @@ -112,9 +112,13 @@ protected override Task TrackAndSaveTest(EntityState state, bool async, } public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async) - // Optional complex properties are not supported on Cosmos - // See https://github.com/dotnet/efcore/issues/31253 - => Task.CompletedTask; + { + if (!async) + { + throw SkipException.ForSkip("Cosmos does not support synchronous operations."); + } + return base.Can_save_default_values_in_optional_complex_property_with_multiple_properties(async); + } protected override async Task ExecuteWithStrategyInTransactionAsync(Func testOperation, Func? nestedTestOperation1 = null, Func? nestedTestOperation2 = null, Func? nestedTestOperation3 = null) { From 3b21620c1c968be1e643f1b7b13c37147fa861cd Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:46:59 +0100 Subject: [PATCH 16/28] Remove old unused class --- ...ingExpressionVisitor.StructuralEquality.cs | 1 - .../Expressions/SqlObjectAccessExpression.cs | 90 ------------------- 2 files changed, 91 deletions(-) delete mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs index 558b4a60e41..d9f79347764 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs deleted file mode 100644 index 4fe2f8c54ac..00000000000 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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.Cosmos.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; - -/// -/// Represents an structural type object access on a CosmosJSON object -/// -/// -/// 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. -/// -[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] -public class SqlObjectAccessExpression(Expression @object) - : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression -{ - /// - /// 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 Expression Object { get; } = @object; - - /// - /// 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 string? PropertyName => null; - - /// - /// 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) - => Update(visitor.Visit(Object)); - - /// - /// 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 SqlObjectAccessExpression Update(Expression @object) - => ReferenceEquals(@object, Object) - ? this - : new SqlObjectAccessExpression(@object); - - /// - /// 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.Visit(Object); - - /// - /// 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 override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is SqlObjectAccessExpression expression - && Equals(expression)); - - private bool Equals(SqlObjectAccessExpression expression) - => base.Equals(expression) - && Object.Equals(expression.Object); - - /// - /// 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 override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Object); -} From 71275863a074bb7801897e70671cab32b345447f Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:04:12 +0100 Subject: [PATCH 17/28] Remove skip on fixed tests --- .../Query/ComplexTypeQueryCosmosTest.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index 156efabfd1a..cc02b711e53 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -133,35 +133,27 @@ public override Task Subquery_over_complex_type(bool async) public override Task Contains_over_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Contains_over_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_entity_type_containing_complex_property(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_entity_type_containing_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_entity_type_containing_complex_property(bool async) => AssertTranslationFailedWithDetails(() => base.Union_entity_type_containing_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Union_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_property_in_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_property_in_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_property_in_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Union_property_in_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_two_different_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_two_different_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_two_different_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Union_two_different_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); @@ -318,35 +310,27 @@ public override Task Subquery_over_struct_complex_type(bool async) public override Task Contains_over_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Contains_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_entity_type_containing_struct_complex_property(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_entity_type_containing_struct_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_entity_type_containing_struct_complex_property(bool async) => AssertTranslationFailedWithDetails(() => base.Union_entity_type_containing_struct_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Union_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_property_in_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_property_in_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_property_in_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Union_property_in_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Concat_two_different_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Concat_two_different_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); - [ConditionalTheory(Skip = "Cosmos: Subquery not detected #37583")] public override Task Union_two_different_struct_complex_type(bool async) => AssertTranslationFailedWithDetails(() => base.Union_two_different_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported); From 82e9b15d93bf661b99be551956e721b121c00740 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:14:52 +0100 Subject: [PATCH 18/28] Rename test class --- .../Query/AdHocCosmosTestHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs index 7af7cd789f2..b640cd21373 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs @@ -20,13 +20,13 @@ public static void UseTestAutoIncrementIntIds(ModelBuilder modelBuilder) if (primaryKey != null && primaryKey.Properties.Count == 1 && primaryKey.Properties[0].ClrType == typeof(int)) { - var valueGenerator = new TestAutoIncerementIntValueGenerator(); + var valueGenerator = new TestAutoIncrementIntValueGenerator(); primaryKey.Properties[0].SetValueGeneratorFactory((_, _) => valueGenerator); } } } - private class TestAutoIncerementIntValueGenerator : ValueGenerator + private class TestAutoIncrementIntValueGenerator : ValueGenerator { private int i; From dd4b3f0c805b1cb916800cb92871b823a9302229 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:24:03 +0100 Subject: [PATCH 19/28] Fix null check structural comparison --- ...ingExpressionVisitor.StructuralEquality.cs | 42 ++++++++++--- ...xPropertiesStructuralEqualityCosmosTest.cs | 60 ++++++++++++++++++- 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs index d9f79347764..a5bb7c45fd4 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -278,14 +279,41 @@ bool TryGeneratePropertyComparisons([NotNullWhen(true)] ref SqlExpression? compa nullCompare = new SqlParameterExpression(queryParam.Name, queryParam.Type, CosmosTypeMapping.Default); } - comparisons = (SqlExpression)Visit( - Expression.OrElse( - Expression.AndAlso( - Expression.MakeBinary(nodeType, reference, Expression.Constant(null)), - Expression.MakeBinary(nodeType, nullCompare, Expression.Constant(null))), + if (nodeType == ExpressionType.Equal) + { + // left == null AND right == null OR (left != null AND right != null AND (left.Prop1 == right.Prop1)) + comparisons = (SqlExpression)Visit( + Expression.OrElse( + Expression.AndAlso( + Expression.Equal(reference, Expression.Constant(null)), + Expression.Equal(nullCompare, Expression.Constant(null))), + Expression.AndAlso( + Expression.AndAlso( + Expression.NotEqual(reference, Expression.Constant(null)), + Expression.NotEqual(nullCompare, Expression.Constant(null))), + comparisons))); + } + else + { + // (left == null AND right != null) OR + // (left != null AND right == null) OR + // (left != null AND right != null AND (left.Prop1 != right.Prop1)) + comparisons = (SqlExpression)Visit( Expression.OrElse( - Expression.NotEqual(nullCompare, Expression.Constant(null)), - comparisons))); + Expression.OrElse( + Expression.AndAlso( + Expression.Equal(reference, Expression.Constant(null)), + Expression.NotEqual(nullCompare, Expression.Constant(null))), + Expression.AndAlso( + Expression.NotEqual(reference, Expression.Constant(null)), + Expression.Equal(nullCompare, Expression.Constant(null)))), + Expression.AndAlso( + Expression.AndAlso( + Expression.NotEqual(reference, Expression.Constant(null)), + Expression.NotEqual(nullCompare, Expression.Constant(null))), + comparisons))); + + } } return comparisons is not null; diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs index caef779990b..aef83efccdb 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -26,6 +26,62 @@ FROM root c """); } + [ConditionalFact] + public virtual async Task Required_two_nested_associates() + { + await AssertQuery( + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == e.RequiredAssociate.RequiredNestedAssociate)); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["String"]))) +"""); + } + + [ConditionalFact] + public virtual async Task Two_nested_associates_not_equal() + { + await AssertQuery( + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate != e.RequiredAssociate.RequiredNestedAssociate)); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) OR ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"]) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["String"])))) +"""); + } + + [ConditionalFact] + public virtual async Task Two_nested_optional_associates() + { + await AssertQuery( + ss => ss.Set().Where(e => e.OptionalAssociate != null && e.RequiredAssociate.OptionalNestedAssociate == e.OptionalAssociate.OptionalNestedAssociate)); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((c["OptionalAssociate"] != null) AND (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] = null)) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["String"]))))) +"""); + } + + [ConditionalFact] + public virtual async Task Two_nested_optional_associates_not_equal() + { + await AssertQuery( + ss => ss.Set().Where(e => e.OptionalAssociate != null && e.RequiredAssociate.OptionalNestedAssociate != e.OptionalAssociate.OptionalNestedAssociate)); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE ((c["OptionalAssociate"] != null) AND ((((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] != null)) OR ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] = null))) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Id"]) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Int"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Ints"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Name"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["String"]))))) +"""); + } + public override Task Not_equals() => AssertTranslationFailed(base.Not_equals); // Complex collection equality... Need ALL support @@ -104,7 +160,7 @@ await AssertQuery( SELECT VALUE c FROM root c -WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) OR (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) +WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) """); } @@ -126,7 +182,7 @@ await AssertQuery( SELECT VALUE c FROM root c -WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested != null)) OR ((@entity_equality_nested != null) OR (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != @entity_equality_nested_Id) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != @entity_equality_nested_Int)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != @entity_equality_nested_Ints)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != @entity_equality_nested_Name)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != @entity_equality_nested_String)))) +WHERE ((((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested != null)) OR ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested = null))) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != @entity_equality_nested_Id) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != @entity_equality_nested_Int)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != @entity_equality_nested_Ints)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != @entity_equality_nested_Name)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != @entity_equality_nested_String)))) """); } From 812694171279bec821f52e439729c86bafa3ce5c Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:30:59 +0100 Subject: [PATCH 20/28] Rename field --- .../Query/AdHocCosmosTestHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs index b640cd21373..ebe995e1558 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs @@ -28,11 +28,11 @@ public static void UseTestAutoIncrementIntIds(ModelBuilder modelBuilder) private class TestAutoIncrementIntValueGenerator : ValueGenerator { - private int i; + private int _autoIncrementingId; public override bool GeneratesTemporaryValues => false; - public override int Next(EntityEntry entry) => Interlocked.Increment(ref i); + public override int Next(EntityEntry entry) => Interlocked.Increment(ref _autoIncrementingId); } public static async Task CreateCustomEntityHelperAsync( From 2a96a9be55098e82ef7e66131e80ec7c0bd56151 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:50:16 +0100 Subject: [PATCH 21/28] WIP: Use direct comparison --- .../Internal/CosmosSerializationUtilities.cs | 93 +++++++ ...ingExpressionVisitor.StructuralEquality.cs | 242 +++++++----------- .../CosmosSqlTranslatingExpressionVisitor.cs | 63 ++++- .../StructuralTypeProjectionExpression.cs | 4 +- .../Internal/InternalUpdateEntryExtensions.cs | 13 +- src/EFCore/Update/PropertyExtensions.cs | 35 +++ .../ComplexPropertiesCollectionCosmosTest.cs | 2 +- ...xPropertiesStructuralEqualityCosmosTest.cs | 159 ++++++------ 8 files changed, 343 insertions(+), 268 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs create mode 100644 src/EFCore/Update/PropertyExtensions.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs new file mode 100644 index 00000000000..2f07389b8dd --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Newtonsoft.Json.Linq; + +namespace Microsoft.EntityFrameworkCore.Cosmos.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. +/// +public static class CosmosSerializationUtilities +{ + /// + /// 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 static readonly MethodInfo SerializeObjectToComplexPropertyMethod + = typeof(CosmosSerializationUtilities).GetMethod(nameof(SerializeObjectToComplexProperty)) ?? throw new UnreachableException(); + + /// + /// 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 static JToken SerializeObjectToComplexProperty(IComplexType type, object? value, bool collection) // #34567 + { + if (value is null) + { + return JValue.CreateNull(); + } + + if (collection) + { + var array = new JArray(); + foreach (var element in (IEnumerable)value) + { + array.Add(SerializeObjectToComplexProperty(type, element, false)); + } + return array; + } + + var obj = new JObject(); + foreach (var property in type.GetProperties()) + { + var jsonPropertyName = property.GetJsonPropertyName(); + + var propertyValue = property.GetGetter().GetClrValue(value); + var providerValue = property.ConvertToProviderValue(propertyValue); + if (providerValue is null) + { + if (!property.IsNullable) + { + throw new InvalidOperationException(CoreStrings.PropertyConceptualNull(property.Name, type.DisplayName())); + } + + obj[jsonPropertyName] = null; + } + else + { + obj[jsonPropertyName] = JToken.FromObject(providerValue, CosmosClientWrapper.Serializer); + } + } + + foreach (var complexProperty in type.GetComplexProperties()) + { + var jsonPropertyName = complexProperty.Name; + var propertyValue = complexProperty.GetGetter().GetClrValue(value); + if (propertyValue is null) + { + if (!complexProperty.IsNullable) + { + throw new InvalidOperationException(CoreStrings.PropertyConceptualNull(complexProperty.Name, type.DisplayName())); + } + + obj[jsonPropertyName] = null; + } + else + { + obj[jsonPropertyName] = SerializeObjectToComplexProperty(complexProperty.ComplexType, propertyValue, complexProperty.IsCollection); + } + } + + return obj; + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs index a5bb7c45fd4..e72f2d94561 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -4,7 +4,7 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; -using static System.Runtime.InteropServices.JavaScript.JSType; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -98,27 +98,39 @@ private bool TryRewriteStructuralTypeEquality( bool equalsMethod, [NotNullWhen(true)] out SqlExpression? result) { - var leftReference = left as StructuralTypeReferenceExpression; - var rightReference = right as StructuralTypeReferenceExpression; - var reference = leftReference ?? rightReference; - if (reference == null) + switch (left, right) { - result = null; - return false; - } + case (StructuralTypeReferenceExpression, SqlConstantExpression { Value: null }): + case (SqlConstantExpression { Value: null }, StructuralTypeReferenceExpression): + return RewriteNullEquality(out result); + + case (StructuralTypeReferenceExpression { StructuralType: IEntityType }, _): + case (_, StructuralTypeReferenceExpression { StructuralType: IEntityType }): + return TryRewriteEntityEquality(out result); + + case (StructuralTypeReferenceExpression { StructuralType: IComplexType }, _): + case (_, StructuralTypeReferenceExpression { StructuralType: IComplexType }): + return TryRewriteComplexTypeEquality(collection: false, out result); + + case (CollectionResultExpression, _): + case (_, CollectionResultExpression): + return TryRewriteComplexTypeEquality(collection: true, out result); - var leftShaper = leftReference?.Parameter - ?? (StructuralTypeShaperExpression?)(leftReference?.Subquery)?.ShaperExpression; - var rightShaper = rightReference?.Parameter - ?? (StructuralTypeShaperExpression?)(rightReference?.Subquery)?.ShaperExpression; - var shaper = leftShaper ?? rightShaper ?? throw new UnreachableException(); + default: + result = null; + return false; + } - if (left is SqlConstantExpression { Value: null } - || right is SqlConstantExpression { Value: null }) + bool RewriteNullEquality(out SqlExpression? result) { + var reference = left as StructuralTypeReferenceExpression ?? (StructuralTypeReferenceExpression)right; + var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!; + + var shaper = reference.Parameter ?? + (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression; if (!shaper.IsNullable) { - result = sqlExpressionFactory.Constant(nodeType != ExpressionType.Equal, typeMappingSource.FindMapping(typeof(bool))); + result = sqlExpressionFactory.Constant(nodeType != ExpressionType.Equal, boolTypeMapping); return true; } @@ -128,33 +140,24 @@ private bool TryRewriteStructuralTypeEquality( access, sqlExpressionFactory.Constant(null, typeof(object), CosmosTypeMapping.Default)!, typeof(bool), - typeMappingSource.FindMapping(typeof(bool)))!; + boolTypeMapping)!; return true; } - var leftStructuralType = leftReference?.StructuralType; - var rightStructuralType = rightReference?.StructuralType; - var structuralType = reference.StructuralType; - - Check.DebugAssert(structuralType != null, "We checked that at least one side is an entity type"); - - switch (structuralType) + bool TryRewriteEntityEquality(out SqlExpression? result) { - case IEntityType entityType: - return TryRewriteEntityEquality(entityType, out result); - case IComplexType complexType: - return TryRewriteComplexTypeEquality( - complexType, out result); - } + var leftReference = left as StructuralTypeReferenceExpression; + var rightReference = right as StructuralTypeReferenceExpression; - result = null; - return false; + var leftEntityType = leftReference?.StructuralType as IEntityType; + var rightEntityType = rightReference?.StructuralType as IEntityType; + var entityType = leftEntityType ?? rightEntityType; - bool TryRewriteEntityEquality(IEntityType entityType, [NotNullWhen(true)] out SqlExpression? result) - { - if (leftStructuralType != null - && rightStructuralType != null - && leftStructuralType.GetRootType() != rightStructuralType.GetRootType()) + Check.DebugAssert(entityType != null, "We checked that at least one side is an entity type before calling this function"); + + if (leftEntityType != null + && rightEntityType != null + && leftEntityType.GetRootType() != rightEntityType.GetRootType()) { result = sqlExpressionFactory.Constant(false); return true; @@ -170,7 +173,7 @@ bool TryRewriteEntityEquality(IEntityType entityType, [NotNullWhen(true)] out Sq : equalsMethod ? "!" + nameof(object.Equals) : "!=", - structuralType.DisplayName())); + entityType.DisplayName())); } result = Visit( @@ -182,141 +185,70 @@ bool TryRewriteEntityEquality(IEntityType entityType, [NotNullWhen(true)] out Sq : Expression.OrElse(l, r))) as SqlExpression; return result is not null; + } - bool TryRewriteComplexTypeEquality(IComplexType complexType, [NotNullWhen(true)] out SqlExpression? result) + bool TryRewriteComplexTypeEquality(bool collection, out SqlExpression? result) { - if (leftStructuralType is not null - && rightStructuralType is not null - && leftStructuralType.ClrType != rightStructuralType.ClrType) + var (leftAccess, leftComplexType) = ParseComplexAccess(left); + var (rightAccess, rightComplexType) = ParseComplexAccess(right); + + if (leftAccess is null || leftAccess == QueryCompilationContext.NotTranslatedExpression || + rightAccess is null || rightAccess == QueryCompilationContext.NotTranslatedExpression) { - // Currently only support comparing complex types of the same CLR type. - // We could allow any case where the complex types have the same properties (some may be shadow). result = null; return false; } - // @TODO: Alternative would be a bitwise comparison... But structure and order of properties would matter then. - - // Generate an expression that compares each property on the left to the same property on the right; this needs to recursively - // include all properties in nested complex types. - var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!; - SqlExpression? comparisons = null; - - if (!TryGeneratePropertyComparisons(ref comparisons)) + if (leftComplexType is not null + && rightComplexType is not null + && leftComplexType.ClrType != rightComplexType.ClrType) { + // Currently only support comparing complex types of the same CLR type. + // We could allow any case where the complex types have the same properties (some may be shadow). result = null; return false; } - result = comparisons; + var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!; + result = new SqlBinaryExpression( + nodeType, + leftAccess, + rightAccess, + typeof(bool), + boolTypeMapping)!; return true; - bool TryGeneratePropertyComparisons([NotNullWhen(true)] ref SqlExpression? comparisons) - { - // We need to know here the difference between - // x.Collection == x.Collection (should return null, as we need All support) - // x.Collection[1] == x.Collection[1] (should run below) - // @TODO: Is there a better way to do this? It feels like this might not be the right place. - // In relational, this wouldn't have come from a StructuralTypeReferenceExpression, but a CollectionResultExpression? At-least if it's json.. - // See BindMember on StructuralTypeProjectionExpression - if ((leftReference?.Parameter ?? leftReference?.Subquery?.ShaperExpression as StructuralTypeShaperExpression) - ?.ValueBufferExpression is ObjectArrayAccessExpression - || - (rightReference?.Parameter ?? rightReference?.Subquery?.ShaperExpression as StructuralTypeShaperExpression) - ?.ValueBufferExpression is ObjectArrayAccessExpression) + (Expression?, IComplexType?) ParseComplexAccess(Expression expression) + => expression switch { - return false; - } - - foreach (var property in complexType.GetProperties().Cast().Concat(complexType.GetComplexProperties())) - { - var leftAccess = CreatePropertyAccessExpression(left, property); - var rightAccess = CreatePropertyAccessExpression(right, property); - - var comparison = Visit(Expression.MakeBinary(nodeType, leftAccess, rightAccess)) as SqlExpression; - if (comparison == null || comparison == QueryCompilationContext.NotTranslatedExpression) - { - return false; - } - - if (comparison is SqlConstantExpression { Value: false } && nodeType == ExpressionType.Equal) - { - comparisons = comparison; - return true; - } - - comparisons = comparisons is null - ? comparison - : nodeType == ExpressionType.Equal - ? sqlExpressionFactory.AndAlso(comparisons, comparison) - : sqlExpressionFactory.OrElse(comparisons, comparison); - } - - var compare = reference == rightReference ? left : right; - if (comparisons != null && (leftShaper?.IsNullable == true || rightShaper?.IsNullable == true)) - { - var nullCompare = compare; - - if (nullCompare is SqlParameterExpression sqlParameterExpression) - { - // @TODO: Can we optimize this instead in CosmosQuerySqlGenerator? - // My idea would be to create a SqlParameterComparisonExpression that will hold the comparisons below - // But also hold a sql == null or != null expression - // The CosmosQuerySqlGenerator will check the value of the parameter and chooses which tree to visit at sql generation time - // (or just call a method on SqlParameterComparisonExpression to get which tree to visit, so logic can live there...) - var lambda = Expression.Lambda( - Expression.Condition( - Expression.Equal( + StructuralTypeReferenceExpression { StructuralType: IComplexType type } reference + => (Visit((reference.Parameter ?? (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression).ValueBufferExpression), type), + CollectionResultExpression { ComplexProperty: IComplexProperty { ComplexType: var type } } collectionResult + => (Visit((collectionResult.Parameter ?? (StructuralTypeShaperExpression)collectionResult.Subquery!.ShaperExpression).ValueBufferExpression), type), + + SqlParameterExpression sqlParameterExpression + => (CreateJsonQueryParameter(sqlParameterExpression), null), + SqlConstantExpression constant + => (sqlExpressionFactory.Constant( + CosmosSerializationUtilities.SerializeObjectToComplexProperty(type, constant.Value, collectionResult != null), + CosmosTypeMapping.Default), null), + + _ => (null, null) + }; + + Expression CreateJsonQueryParameter(SqlParameterExpression sqlParameterExpression) + { + var lambda = Expression.Lambda( + Expression.Call( + CosmosSerializationUtilities.SerializeObjectToComplexPropertyMethod, + Expression.Constant(type, typeof(IComplexType)), Expression.Call(ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string))), - Expression.Constant(null)), - Expression.Constant(null), - Expression.Constant(new object())), - QueryCompilationContext.QueryContextParameter); - - var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}"; - var queryParam = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); - nullCompare = new SqlParameterExpression(queryParam.Name, queryParam.Type, CosmosTypeMapping.Default); - } - - if (nodeType == ExpressionType.Equal) - { - // left == null AND right == null OR (left != null AND right != null AND (left.Prop1 == right.Prop1)) - comparisons = (SqlExpression)Visit( - Expression.OrElse( - Expression.AndAlso( - Expression.Equal(reference, Expression.Constant(null)), - Expression.Equal(nullCompare, Expression.Constant(null))), - Expression.AndAlso( - Expression.AndAlso( - Expression.NotEqual(reference, Expression.Constant(null)), - Expression.NotEqual(nullCompare, Expression.Constant(null))), - comparisons))); - } - else - { - // (left == null AND right != null) OR - // (left != null AND right == null) OR - // (left != null AND right != null AND (left.Prop1 != right.Prop1)) - comparisons = (SqlExpression)Visit( - Expression.OrElse( - Expression.OrElse( - Expression.AndAlso( - Expression.Equal(reference, Expression.Constant(null)), - Expression.NotEqual(nullCompare, Expression.Constant(null))), - Expression.AndAlso( - Expression.NotEqual(reference, Expression.Constant(null)), - Expression.Equal(nullCompare, Expression.Constant(null)))), - Expression.AndAlso( - Expression.AndAlso( - Expression.NotEqual(reference, Expression.Constant(null)), - Expression.NotEqual(nullCompare, Expression.Constant(null))), - comparisons))); - - } - } + Expression.Constant(collectionResult != null)), + QueryCompilationContext.QueryContextParameter); - return comparisons is not null; + var param = queryCompilationContext.RegisterRuntimeParameter($"{RuntimeParameterPrefix}{sqlParameterExpression.Name}", lambda); + return new SqlParameterExpression(param.Name, param.Type, CosmosTypeMapping.Default); } } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 8252d822eaa..bbde8614722 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; @@ -340,6 +339,8 @@ protected override Expression VisitExtension(Expression extensionExpression) case QueryParameterExpression queryParameter: return new SqlParameterExpression(queryParameter.Name, queryParameter.Type, null); + case StructuralTypeShaperExpression { StructuralType: IComplexType { ComplexProperty.IsCollection: true } } shaper: + return new CollectionResultExpression(shaper); case StructuralTypeShaperExpression shaper: return new StructuralTypeReferenceExpression(shaper); @@ -385,7 +386,9 @@ protected override Expression VisitExtension(Expression extensionExpression) && (convertedType == null || convertedType.IsAssignableFrom(ese.Type))) { - return new StructuralTypeReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); + return ese.StructuralType is IComplexType { ComplexProperty.IsCollection: true } + ? new CollectionResultExpression(shapedQuery.UpdateShaperExpression(innerExpression)) + : new StructuralTypeReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); } if (innerExpression is ProjectionBindingExpression pbe @@ -952,19 +955,20 @@ public virtual bool TryBindMember( Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null"); - switch (expression) + if (wrapResultExpressionInReferenceExpression) { - case StructuralTypeShaperExpression shaper when wrapResultExpressionInReferenceExpression: - expression = new StructuralTypeReferenceExpression(shaper); - return true; - // case ObjectArrayAccessExpression objectArrayProjectionExpression: - // expression = objectArrayProjectionExpression; - // return true; - default: - return true; + switch (expression) + { + case StructuralTypeShaperExpression { StructuralType: IComplexType { ComplexProperty.IsCollection: true } } shaper: + expression = new CollectionResultExpression(shaper); + return true; + case StructuralTypeShaperExpression shaper: + expression = new StructuralTypeReferenceExpression(shaper); + return true; + } } - // return true; + return true; } private static Expression TryRemoveImplicitConvert(Expression expression) @@ -1094,6 +1098,41 @@ private string DebuggerDisplay() }; } + [DebuggerDisplay("{DebuggerDisplay(),nq}")] + private sealed class CollectionResultExpression : Expression + { + public CollectionResultExpression(StructuralTypeShaperExpression parameter) + { + Parameter = parameter; + ComplexProperty = ((IComplexType)parameter.StructuralType).ComplexProperty; + } + + public CollectionResultExpression(ShapedQueryExpression subquery) + { + Subquery = subquery; + ComplexProperty = ((IComplexType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType).ComplexProperty; + } + + public new StructuralTypeShaperExpression? Parameter { get; } + public ShapedQueryExpression? Subquery { get; } + + public IComplexProperty ComplexProperty { get; } + + public override Type Type + => ComplexProperty.ComplexType.ClrType; + + public override ExpressionType NodeType + => ExpressionType.Extension; + + private string DebuggerDisplay() + => this switch + { + { Parameter: not null } => Parameter.DebuggerDisplay(), + { Subquery: not null } => ExpressionPrinter.Print(Subquery!), + _ => throw new UnreachableException() + }; + } + // private sealed class EntityReferenceExpression : Expression // { // public EntityReferenceExpression(EntityProjectionExpression parameter) diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs index 6d2a238a51d..d59b1fef56c 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs @@ -183,7 +183,7 @@ public virtual Expression BindComplexProperty(IComplexProperty complexProperty, && !complexProperty.DeclaringType.IsAssignableFrom(StructuralType)) { throw new InvalidOperationException( - CosmosStrings.UnableToBindMemberToEntityProjection("navigation", complexProperty.Name, StructuralType.DisplayName())); + CosmosStrings.UnableToBindMemberToEntityProjection("complex property", complexProperty.Name, StructuralType.DisplayName())); } if (!_complexPropertyExpressionsMap.TryGetValue(complexProperty, out var expression)) @@ -334,5 +334,5 @@ public override int GetHashCode() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string ToString() - => $"EntityProjectionExpression: {StructuralType.ShortName()}"; + => $"StructuralTypeProjectionExpression: {StructuralType.ShortName()}"; } diff --git a/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs b/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs index d2ab9842ef7..aaba4ce641a 100644 --- a/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs +++ b/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs @@ -22,17 +22,6 @@ public static class InternalUpdateEntryExtensions public static object? GetCurrentProviderValue(this IInternalEntry updateEntry, IProperty property) { var value = updateEntry.GetCurrentValue(property); - var typeMapping = property.GetTypeMapping(); - value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum - ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value) - : value; - - var converter = typeMapping.Converter; - if (converter != null) - { - value = converter.ConvertToProvider(value); - } - - return value; + return property.ConvertToProviderValue(value); } } diff --git a/src/EFCore/Update/PropertyExtensions.cs b/src/EFCore/Update/PropertyExtensions.cs new file mode 100644 index 00000000000..ade05d1b467 --- /dev/null +++ b/src/EFCore/Update/PropertyExtensions.cs @@ -0,0 +1,35 @@ +// 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.Update; + +/// +/// 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 static class PropertyExtensions +{ + /// + /// 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 static object? ConvertToProviderValue(this IProperty property, object? value) + { + var typeMapping = property.GetTypeMapping(); + value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum + ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value) + : value; + + var converter = typeMapping.Converter; + if (converter != null) + { + value = converter.ConvertToProvider(value); + } + + return value; + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs index b0cd0c3c51d..2fa83bbb327 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -80,7 +80,7 @@ public override Task Distinct_over_projected_nested_collection() => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); public override Task Distinct_over_projected_filtered_nested_collection() - => AssertTranslationFailed(base.Distinct_over_projected_nested_collection); + => AssertTranslationFailed(base.Distinct_over_projected_filtered_nested_collection); #endregion Distinct diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs index aef83efccdb..63ae7093b53 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -7,99 +7,72 @@ public class ComplexPropertiesStructuralEqualityCosmosTest : ComplexPropertiesSt { public ComplexPropertiesStructuralEqualityCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) { + Environment.SetEnvironmentVariable("EF_TEST_REWRITE_BASELINES", "1"); + Fixture.TestSqlLoggerFactory.Clear(); Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); } - public override Task Two_associates() - => AssertTranslationFailed(base.Two_associates); - - public override async Task Two_nested_associates() + public override async Task Two_associates() { - await base.Two_nested_associates(); - + await base.Two_associates(); AssertSql( """ SELECT VALUE c FROM root c -WHERE (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["String"])) +WHERE (c["RequiredAssociate"] = c["OptionalAssociate"]) """); } - [ConditionalFact] - public virtual async Task Required_two_nested_associates() + public override async Task Two_nested_associates() { - await AssertQuery( - ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == e.RequiredAssociate.RequiredNestedAssociate)); + await base.Two_nested_associates(); AssertSql( """ SELECT VALUE c FROM root c -WHERE ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = c["RequiredAssociate"]["RequiredNestedAssociate"]["String"]))) +WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"] = c["OptionalAssociate"]["RequiredNestedAssociate"]) """); } - [ConditionalFact] - public virtual async Task Two_nested_associates_not_equal() + public override async Task Not_equals() { - await AssertQuery( - ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate != e.RequiredAssociate.RequiredNestedAssociate)); - + await base.Not_equals(); AssertSql( """ SELECT VALUE c FROM root c -WHERE ((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) OR ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"]) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != c["RequiredAssociate"]["RequiredNestedAssociate"]["String"])))) +WHERE (c["RequiredAssociate"] != c["OptionalAssociate"]) """); } - [ConditionalFact] - public virtual async Task Two_nested_optional_associates() + public override async Task Associate_with_inline_null() { - await AssertQuery( - ss => ss.Set().Where(e => e.OptionalAssociate != null && e.RequiredAssociate.OptionalNestedAssociate == e.OptionalAssociate.OptionalNestedAssociate)); + await base.Associate_with_inline_null(); AssertSql( """ SELECT VALUE c FROM root c -WHERE ((c["OptionalAssociate"] != null) AND (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] = null)) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = c["OptionalAssociate"]["OptionalNestedAssociate"]["String"]))))) +WHERE (c["OptionalAssociate"] = null) """); } - [ConditionalFact] - public virtual async Task Two_nested_optional_associates_not_equal() + public override async Task Associate_with_parameter_null() { - await AssertQuery( - ss => ss.Set().Where(e => e.OptionalAssociate != null && e.RequiredAssociate.OptionalNestedAssociate != e.OptionalAssociate.OptionalNestedAssociate)); + await base.Associate_with_parameter_null(); AssertSql( """ -SELECT VALUE c -FROM root c -WHERE ((c["OptionalAssociate"] != null) AND ((((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] != null)) OR ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] = null))) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (c["OptionalAssociate"]["OptionalNestedAssociate"] != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Id"]) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Int"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Ints"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["Name"])) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != c["OptionalAssociate"]["OptionalNestedAssociate"]["String"]))))) -"""); - } - - public override Task Not_equals() - => AssertTranslationFailed(base.Not_equals); // Complex collection equality... Need ALL support +@entity_equality_related='null' - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ SELECT VALUE c FROM root c -WHERE (c["OptionalAssociate"] = null) +WHERE (c["OptionalAssociate"] = @entity_equality_related) """); } - public override Task Associate_with_parameter_null() - => AssertTranslationFailed(base.Associate_with_parameter_null); // Complex collection equality... Need ALL support - public override async Task Nested_associate_with_inline_null() { await base.Nested_associate_with_inline_null(); @@ -120,7 +93,7 @@ public override async Task Nested_associate_with_inline() """ SELECT VALUE c FROM root c -WHERE (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = 1000) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = 8)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = [1,2,3])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = "Root1_RequiredAssociate_RequiredNestedAssociate")) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = "foo")) +WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"] = {"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"}) """); } @@ -130,15 +103,11 @@ public override async Task Nested_associate_with_parameter() AssertSql( """ -@entity_equality_nested_Id='1000' -@entity_equality_nested_Int='8' -@entity_equality_nested_Ints='[1,2,3]' -@entity_equality_nested_Name='Root1_RequiredAssociate_RequiredNestedAssociate' -@entity_equality_nested_String='foo' +@entity_equality_nested='{"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"}' SELECT VALUE c FROM root c -WHERE (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = @entity_equality_nested_String)) +WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"] = @entity_equality_nested) """); } @@ -151,16 +120,11 @@ await AssertQuery( AssertSql( """ -@entity_equality_nested=null -@entity_equality_nested_Id=null -@entity_equality_nested_Int=null -@entity_equality_nested_Ints=null -@entity_equality_nested_Name=null -@entity_equality_nested_String=null +@entity_equality_nested='null' SELECT VALUE c FROM root c -WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) +WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = @entity_equality_nested) """); } @@ -173,27 +137,43 @@ await AssertQuery( AssertSql( """ -@entity_equality_nested=null -@entity_equality_nested_Id=null -@entity_equality_nested_Int=null -@entity_equality_nested_Ints=null -@entity_equality_nested_Name=null -@entity_equality_nested_String=null +@entity_equality_nested='null' SELECT VALUE c FROM root c -WHERE ((((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested != null)) OR ((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested = null))) OR (((c["RequiredAssociate"]["OptionalNestedAssociate"] != null) AND (@entity_equality_nested != null)) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] != @entity_equality_nested_Id) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] != @entity_equality_nested_Int)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] != @entity_equality_nested_Ints)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] != @entity_equality_nested_Name)) OR (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] != @entity_equality_nested_String)))) +WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] != @entity_equality_nested) """); } - public override Task Two_nested_collections() - => AssertTranslationFailed(base.Two_nested_collections); // Complex collection equality... Need ALL support + public override async Task Two_nested_collections() + { + await base.Two_nested_collections(); + + AssertSql( + """ + +"""); +} + + public override async Task Nested_collection_with_inline() + { + await base.Nested_collection_with_inline(); + + AssertSql( + """ + +"""); + } + + public override async Task Nested_collection_with_parameter() + { + await base.Nested_collection_with_parameter(); - public override Task Nested_collection_with_inline() - => AssertTranslationFailed(base.Nested_collection_with_inline); // Complex collection equality... Need ALL support + AssertSql( + """ - public override Task Nested_collection_with_parameter() - => AssertTranslationFailed(base.Nested_collection_with_parameter); // Complex collection equality... Need ALL support +"""); + } [ConditionalFact] public override async Task Nullable_value_type_with_null() @@ -221,7 +201,7 @@ FROM root c WHERE EXISTS ( SELECT 1 FROM n IN c["RequiredAssociate"]["NestedCollection"] - WHERE (((((n["Id"] = 1002) AND (n["Int"] = 8)) AND (n["Ints"] = [1,2,3])) AND (n["Name"] = "Root1_RequiredAssociate_NestedCollection_1")) AND (n["String"] = "foo"))) + WHERE (n = {"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"})) """); } @@ -231,18 +211,14 @@ public override async Task Contains_with_parameter() AssertSql( """ -@entity_equality_nested_Id='1002' -@entity_equality_nested_Int='8' -@entity_equality_nested_Ints='[1,2,3]' -@entity_equality_nested_Name='Root1_RequiredAssociate_NestedCollection_1' -@entity_equality_nested_String='foo' +@entity_equality_nested='{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"}' SELECT VALUE c FROM root c WHERE EXISTS ( SELECT 1 FROM n IN c["RequiredAssociate"]["NestedCollection"] - WHERE (((((n["Id"] = @entity_equality_nested_Id) AND (n["Int"] = @entity_equality_nested_Int)) AND (n["Ints"] = @entity_equality_nested_Ints)) AND (n["Name"] = @entity_equality_nested_Name)) AND (n["String"] = @entity_equality_nested_String))) + WHERE (n = @entity_equality_nested)) """); } @@ -253,23 +229,34 @@ public override async Task Contains_with_operators_composed_on_the_collection() AssertSql( """ @get_Item_Int='106' -@entity_equality_get_Item_Id='3003' -@entity_equality_get_Item_Int='108' -@entity_equality_get_Item_Ints='[8,9,109]' -@entity_equality_get_Item_Name='Root3_RequiredAssociate_NestedCollection_2' -@entity_equality_get_Item_String='foo104' +@entity_equality_get_Item='{"Id":3003,"Int":108,"Ints":[8,9,109],"Name":"Root3_RequiredAssociate_NestedCollection_2","String":"foo104"}' SELECT VALUE c FROM root c WHERE EXISTS ( SELECT 1 FROM n IN c["RequiredAssociate"]["NestedCollection"] - WHERE ((n["Int"] > @get_Item_Int) AND (((((n["Id"] = @entity_equality_get_Item_Id) AND (n["Int"] = @entity_equality_get_Item_Int)) AND (n["Ints"] = @entity_equality_get_Item_Ints)) AND (n["Name"] = @entity_equality_get_Item_Name)) AND (n["String"] = @entity_equality_get_Item_String)))) + WHERE ((n["Int"] > @get_Item_Int) AND (n = @entity_equality_get_Item))) """); } public override async Task Contains_with_nested_and_composed_operators() - => await AssertTranslationFailed(base.Contains_with_nested_and_composed_operators); // Complex collection equality... Need ALL support + { + await base.Contains_with_nested_and_composed_operators(); + + AssertSql( + """ +@get_Item_Id='302' +@entity_equality_get_Item='{"Id":303,"Int":130,"Ints":[8,9,131],"Name":"Root3_AssociateCollection_2","String":"foo115","NestedCollection":[{"Id":3014,"Int":136,"Ints":[8,9,137],"Name":"Root3_AssociateCollection_2_NestedCollection_1","String":"foo118"},{"Id":3015,"Int":138,"Ints":[8,9,139],"Name":"Root3_Root1_AssociateCollection_2_NestedCollection_2","String":"foo119"}],"OptionalNestedAssociate":{"Id":3013,"Int":134,"Ints":[8,9,135],"Name":"Root3_AssociateCollection_2_OptionalNestedAssociate","String":"foo117"},"RequiredNestedAssociate":{"Id":3012,"Int":132,"Ints":[8,9,133],"Name":"Root3_AssociateCollection_2_RequiredNestedAssociate","String":"foo116"}}' + +SELECT VALUE c +FROM root c +WHERE EXISTS ( + SELECT 1 + FROM a IN c["AssociateCollection"] + WHERE ((a["Id"] > @get_Item_Id) AND (a = @entity_equality_get_Item))) +"""); + } #endregion Contains From dbd1d17ff3048c14ee00f5b930b06212bccdbd85 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:53:47 +0100 Subject: [PATCH 22/28] Use CollectionResultExpression for complex collections --- ...yableMethodTranslatingExpressionVisitor.cs | 16 +++- .../Internal/CosmosSerializationUtilities.cs | 2 +- ...ingExpressionVisitor.StructuralEquality.cs | 59 ++++++++---- .../CosmosSqlTranslatingExpressionVisitor.cs | 43 +-------- .../Expressions/CollectionResultExpression.cs | 90 +++++++++++++++++++ .../StructuralTypeProjectionExpression.cs | 12 +-- ...xPropertiesStructuralEqualityCosmosTest.cs | 14 ++- .../Query/ComplexTypeQueryCosmosTest.cs | 25 ++---- .../Query/ComplexTypeQueryTestBase.cs | 4 +- 9 files changed, 176 insertions(+), 89 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index c682934569a..9b8b9988d18 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1386,8 +1386,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s switch (translatedExpression) { - case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true } - or IComplexProperty { IsCollection: true }: + case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true }: { var targetStructuralType = shaper.StructuralType; var projection = new StructuralTypeProjectionExpression( @@ -1399,6 +1398,19 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s return CreateShapedQueryExpression(targetStructuralType, select); } + case CollectionResultExpression collectionResult: + { + var shaper = (StructuralTypeShaperExpression)(collectionResult.Parameter ?? ((StructuralTypeShaperExpression)collectionResult.Subquery!.ShaperExpression).ValueBufferExpression); + var targetStructuralType = shaper.StructuralType; + var projection = new StructuralTypeProjectionExpression( + new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType); + var select = SelectExpression.CreateForCollection( + shaper.ValueBufferExpression, + sourceAlias, + projection); + return CreateShapedQueryExpression(targetStructuralType, select); + } + // Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor // (no collection -> no queryable operators) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs index 2f07389b8dd..9fe3eb34878 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs @@ -32,7 +32,7 @@ public static readonly MethodInfo SerializeObjectToComplexPropertyMethod /// public static JToken SerializeObjectToComplexProperty(IComplexType type, object? value, bool collection) // #34567 { - if (value is null) + if (value == null) { return JValue.CreateNull(); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs index e72f2d94561..7ccc9d835b4 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -190,16 +189,20 @@ bool TryRewriteEntityEquality(out SqlExpression? result) bool TryRewriteComplexTypeEquality(bool collection, out SqlExpression? result) { - var (leftAccess, leftComplexType) = ParseComplexAccess(left); - var (rightAccess, rightComplexType) = ParseComplexAccess(right); + var (leftShaper, leftComplexType) = ParseComplexType(left); + var (rightShaper, rightComplexType) = ParseComplexType(right); - if (leftAccess is null || leftAccess == QueryCompilationContext.NotTranslatedExpression || - rightAccess is null || rightAccess == QueryCompilationContext.NotTranslatedExpression) + if (leftShaper is null && rightShaper is null) { result = null; return false; } + var shaper = leftShaper ?? rightShaper; + var complexType = leftComplexType ?? rightComplexType; + + Debug.Assert(complexType != null); + if (leftComplexType is not null && rightComplexType is not null && leftComplexType.ClrType != rightComplexType.ClrType) @@ -210,31 +213,46 @@ bool TryRewriteComplexTypeEquality(bool collection, out SqlExpression? result) return false; } - var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!; + var leftAccess = leftShaper != null ? Visit(leftShaper.ValueBufferExpression) : ParseComplexAccess(left); + var rightAccess = rightShaper != null ? Visit(rightShaper.ValueBufferExpression) : ParseComplexAccess(right); + + if (leftAccess is null || leftAccess == QueryCompilationContext.NotTranslatedExpression || + rightAccess is null || rightAccess == QueryCompilationContext.NotTranslatedExpression) + { + result = null; + return false; + } + result = new SqlBinaryExpression( nodeType, leftAccess, rightAccess, typeof(bool), - boolTypeMapping)!; + typeMappingSource.FindMapping(typeof(bool))!)!; return true; - (Expression?, IComplexType?) ParseComplexAccess(Expression expression) + (StructuralTypeShaperExpression?, IComplexType?) ParseComplexType(Expression expression) => expression switch { StructuralTypeReferenceExpression { StructuralType: IComplexType type } reference - => (Visit((reference.Parameter ?? (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression).ValueBufferExpression), type), + => (reference.Parameter ?? (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression, type), CollectionResultExpression { ComplexProperty: IComplexProperty { ComplexType: var type } } collectionResult - => (Visit((collectionResult.Parameter ?? (StructuralTypeShaperExpression)collectionResult.Subquery!.ShaperExpression).ValueBufferExpression), type), + => (collectionResult.Parameter ?? (StructuralTypeShaperExpression)collectionResult.Subquery!.ShaperExpression, type), + _ => (null, null) + }; + + Expression? ParseComplexAccess(Expression expression) + => expression switch + { SqlParameterExpression sqlParameterExpression - => (CreateJsonQueryParameter(sqlParameterExpression), null), + => CreateJsonQueryParameter(sqlParameterExpression), SqlConstantExpression constant - => (sqlExpressionFactory.Constant( - CosmosSerializationUtilities.SerializeObjectToComplexProperty(type, constant.Value, collectionResult != null), - CosmosTypeMapping.Default), null), + => sqlExpressionFactory.Constant( + CosmosSerializationUtilities.SerializeObjectToComplexProperty(complexType, constant.Value, collection), + CosmosTypeMapping.Default), - _ => (null, null) + _ => null }; Expression CreateJsonQueryParameter(SqlParameterExpression sqlParameterExpression) @@ -242,9 +260,14 @@ Expression CreateJsonQueryParameter(SqlParameterExpression sqlParameterExpressio var lambda = Expression.Lambda( Expression.Call( CosmosSerializationUtilities.SerializeObjectToComplexPropertyMethod, - Expression.Constant(type, typeof(IComplexType)), - Expression.Call(ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string))), - Expression.Constant(collectionResult != null)), + Expression.Constant(complexType, typeof(IComplexType)), + Expression.Convert( + Expression.Call( + ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), + QueryCompilationContext.QueryContextParameter, + Expression.Constant(sqlParameterExpression.Name, typeof(string))), + typeof(object)), + Expression.Constant(collection)), QueryCompilationContext.QueryContextParameter); var param = queryCompilationContext.RegisterRuntimeParameter($"{RuntimeParameterPrefix}{sqlParameterExpression.Name}", lambda); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index bbde8614722..9ebcc68161f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -333,14 +334,14 @@ protected override Expression VisitExtension(Expression extensionExpression) { case StructuralTypeProjectionExpression: case StructuralTypeReferenceExpression: + case ObjectArrayAccessExpression: + case CollectionResultExpression: case SqlExpression: return extensionExpression; case QueryParameterExpression queryParameter: return new SqlParameterExpression(queryParameter.Name, queryParameter.Type, null); - case StructuralTypeShaperExpression { StructuralType: IComplexType { ComplexProperty.IsCollection: true } } shaper: - return new CollectionResultExpression(shaper); case StructuralTypeShaperExpression shaper: return new StructuralTypeReferenceExpression(shaper); @@ -959,9 +960,6 @@ public virtual bool TryBindMember( { switch (expression) { - case StructuralTypeShaperExpression { StructuralType: IComplexType { ComplexProperty.IsCollection: true } } shaper: - expression = new CollectionResultExpression(shaper); - return true; case StructuralTypeShaperExpression shaper: expression = new StructuralTypeReferenceExpression(shaper); return true; @@ -1098,41 +1096,6 @@ private string DebuggerDisplay() }; } - [DebuggerDisplay("{DebuggerDisplay(),nq}")] - private sealed class CollectionResultExpression : Expression - { - public CollectionResultExpression(StructuralTypeShaperExpression parameter) - { - Parameter = parameter; - ComplexProperty = ((IComplexType)parameter.StructuralType).ComplexProperty; - } - - public CollectionResultExpression(ShapedQueryExpression subquery) - { - Subquery = subquery; - ComplexProperty = ((IComplexType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType).ComplexProperty; - } - - public new StructuralTypeShaperExpression? Parameter { get; } - public ShapedQueryExpression? Subquery { get; } - - public IComplexProperty ComplexProperty { get; } - - public override Type Type - => ComplexProperty.ComplexType.ClrType; - - public override ExpressionType NodeType - => ExpressionType.Extension; - - private string DebuggerDisplay() - => this switch - { - { Parameter: not null } => Parameter.DebuggerDisplay(), - { Subquery: not null } => ExpressionPrinter.Print(Subquery!), - _ => throw new UnreachableException() - }; - } - // private sealed class EntityReferenceExpression : Expression // { // public EntityReferenceExpression(EntityProjectionExpression parameter) diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs new file mode 100644 index 00000000000..e826f70d87e --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore.Cosmos.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. +/// +[DebuggerDisplay("{DebuggerDisplay(),nq}")] +public class CollectionResultExpression : Expression +{ + /// + /// 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 CollectionResultExpression(StructuralTypeShaperExpression parameter) + { + Parameter = parameter; + ComplexProperty = ((IComplexType)parameter.StructuralType).ComplexProperty; + } + + /// + /// 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 CollectionResultExpression(ShapedQueryExpression subquery) + { + Subquery = subquery; + ComplexProperty = ((IComplexType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType).ComplexProperty; + } + + /// + /// 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 new StructuralTypeShaperExpression? Parameter { 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 virtual ShapedQueryExpression? Subquery { 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 virtual IComplexProperty ComplexProperty { 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 override Type Type + => ComplexProperty.ComplexType.ClrType; + + /// + /// 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 override ExpressionType NodeType + => ExpressionType.Extension; + + private string DebuggerDisplay() + => this switch + { + { Parameter: not null } => Parameter.DebuggerDisplay(), + { Subquery: not null } => ExpressionPrinter.Print(Subquery!), + _ => throw new UnreachableException() + }; +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs index d59b1fef56c..9e9bc4d9ed4 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -16,7 +17,7 @@ public class StructuralTypeProjectionExpression : Expression, IPrintableExpressi { private readonly Dictionary _propertyExpressionsMap = new(); private readonly Dictionary _navigationExpressionsMap = new(); - private readonly Dictionary _complexPropertyExpressionsMap = new(); + private readonly Dictionary _complexPropertyExpressionsMap = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -190,10 +191,11 @@ public virtual Expression BindComplexProperty(IComplexProperty complexProperty, { // TODO: Unify ObjectAccessExpression and ObjectArrayAccessExpression expression = complexProperty.IsCollection - ? new StructuralTypeShaperExpression( - complexProperty.ComplexType, - new ObjectArrayAccessExpression(Object, complexProperty), - nullable: true) + ? new CollectionResultExpression( + new StructuralTypeShaperExpression( + complexProperty.ComplexType, + new ObjectArrayAccessExpression(Object, complexProperty), + nullable: true)) : new StructuralTypeShaperExpression( complexProperty.ComplexType, new StructuralTypeProjectionExpression(new ObjectAccessExpression(Object, complexProperty), complexProperty.ComplexType), diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs index 63ae7093b53..6f0f36dbeaf 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -7,8 +7,6 @@ public class ComplexPropertiesStructuralEqualityCosmosTest : ComplexPropertiesSt { public ComplexPropertiesStructuralEqualityCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) { - Environment.SetEnvironmentVariable("EF_TEST_REWRITE_BASELINES", "1"); - Fixture.TestSqlLoggerFactory.Clear(); Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); } @@ -151,7 +149,9 @@ public override async Task Two_nested_collections() AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["NestedCollection"] = c["OptionalAssociate"]["NestedCollection"]) """); } @@ -161,7 +161,9 @@ public override async Task Nested_collection_with_inline() AssertSql( """ - +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["NestedCollection"] = [{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}]) """); } @@ -171,7 +173,11 @@ public override async Task Nested_collection_with_parameter() AssertSql( """ +@entity_equality_nestedCollection='[{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}]' +SELECT VALUE c +FROM root c +WHERE (c["RequiredAssociate"]["NestedCollection"] = @entity_equality_nestedCollection) """); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index cc02b711e53..f60f6ea82cd 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -90,7 +90,7 @@ public override Task Complex_type_equals_complex_type(bool async) """ SELECT VALUE c FROM root c -WHERE (((((c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"]) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["Tags"] = c["BillingAddress"]["Tags"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) AND ((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"]))) +WHERE (c["ShippingAddress"] = c["BillingAddress"]) """); }); @@ -103,7 +103,7 @@ public override Task Complex_type_equals_constant(bool async) """ SELECT VALUE c FROM root c -WHERE (((((c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road") AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["Tags"] = ["foo","bar"])) AND (c["ShippingAddress"]["ZipCode"] = 38654)) AND ((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States"))) +WHERE (c["ShippingAddress"] = {"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"Tags":["foo","bar"],"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}}) """); }); @@ -114,16 +114,11 @@ public override Task Complex_type_equals_parameter(bool async) AssertSql( """ -@entity_equality_address_AddressLine1='804 S. Lakeshore Road' -@entity_equality_address_AddressLine2=null -@entity_equality_address_Tags='["foo","bar"]' -@entity_equality_address_ZipCode='38654' -@entity_equality_entity_equality_address_Country_Code='US' -@entity_equality_entity_equality_address_Country_FullName='United States' +@entity_equality_address='{"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"Tags":["foo","bar"],"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}}' SELECT VALUE c FROM root c -WHERE (((((c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["Tags"] = @entity_equality_address_Tags)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) AND ((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName))) +WHERE (c["ShippingAddress"] = @entity_equality_address) """); }); @@ -268,7 +263,7 @@ public override Task Struct_complex_type_equals_struct_complex_type(bool async) """ SELECT VALUE c FROM root c -WHERE ((((c["ShippingAddress"]["AddressLine1"] = c["BillingAddress"]["AddressLine1"]) AND (c["ShippingAddress"]["AddressLine2"] = c["BillingAddress"]["AddressLine2"])) AND (c["ShippingAddress"]["ZipCode"] = c["BillingAddress"]["ZipCode"])) AND ((c["ShippingAddress"]["Country"]["Code"] = c["BillingAddress"]["Country"]["Code"]) AND (c["ShippingAddress"]["Country"]["FullName"] = c["BillingAddress"]["Country"]["FullName"]))) +WHERE (c["ShippingAddress"] = c["BillingAddress"]) """); }); @@ -281,7 +276,7 @@ public override Task Struct_complex_type_equals_constant(bool async) """ SELECT VALUE c FROM root c -WHERE ((((c["ShippingAddress"]["AddressLine1"] = "804 S. Lakeshore Road") AND (c["ShippingAddress"]["AddressLine2"] = null)) AND (c["ShippingAddress"]["ZipCode"] = 38654)) AND ((c["ShippingAddress"]["Country"]["Code"] = "US") AND (c["ShippingAddress"]["Country"]["FullName"] = "United States"))) +WHERE (c["ShippingAddress"] = {"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}}) """); }); @@ -292,15 +287,11 @@ public override Task Struct_complex_type_equals_parameter(bool async) AssertSql( """ -@entity_equality_address_AddressLine1='804 S. Lakeshore Road' -@entity_equality_address_AddressLine2=null -@entity_equality_address_ZipCode='38654' -@entity_equality_entity_equality_address_Country_Code='US' -@entity_equality_entity_equality_address_Country_FullName='United States' +@entity_equality_address='{"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}}' SELECT VALUE c FROM root c -WHERE ((((c["ShippingAddress"]["AddressLine1"] = @entity_equality_address_AddressLine1) AND (c["ShippingAddress"]["AddressLine2"] = @entity_equality_address_AddressLine2)) AND (c["ShippingAddress"]["ZipCode"] = @entity_equality_address_ZipCode)) AND ((c["ShippingAddress"]["Country"]["Code"] = @entity_equality_entity_equality_address_Country_Code) AND (c["ShippingAddress"]["Country"]["FullName"] = @entity_equality_entity_equality_address_Country_FullName))) +WHERE (c["ShippingAddress"] = @entity_equality_address) """); }); diff --git a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs index 23062c6dc78..0cf5ec4e1b1 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs @@ -367,7 +367,7 @@ public virtual Task Struct_complex_type_equals_constant(bool async) })); [ConditionalTheory, MemberData(nameof(IsAsyncData))] - public virtual Task Struct_complex_type_equals_parameter(bool async) + public virtual async Task Struct_complex_type_equals_parameter(bool async) { var address = new AddressStruct { @@ -376,7 +376,7 @@ public virtual Task Struct_complex_type_equals_parameter(bool async) Country = new CountryStruct { FullName = "United States", Code = "US" } }; - return AssertQuery( + await AssertQuery( async, ss => ss.Set().Where(c => c.ShippingAddress == address)); } From ee66c0a1317638e411659ac5537a3da695dc8afe Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:06:36 +0100 Subject: [PATCH 23/28] Regenerate test --- .../Query/AdHocComplexTypeQueryCosmosTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs index c8d84c81d23..2c22522e2ee 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -13,13 +13,11 @@ public override async Task Complex_type_equals_parameter_with_nested_types_with_ AssertSql( """ -@entity_equality_container_Id='1' -@entity_equality_entity_equality_container_Containee1_Id='2' -@entity_equality_entity_equality_container_Containee2_Id='3' +@entity_equality_container='{"Id":1,"Containee1":{"Id":2},"Containee2":{"Id":3}}' SELECT VALUE c FROM root c -WHERE (((c["ComplexContainer"]["Id"] = @entity_equality_container_Id) AND (c["ComplexContainer"]["Containee1"]["Id"] = @entity_equality_entity_equality_container_Containee1_Id)) AND (c["ComplexContainer"]["Containee2"]["Id"] = @entity_equality_entity_equality_container_Containee2_Id)) +WHERE (c["ComplexContainer"] = @entity_equality_container) OFFSET 0 LIMIT 2 """); } From c258b9cf583f16ee91c1e75b91d63fdb1311fbad Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:11:16 +0100 Subject: [PATCH 24/28] Add test and fix --- ...yableMethodTranslatingExpressionVisitor.cs | 4 +- .../CosmosSqlTranslatingExpressionVisitor.cs | 4 +- .../ComplexPropertiesCollectionCosmosTest.cs | 46 +++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 9b8b9988d18..da1cc20abdb 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -1400,7 +1400,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s case CollectionResultExpression collectionResult: { - var shaper = (StructuralTypeShaperExpression)(collectionResult.Parameter ?? ((StructuralTypeShaperExpression)collectionResult.Subquery!.ShaperExpression).ValueBufferExpression); + Debug.Assert(collectionResult.Parameter != null, "CollectionResultExpression can't be bound to member without parameter."); + + var shaper = collectionResult.Parameter; var targetStructuralType = shaper.StructuralType; var projection = new StructuralTypeProjectionExpression( new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 9ebcc68161f..b192483c687 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -387,9 +387,7 @@ protected override Expression VisitExtension(Expression extensionExpression) && (convertedType == null || convertedType.IsAssignableFrom(ese.Type))) { - return ese.StructuralType is IComplexType { ComplexProperty.IsCollection: true } - ? new CollectionResultExpression(shapedQuery.UpdateShaperExpression(innerExpression)) - : new StructuralTypeReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); + return new StructuralTypeReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); } if (innerExpression is ProjectionBindingExpression pbe diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs index 2fa83bbb327..e92b46d76d0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -41,6 +41,52 @@ FROM a IN c["AssociateCollection"] """); } + [ConditionalFact] + public async Task Where_subquery_structural_equality() + { + var param = new AssociateType + { + Id = 1, + Name = "Name 1", + Int = 8, + String = "String 1", + Ints = new List { 1, 2, 3 }, + RequiredNestedAssociate = new NestedAssociateType + { + Id = 1, + Name = "Name 1", + Int = 8, + String = "String 1", + Ints = new List { 1, 2, 3 } + }, + NestedCollection = new List + { + new NestedAssociateType + { + Id = 1, + Name = "Name 1", + Int = 8, + String = "String 1", + Ints = new List { 1, 2, 3 } + } + } + }; + + await AssertQuery( + ss => ss.Set().Where(e => e.AssociateCollection[0] != param), + ss => ss.Set().Where(e => e.AssociateCollection.Count > 0 && e.AssociateCollection[0] != param)); + + + AssertSql( + """ +@entity_equality_param='{"Id":1,"Int":8,"Ints":[1,2,3],"Name":"Name 1","String":"String 1","NestedCollection":[{"Id":1,"Int":8,"Ints":[1,2,3],"Name":"Name 1","String":"String 1"}],"OptionalNestedAssociate":null,"RequiredNestedAssociate":{"Id":1,"Int":8,"Ints":[1,2,3],"Name":"Name 1","String":"String 1"}}' + +SELECT VALUE c +FROM root c +WHERE (c["AssociateCollection"][0] != @entity_equality_param) +"""); + } + public override async Task OrderBy_ElementAt() { // 'ORDER BY' is not supported in subqueries. From d24862363d3b2b5b7abca01a66329b91c293fe32 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:33:55 +0100 Subject: [PATCH 25/28] Add docs --- src/EFCore/Update/PropertyExtensions.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/EFCore/Update/PropertyExtensions.cs b/src/EFCore/Update/PropertyExtensions.cs index ade05d1b467..33d8d05f396 100644 --- a/src/EFCore/Update/PropertyExtensions.cs +++ b/src/EFCore/Update/PropertyExtensions.cs @@ -4,19 +4,20 @@ namespace Microsoft.EntityFrameworkCore.Update; /// -/// 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. +/// Extension methods for . /// +/// +/// See Implementation of database providers and extensions +/// for more information and examples. +/// public static class PropertyExtensions { /// - /// 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. + /// Converts the value of a property to the provider-expected value. /// + /// The value to convert. + /// The property the is for. + /// The converted value. public static object? ConvertToProviderValue(this IProperty property, object? value) { var typeMapping = property.GetTypeMapping(); From b3d68ebd7cb6b3ecd58885953b65cc742a56f6bc Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:34:38 +0100 Subject: [PATCH 26/28] Use property clr type --- .../Query/Internal/Expressions/CollectionResultExpression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs index e826f70d87e..81a2b668a02 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs @@ -69,7 +69,7 @@ public CollectionResultExpression(ShapedQueryExpression subquery) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Type Type - => ComplexProperty.ComplexType.ClrType; + => ComplexProperty.ClrType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to From 3a19515b75dd83933d82e62afb573756661e4219 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:50:56 +0100 Subject: [PATCH 27/28] Remove bom --- src/EFCore/Update/PropertyExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore/Update/PropertyExtensions.cs b/src/EFCore/Update/PropertyExtensions.cs index 33d8d05f396..fc3317481b6 100644 --- a/src/EFCore/Update/PropertyExtensions.cs +++ b/src/EFCore/Update/PropertyExtensions.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Update; From ddcba7e54c1aa1e29c20c36dc56b49f78fd03130 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:54:02 +0100 Subject: [PATCH 28/28] Make internal --- .../Internal/CosmosSerializationUtilities.cs | 3 ++ .../Update/Internal/PropertyExtensions.cs | 35 ++++++++++++++++++ src/EFCore/Update/PropertyExtensions.cs | 36 ------------------- 3 files changed, 38 insertions(+), 36 deletions(-) create mode 100644 src/EFCore/Update/Internal/PropertyExtensions.cs delete mode 100644 src/EFCore/Update/PropertyExtensions.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs index 9fe3eb34878..41e34876613 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs @@ -3,6 +3,7 @@ using System.Collections; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Microsoft.EntityFrameworkCore.Update.Internal; using Newtonsoft.Json.Linq; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -53,7 +54,9 @@ public static JToken SerializeObjectToComplexProperty(IComplexType type, object? var jsonPropertyName = property.GetJsonPropertyName(); var propertyValue = property.GetGetter().GetClrValue(value); +#pragma warning disable EF1001 // Internal EF Core API usage. var providerValue = property.ConvertToProviderValue(propertyValue); +#pragma warning restore EF1001 // Internal EF Core API usage. if (providerValue is null) { if (!property.IsNullable) diff --git a/src/EFCore/Update/Internal/PropertyExtensions.cs b/src/EFCore/Update/Internal/PropertyExtensions.cs new file mode 100644 index 00000000000..0f7cb3655ed --- /dev/null +++ b/src/EFCore/Update/Internal/PropertyExtensions.cs @@ -0,0 +1,35 @@ +// 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.Update.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. +/// +public static class PropertyExtensions +{ + /// + /// 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 static object? ConvertToProviderValue(this IProperty property, object? value) + { + var typeMapping = property.GetTypeMapping(); + value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum + ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value) + : value; + + var converter = typeMapping.Converter; + if (converter != null) + { + value = converter.ConvertToProvider(value); + } + + return value; + } +} diff --git a/src/EFCore/Update/PropertyExtensions.cs b/src/EFCore/Update/PropertyExtensions.cs deleted file mode 100644 index fc3317481b6..00000000000 --- a/src/EFCore/Update/PropertyExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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.Update; - -/// -/// Extension methods for . -/// -/// -/// See Implementation of database providers and extensions -/// for more information and examples. -/// -public static class PropertyExtensions -{ - /// - /// Converts the value of a property to the provider-expected value. - /// - /// The value to convert. - /// The property the is for. - /// The converted value. - public static object? ConvertToProviderValue(this IProperty property, object? value) - { - var typeMapping = property.GetTypeMapping(); - value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum - ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value) - : value; - - var converter = typeMapping.Converter; - if (converter != null) - { - value = converter.ConvertToProvider(value); - } - - return value; - } -}