From 654e8aaee33e390edc4cc8cd06a1ac70dbe7e5eb Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:26:34 +0100 Subject: [PATCH 1/4] Cosmos: Initialize empty collections Closes: #36577 --- ...ionBindingRemovingExpressionVisitorBase.cs | 26 +++++-- .../OwnedNavigationsCosmosFixture.cs | 78 ------------------- 2 files changed, 18 insertions(+), 86 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index 00736d76676..f9acd1bb88f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -502,16 +502,21 @@ private static void IncludeCollection( if (relatedEntities != null) { + var enumarated = false; foreach (var relatedEntity in relatedEntities) { + enumarated = true; fixup(includingEntity, relatedEntity); inverseNavigation?.SetIsLoadedWhenNoTracking(relatedEntity); } + + if (enumarated) + { + return; + } } - else - { - initialize(includingEntity); - } + + initialize(includingEntity); } else { @@ -525,14 +530,19 @@ private static void IncludeCollection( if (relatedEntities != null) { using var enumerator = relatedEntities.GetEnumerator(); + var enumerated = false; while (enumerator.MoveNext()) { + enumerated = true; + } + + if (enumerated) + { + return; } } - else - { - initialize((TIncludingEntity)entity); - } + + initialize((TIncludingEntity)entity); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCosmosFixture.cs index 75ea4719072..b07fc014f60 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCosmosFixture.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCosmosFixture.cs @@ -31,82 +31,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .ToContainer("RootEntities") .HasNoDiscriminator(); } - - // We need to override the following asserters because of #36577: - // the Cosmos provider incorrectly returns null for empty collections in some cases - protected override void AssertRootEntity(RootEntity e, RootEntity a) - { - Assert.Equal(e.Id, a.Id); - Assert.Equal(e.Name, a.Name); - - NullSafeAssert(e.RequiredAssociate, a.RequiredAssociate, AssertAssociate); - NullSafeAssert(e.OptionalAssociate, a.OptionalAssociate, AssertAssociate); - - if (e.AssociateCollection is not null && a.AssociateCollection is not null) - { - Assert.Equal(e.AssociateCollection.Count, a.AssociateCollection.Count); - - var (orderedExpected, orderedActual) = (e.AssociateCollection, a.AssociateCollection); - - for (var i = 0; i < e.AssociateCollection.Count; i++) - { - AssertAssociate(orderedExpected[i], orderedActual[i]); - } - } - else - { - // #36577: the Cosmos provider incorrectly returns null for empty collections in some cases - if (e.AssociateCollection is [] && a.AssociateCollection is null) - { - return; - } - - Assert.Equal(e.AssociateCollection, a.AssociateCollection); - } - } - - protected override void AssertAssociate(AssociateType e, AssociateType a) - { - Assert.Equal(e.Id, a.Id); - Assert.Equal(e.Name, a.Name); - - Assert.Equal(e.Int, a.Int); - Assert.Equal(e.String, a.String); - - NullSafeAssert(e.RequiredNestedAssociate, a.RequiredNestedAssociate, AssertNestedAssociate); - NullSafeAssert(e.OptionalNestedAssociate, a.OptionalNestedAssociate, AssertNestedAssociate); - - if (e.NestedCollection is not null && a.NestedCollection != null) - { - Assert.Equal(e.NestedCollection.Count, a.NestedCollection.Count); - - var (orderedExpected, orderedActual) = (e.NestedCollection, a.NestedCollection); - - for (var i = 0; i < e.NestedCollection.Count; i++) - { - AssertNestedAssociate(orderedExpected[i], orderedActual[i]); - } - } - else - { - // #36577: the Cosmos provider incorrectly returns null for empty collections in some cases - if (e.NestedCollection is [] && a.NestedCollection is null) - { - return; - } - - Assert.Equal(e.NestedCollection, a.NestedCollection); - } - } - - private static void NullSafeAssert(object? e, object? a, Action assertAction) - { - if (e is T ee && a is T aa) - { - assertAction(ee, aa); - return; - } - - Assert.Equal(e, a); - } } From 970b98723abfd0b668fa75830c699816dc920725 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:19:58 +0100 Subject: [PATCH 2/4] Use GetOrCreate --- ...ionBindingRemovingExpressionVisitorBase.cs | 51 +++---------------- 1 file changed, 7 insertions(+), 44 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index f9acd1bb88f..1fb0874148f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -402,7 +402,6 @@ private void AddInclude( var inverseNavigation = navigation.Inverse; var fixup = GenerateFixup( includingClrType, relatedEntityClrType, navigation, inverseNavigation); - var initialize = GenerateInitialize(includingClrType, navigation); var navigationExpression = Visit(includeExpression.NavigationExpression); @@ -421,7 +420,6 @@ private void AddInclude( Constant(navigation), Constant(inverseNavigation, typeof(INavigation)), Constant(fixup), - Constant(initialize, typeof(Action<>).MakeGenericType(includingClrType)), #pragma warning disable EF1001 // Internal EF Core API usage. Constant(includeExpression.SetLoaded)))); #pragma warning restore EF1001 // Internal EF Core API usage. @@ -441,8 +439,7 @@ private static void IncludeReference( INavigation navigation, INavigation inverseNavigation, Action fixup, - Action _, - bool __) + bool _) { if (entity == null || !navigation.DeclaringEntityType.IsAssignableFrom(entityType)) @@ -486,7 +483,6 @@ private static void IncludeCollection( INavigation navigation, INavigation inverseNavigation, Action fixup, - Action initialize, bool setLoaded) { if (entity == null @@ -495,6 +491,11 @@ private static void IncludeCollection( return; } + if (relatedEntities != null) + { + navigation.GetCollectionAccessor()!.GetOrCreate(entity, forMaterialization: true); + } + if (entry == null) { var includingEntity = (TIncludingEntity)entity; @@ -502,21 +503,12 @@ private static void IncludeCollection( if (relatedEntities != null) { - var enumarated = false; foreach (var relatedEntity in relatedEntities) { - enumarated = true; fixup(includingEntity, relatedEntity); inverseNavigation?.SetIsLoadedWhenNoTracking(relatedEntity); } - - if (enumarated) - { - return; - } } - - initialize(includingEntity); } else { @@ -529,20 +521,12 @@ private static void IncludeCollection( if (relatedEntities != null) { + // Enumarator contains logic for tracking the entities, so we need to make sure to enumerate it using var enumerator = relatedEntities.GetEnumerator(); - var enumerated = false; while (enumerator.MoveNext()) { - enumerated = true; - } - - if (enumerated) - { - return; } } - - initialize((TIncludingEntity)entity); } } @@ -573,27 +557,6 @@ private static Delegate GenerateFixup( .Compile(); } - private static Delegate GenerateInitialize( - Type entityType, - INavigation navigation) - { - if (!navigation.IsCollection) - { - return null; - } - - var entityParameter = Parameter(entityType); - - var getOrCreateExpression = Call( - Constant(navigation.GetCollectionAccessor()), - CollectionAccessorGetOrCreateMethodInfo, - entityParameter, - Constant(true)); - - return Lambda(Block(typeof(void), getOrCreateExpression), entityParameter) - .Compile(); - } - private static Expression AssignReferenceNavigation( ParameterExpression entity, ParameterExpression relatedEntity, From 6ee770b893f0ed3d5ec241047327953e58d437ae Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:29:56 +0100 Subject: [PATCH 3/4] Always initialize --- ...smosProjectionBindingRemovingExpressionVisitorBase.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index 1fb0874148f..8e6fa41e61b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -37,10 +37,6 @@ private static readonly MethodInfo CollectionAccessorAddMethodInfo = typeof(IClrCollectionAccessor).GetTypeInfo() .GetDeclaredMethod(nameof(IClrCollectionAccessor.Add)); - private static readonly MethodInfo CollectionAccessorGetOrCreateMethodInfo - = typeof(IClrCollectionAccessor).GetTypeInfo() - .GetDeclaredMethod(nameof(IClrCollectionAccessor.GetOrCreate)); - private readonly IDictionary _materializationContextBindings = new Dictionary(); @@ -491,10 +487,7 @@ private static void IncludeCollection( return; } - if (relatedEntities != null) - { - navigation.GetCollectionAccessor()!.GetOrCreate(entity, forMaterialization: true); - } + navigation.GetCollectionAccessor()!.GetOrCreate(entity, forMaterialization: true); if (entry == null) { From e166e109e30c8d19fbb22931a010c7ef0f536a82 Mon Sep 17 00:00:00 2001 From: JoasE <32096708+JoasE@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:30:21 +0100 Subject: [PATCH 4/4] fix typo --- ...itor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index 8e6fa41e61b..d9b7b6fa3e5 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -514,7 +514,7 @@ private static void IncludeCollection( if (relatedEntities != null) { - // Enumarator contains logic for tracking the entities, so we need to make sure to enumerate it + // Enumerator contains logic for tracking the entities, so we need to make sure to enumerate it using var enumerator = relatedEntities.GetEnumerator(); while (enumerator.MoveNext()) {