From ea662ee2cd89f6ab5c3bf139286158fbfb3d9183 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Fri, 1 May 2026 12:43:22 -0700 Subject: [PATCH 01/16] Fix flaky CosmosBulkConcurrencyTest by using unique database name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CosmosBulkConcurrencyTest and CosmosConcurrencyTest both inherited the same StoreName ('CosmosConcurrencyTest'). Since xUnit runs test classes in parallel, their CleanAsync calls raced against the same database — one class could delete the database while the other was creating containers, causing a 404 NotFound. Override StoreName in the bulk fixture so each class uses its own database. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Update/CosmosBulkConcurrencyTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs index 59bc6392de3..73d5e641240 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs @@ -17,6 +17,9 @@ public override Task Updating_then_updating_the_same_entity_results_in_DbUpdateC public class ConcurrencyFixture : CosmosConcurrencyTest.CosmosFixture { + protected override string StoreName + => "CosmosBulkConcurrencyTest"; + public override ConcurrencyContext CreateContext() { var context = base.CreateContext(); From 25ebd58bcd99efe3995f40222ff0c58601d052d3 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Fri, 1 May 2026 13:47:24 -0700 Subject: [PATCH 02/16] Update test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Update/CosmosBulkConcurrencyTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs index 73d5e641240..c9a03fd6f43 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs @@ -18,7 +18,7 @@ public override Task Updating_then_updating_the_same_entity_results_in_DbUpdateC public class ConcurrencyFixture : CosmosConcurrencyTest.CosmosFixture { protected override string StoreName - => "CosmosBulkConcurrencyTest"; + => nameof(CosmosBulkConcurrencyTest); public override ConcurrencyContext CreateContext() { From 3f2d6882ab0b713c4f967dc8c38cc12cb33f8283 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Fri, 1 May 2026 23:39:52 -0700 Subject: [PATCH 03/16] Clear change tracker before CleanAsyncImpl to fix retry identity conflicts When the AsyncSeeder adds entities to the context and SaveChangesAsync hits a transient Cosmos error (429/503), the execution strategy retries CleanAsyncImpl with the same context. The retry's seeder then fails trying to Add entities already tracked from the previous attempt. Clear ChangeTracker at the start of each attempt so retries start clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestUtilities/CosmosTestStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 8db814b5c87..997430b1095 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -253,6 +253,8 @@ public override Task CleanAsync(DbContext context, bool createTables = true) => new TestCosmosExecutionStrategy().ExecuteAsync( (context, createTables), async (_, state, ct) => { + // Clear tracked entities so execution strategy retries don't fail with identity conflicts + state.context.ChangeTracker.Clear(); await CleanAsyncImpl(state.context, state.createTables).ConfigureAwait(false); return true; }, null, default); From 89549427c730256d830743204b5e471713c6759d Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Fri, 1 May 2026 23:53:20 -0700 Subject: [PATCH 04/16] Move ChangeTracker.Clear to CosmosDatabaseCreator.EnsureCreatedAsync The seeder invoked by SeedDataAsync adds entities to the context, so if EnsureCreatedAsync is called inside any retry loop and the seeder's SaveChangesAsync hits a transient error, the next attempt finds stale tracked entities and fails with an identity conflict. Clearing the tracker at the start of EnsureCreatedAsync makes the method inherently retry-safe rather than relying on callers to clear state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs | 5 +++++ .../TestUtilities/CosmosTestStore.cs | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 0c93c65b14b..dbe470f3455 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -51,6 +51,11 @@ public CosmosDatabaseCreator( /// public virtual async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { + // Clear tracked entities so this method is safe to call inside a retry loop. + // The seeder adds entities to the context, so a previous failed call would leave + // stale entries in the change tracker that conflict on the next attempt. + _currentContext.Context.ChangeTracker.Clear(); + var model = _designTimeModel.Model; var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(model.GetThroughput(), cancellationToken) .ConfigureAwait(false); diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 997430b1095..8db814b5c87 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -253,8 +253,6 @@ public override Task CleanAsync(DbContext context, bool createTables = true) => new TestCosmosExecutionStrategy().ExecuteAsync( (context, createTables), async (_, state, ct) => { - // Clear tracked entities so execution strategy retries don't fail with identity conflicts - state.context.ChangeTracker.Clear(); await CleanAsyncImpl(state.context, state.createTables).ConfigureAwait(false); return true; }, null, default); From de4234143b7ec0cc36dd6357fee515d8dcc0ade6 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Sat, 2 May 2026 00:08:31 -0700 Subject: [PATCH 05/16] Defer shared Cosmos database deletion to process exit Multiple test fixtures can share the same Cosmos database (e.g. all Northwind query tests share 'Northwind'). Previously, the first fixture to dispose would delete the database, causing other fixtures still running in parallel to fail with NotFound or wrong data. Shared stores now register themselves in a static dictionary at construction time. A static constructor registers a ProcessExit handler that deletes all shared databases in parallel after the test run completes. Non-shared databases continue to be deleted immediately on dispose. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestUtilities/CosmosTestStore.cs | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 8db814b5c87..c418893089b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -1,6 +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.Collections.Concurrent; using System.Net; using System.Net.Sockets; using Azure; @@ -24,6 +25,30 @@ public class CosmosTestStore : TestStore private static readonly Guid _runId = Guid.NewGuid(); private static bool? _connectionAvailable; + // Shared databases are deleted at process exit, not when individual fixtures dispose, + // because multiple fixtures can share the same database concurrently. + private static readonly ConcurrentDictionary _sharedStores = new(); + + static CosmosTestStore() + { + AppDomain.CurrentDomain.ProcessExit += static (_, _) => + { + Task.WhenAll(_sharedStores.Select( + async entry => + { + try + { + await entry.Value.EnsureDeletedAsync(entry.Value._storeContext).ConfigureAwait(false); + } + catch + { + } + + entry.Value._storeContext.Dispose(); + })).GetAwaiter().GetResult(); + }; + } + public static CosmosTestStore Create(string name, Action? extensionConfiguration = null) => new(name, shared: false, extensionConfiguration: extensionConfiguration); @@ -58,6 +83,11 @@ private CosmosTestStore( }; _storeContext = new TestStoreContext(this); + + if (shared) + { + _sharedStores.TryAdd(Name, this); + } } private static string CreateName(string name) @@ -481,22 +511,11 @@ private static async Task SeedAsync(DbContext context) public override async ValueTask DisposeAsync() { - if (_initialized) + if (_initialized && _connectionAvailable != false && !Shared) { - if (_connectionAvailable == false) - { - return; - } - - if (Shared) - { - GetTestStoreIndex(ServiceProvider).RemoveShared(GetType().Name + Name); - } - await EnsureDeletedAsync(_storeContext).ConfigureAwait(false); + _storeContext.Dispose(); } - - _storeContext.Dispose(); } private class TestStoreContext(CosmosTestStore testStore) : DbContext From 03a6ac89613fafbc6a3ecb27ad2ef34a21b6b74e Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Sat, 2 May 2026 00:52:22 -0700 Subject: [PATCH 06/16] Wrap seeding in execution strategy and clear change tracker - CosmosDatabaseCreator.EnsureCreatedAsync: wrap entire body in execution strategy. Use StrongBox to persist the 'created' flag across retries. Clear ChangeTracker only when AsyncSeeder is set. - RelationalDatabaseCreator.EnsureCreated/EnsureCreatedAsync: wrap seeding in the execution strategy. Clear ChangeTracker only when the seeder is set. - Migrator.MigrateImplementation/MigrateAsyncImplementation: clear the change tracker before invoking the seeder. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/Internal/CosmosDatabaseCreator.cs | 59 ++++++++----- .../Migrations/Internal/Migrator.cs | 2 + .../Storage/RelationalDatabaseCreator.cs | 84 +++++++++++-------- .../TestUtilities/CosmosTestStore.cs | 39 ++++++--- 4 files changed, 116 insertions(+), 68 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index dbe470f3455..be4e3fbf2fc 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -1,6 +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.Runtime.CompilerServices; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; @@ -20,6 +21,7 @@ public class CosmosDatabaseCreator : IDatabaseCreator private readonly IDatabase _database; private readonly ICurrentDbContext _currentContext; private readonly IDbContextOptions _contextOptions; + private readonly IExecutionStrategy _executionStrategy; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,7 +35,8 @@ public CosmosDatabaseCreator( IUpdateAdapterFactory updateAdapterFactory, IDatabase database, ICurrentDbContext currentContext, - IDbContextOptions contextOptions) + IDbContextOptions contextOptions, + IExecutionStrategy executionStrategy) { _cosmosClient = cosmosClient; _designTimeModel = designTimeModel; @@ -41,6 +44,7 @@ public CosmosDatabaseCreator( _database = database; _currentContext = currentContext; _contextOptions = contextOptions; + _executionStrategy = executionStrategy; } /// @@ -49,31 +53,46 @@ public CosmosDatabaseCreator( /// 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 async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) + public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { - // Clear tracked entities so this method is safe to call inside a retry loop. - // The seeder adds entities to the context, so a previous failed call would leave - // stale entries in the change tracker that conflict on the next attempt. - _currentContext.Context.ChangeTracker.Clear(); + var created = new StrongBox(false); + return _executionStrategy.ExecuteAsync( + (Creator: this, Created: created), static async (_, state, ct) => + { + var creator = state.Creator; + var model = creator._designTimeModel.Model; + state.Created.Value |= await creator._cosmosClient + .CreateDatabaseIfNotExistsAsync(model.GetThroughput(), ct) + .ConfigureAwait(false); - var model = _designTimeModel.Model; - var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(model.GetThroughput(), cancellationToken) - .ConfigureAwait(false); + foreach (var container in GetContainersToCreate(model)) + { + state.Created.Value |= await creator._cosmosClient + .CreateContainerIfNotExistsAsync(container, ct) + .ConfigureAwait(false); + } - foreach (var container in GetContainersToCreate(model)) - { - created |= await _cosmosClient.CreateContainerIfNotExistsAsync(container, cancellationToken) - .ConfigureAwait(false); - } + if (state.Created.Value) + { + await creator.InsertDataAsync(ct).ConfigureAwait(false); + } - if (created) - { - await InsertDataAsync(cancellationToken).ConfigureAwait(false); - } + var coreOptionsExtension = + creator._contextOptions.FindExtension(); - await SeedDataAsync(created, cancellationToken).ConfigureAwait(false); + if (coreOptionsExtension?.AsyncSeeder is not null) + { + creator._currentContext.Context.ChangeTracker.Clear(); + await coreOptionsExtension.AsyncSeeder( + creator._currentContext.Context, state.Created.Value, ct).ConfigureAwait(false); + } + else if (coreOptionsExtension?.Seeder is not null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } - return created; + return state.Created.Value; + }, verifySucceeded: null, cancellationToken); } private static IEnumerable GetContainersToCreate(IModel model) diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 55661d495eb..92f03d00b5f 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -192,6 +192,7 @@ private bool MigrateImplementation( var seed = coreOptionsExtension.Seeder; if (seed != null) { + context.ChangeTracker.Clear(); seed(context, state.AnyOperationPerformed); } else if (coreOptionsExtension.AsyncSeeder != null) @@ -328,6 +329,7 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync( var seedAsync = coreOptionsExtension.AsyncSeeder; if (seedAsync != null) { + context.ChangeTracker.Clear(); await seedAsync(context, state.AnyOperationPerformed, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension.Seeder != null) diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index fffd11eac4a..b5c7899cc9a 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -250,24 +250,29 @@ public virtual bool EnsureCreated() operationsPerformed = true; } - var coreOptionsExtension = - Dependencies.ContextOptions.FindExtension() - ?? new CoreOptionsExtension(); - - var seed = coreOptionsExtension.Seeder; - if (seed != null) - { - var context = Dependencies.CurrentContext.Context; - using var transaction = context.Database.BeginTransaction(); - seed(context, operationsPerformed); - transaction.Commit(); - } - else if (coreOptionsExtension.AsyncSeeder != null) - { - throw new InvalidOperationException(CoreStrings.MissingSeeder); - } - - return operationsPerformed; + return Dependencies.ExecutionStrategy.Execute( + (this, operationsPerformed), static (context, state) => + { + var (creator, created) = state; + + var coreOptionsExtension = + creator.Dependencies.ContextOptions.FindExtension(); + + var seed = coreOptionsExtension?.Seeder; + if (seed != null) + { + context.ChangeTracker.Clear(); + using var transaction = context.Database.BeginTransaction(); + seed(context, created); + transaction.Commit(); + } + else if (coreOptionsExtension?.AsyncSeeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return created; + }, verifySucceeded: null); } /// @@ -300,25 +305,30 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio operationsPerformed = true; } - var coreOptionsExtension = - Dependencies.ContextOptions.FindExtension() - ?? new CoreOptionsExtension(); - - var seedAsync = coreOptionsExtension.AsyncSeeder; - if (seedAsync != null) - { - var context = Dependencies.CurrentContext.Context; - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using var _ = transaction.ConfigureAwait(false); - await seedAsync(context, operationsPerformed, cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } - else if (coreOptionsExtension.Seeder != null) - { - throw new InvalidOperationException(CoreStrings.MissingSeeder); - } - - return operationsPerformed; + return await Dependencies.ExecutionStrategy.ExecuteAsync( + (this, operationsPerformed), static async (context, state, ct) => + { + var (creator, created) = state; + + var coreOptionsExtension = + creator.Dependencies.ContextOptions.FindExtension(); + + var seedAsync = coreOptionsExtension?.AsyncSeeder; + if (seedAsync != null) + { + context.ChangeTracker.Clear(); + var transaction = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false); + await using var _ = transaction.ConfigureAwait(false); + await seedAsync(context, created, ct).ConfigureAwait(false); + await transaction.CommitAsync(ct).ConfigureAwait(false); + } + else if (coreOptionsExtension?.Seeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return created; + }, verifySucceeded: null, cancellationToken).ConfigureAwait(false); } /// diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index c418893089b..fd7f91c8cf3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -25,26 +25,31 @@ public class CosmosTestStore : TestStore private static readonly Guid _runId = Guid.NewGuid(); private static bool? _connectionAvailable; - // Shared databases are deleted at process exit, not when individual fixtures dispose, - // because multiple fixtures can share the same database concurrently. - private static readonly ConcurrentDictionary _sharedStores = new(); + // Databases shared across multiple test fixtures are deleted at process exit + // to avoid one fixture's disposal racing with another fixture's queries. + // Single-fixture databases are deleted immediately to avoid hitting emulator limits. + private static readonly HashSet _deferredDeletionStoreNames = ["Northwind", "F1Test"]; + private static readonly ConcurrentDictionary _deferredStores = new(); static CosmosTestStore() { AppDomain.CurrentDomain.ProcessExit += static (_, _) => { - Task.WhenAll(_sharedStores.Select( + Task.WhenAll(_deferredStores.Select( async entry => { + var store = entry.Value; try { - await entry.Value.EnsureDeletedAsync(entry.Value._storeContext).ConfigureAwait(false); + store.GetTestStoreIndex(store.ServiceProvider) + .RemoveShared(store.GetType().Name + store.Name); + await store.EnsureDeletedAsync(store._storeContext).ConfigureAwait(false); } catch { } - entry.Value._storeContext.Dispose(); + store._storeContext.Dispose(); })).GetAwaiter().GetResult(); }; } @@ -84,9 +89,9 @@ private CosmosTestStore( _storeContext = new TestStoreContext(this); - if (shared) + if (shared && _deferredDeletionStoreNames.Contains(name)) { - _sharedStores.TryAdd(Name, this); + _deferredStores.TryAdd(Name, this); } } @@ -511,11 +516,23 @@ private static async Task SeedAsync(DbContext context) public override async ValueTask DisposeAsync() { - if (_initialized && _connectionAvailable != false && !Shared) + if (!_initialized || _connectionAvailable == false) { - await EnsureDeletedAsync(_storeContext).ConfigureAwait(false); - _storeContext.Dispose(); + return; + } + + if (_deferredStores.ContainsKey(Name)) + { + return; } + + if (Shared) + { + GetTestStoreIndex(ServiceProvider).RemoveShared(GetType().Name + Name); + } + + await EnsureDeletedAsync(_storeContext).ConfigureAwait(false); + _storeContext.Dispose(); } private class TestStoreContext(CosmosTestStore testStore) : DbContext From 8af8b6a05ee3d6632c744688ba6be0b318b207e4 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Sat, 2 May 2026 22:34:40 -0700 Subject: [PATCH 07/16] Fix remaining flaky tests and refactor seeding - Refactor CosmosDatabaseCreator: move ChangeTracker.Clear() into SeedDataAsync so both EnsureCreatedAsync and CosmosTestStore.SeedAsync benefit from the fix without duplication. - Add InternalServerError (500) to CosmosExecutionStrategy retryable errors. The Cosmos emulator returns 500 when overwhelmed with concurrent operations. - Fix ReadItemPartitionKeyQueryFixtureBase entity sorter for SinglePartitionKeyEntity to sort by (Id, PartitionKey) instead of just Id. Two entities share the same Id across partitions, causing non-deterministic assertion matching. - Dispose _storeContext for deferred shared stores in DisposeAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/Internal/CosmosDatabaseCreator.cs | 22 +++++-------------- .../Internal/CosmosExecutionStrategy.cs | 1 + .../ReadItemPartitionKeyQueryFixtureBase.cs | 2 +- .../TestUtilities/CosmosTestStore.cs | 1 + 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index be4e3fbf2fc..f37672889b9 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -77,19 +77,7 @@ public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken await creator.InsertDataAsync(ct).ConfigureAwait(false); } - var coreOptionsExtension = - creator._contextOptions.FindExtension(); - - if (coreOptionsExtension?.AsyncSeeder is not null) - { - creator._currentContext.Context.ChangeTracker.Clear(); - await coreOptionsExtension.AsyncSeeder( - creator._currentContext.Context, state.Created.Value, ct).ConfigureAwait(false); - } - else if (coreOptionsExtension?.Seeder is not null) - { - throw new InvalidOperationException(CoreStrings.MissingSeeder); - } + await creator.SeedDataAsync(state.Created.Value, ct).ConfigureAwait(false); return state.Created.Value; }, verifySucceeded: null, cancellationToken); @@ -220,14 +208,14 @@ private IUpdateAdapter AddModelData() public virtual async Task SeedDataAsync(bool created, CancellationToken cancellationToken = default) { var coreOptionsExtension = - _contextOptions.FindExtension() - ?? new CoreOptionsExtension(); + _contextOptions.FindExtension(); - if (coreOptionsExtension.AsyncSeeder is not null) + if (coreOptionsExtension?.AsyncSeeder is not null) { + _currentContext.Context.ChangeTracker.Clear(); await coreOptionsExtension.AsyncSeeder(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); } - else if (coreOptionsExtension.Seeder != null) + else if (coreOptionsExtension?.Seeder is not null) { throw new InvalidOperationException(CoreStrings.MissingSeeder); } diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs index ce17e5add19..5b2a8514914 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs @@ -104,6 +104,7 @@ protected override bool ShouldRetryOn(Exception exception) static bool IsTransient(HttpStatusCode? statusCode) => statusCode is null + or HttpStatusCode.InternalServerError or HttpStatusCode.ServiceUnavailable or HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs index e451dfb03d7..5b9ff1577a2 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs @@ -108,7 +108,7 @@ public override ISetSource GetExpectedData() { { typeof(HierarchicalPartitionKeyEntity), e => ((HierarchicalPartitionKeyEntity?)e)?.Id }, { typeof(OnlyHierarchicalPartitionKeyEntity), e => ((OnlyHierarchicalPartitionKeyEntity?)e)?.Payload }, - { typeof(SinglePartitionKeyEntity), e => ((SinglePartitionKeyEntity?)e)?.Id }, + { typeof(SinglePartitionKeyEntity), e => (((SinglePartitionKeyEntity?)e)?.Id, ((SinglePartitionKeyEntity?)e)?.PartitionKey) }, { typeof(FancyDiscriminatorEntity), e => ((FancyDiscriminatorEntity?)e)?.Id }, { typeof(OnlySinglePartitionKeyEntity), e => ((OnlySinglePartitionKeyEntity?)e)?.Payload }, { typeof(NoPartitionKeyEntity), e => ((NoPartitionKeyEntity?)e)?.Id }, diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index fd7f91c8cf3..ef86eaee6d7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -523,6 +523,7 @@ public override async ValueTask DisposeAsync() if (_deferredStores.ContainsKey(Name)) { + _storeContext.Dispose(); return; } From bdb4d0ac150bbfafde0b27e840b28d35c8824a62 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Sun, 3 May 2026 09:28:37 -0700 Subject: [PATCH 08/16] Only clear ChangeTracker on retry, fix InsertData idempotency and disposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CosmosDatabaseCreator.EnsureCreatedAsync: - Track DataInserted flag so InsertDataAsync (not idempotent) is not re-run on retry. On retry, clear ChangeTracker instead. - Remove ChangeTracker.Clear from SeedDataAsync since callers handle it. CosmosTestStore: - CleanAsync: track retry state, only clear ChangeTracker on retry. - DisposeAsync: only dispose context for non-canonical instances in _deferredStores (the canonical instance stays alive for ProcessExit). - Add CosmosBulkExecutionTest to deferred deletion list (shared by CosmosBulkWarningTest). Remove F1Test (not actually shared). RelationalDatabaseCreator.EnsureCreated/Async: - Track retry state, only clear ChangeTracker on retry. Migrator.MigrateImplementation/Async: - Use MigrationExecutionState.SeedingAttempted flag to only clear ChangeTracker on retry. CosmosExecutionStrategy: - Revert InternalServerError (500) from retryable errors — the 500s were caused by leaked CosmosClient instances from improper disposal. ReadItemPartitionKeyQueryInheritanceFixtureBase: - Fix DerivedSinglePartitionKeyEntity entity sorter to sort by (Id, PartitionKey) to avoid non-deterministic matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/Internal/CosmosDatabaseCreator.cs | 35 ++++++++++++------ .../Internal/CosmosExecutionStrategy.cs | 1 - .../Migrations/Internal/Migrator.cs | 14 ++++++- .../Migrations/MigrationExecutionState.cs | 5 +++ .../Storage/RelationalDatabaseCreator.cs | 37 ++++++++++++------- ...PartitionKeyQueryInheritanceFixtureBase.cs | 2 +- .../TestUtilities/CosmosTestStore.cs | 19 ++++++++-- 7 files changed, 79 insertions(+), 34 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index f37672889b9..7f2ba8e4dec 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -56,25 +56,37 @@ public CosmosDatabaseCreator( public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { var created = new StrongBox(false); + var dataInserted = new StrongBox(false); return _executionStrategy.ExecuteAsync( - (Creator: this, Created: created), static async (_, state, ct) => + (Creator: this, Created: created, DataInserted: dataInserted), static async (_, state, ct) => { var creator = state.Creator; - var model = creator._designTimeModel.Model; - state.Created.Value |= await creator._cosmosClient - .CreateDatabaseIfNotExistsAsync(model.GetThroughput(), ct) - .ConfigureAwait(false); - foreach (var container in GetContainersToCreate(model)) + if (state.DataInserted.Value) { + // On retry after InsertDataAsync succeeded, clear stale tracked entities + // from the previous seeding attempt but don't re-insert model data. + creator._currentContext.Context.ChangeTracker.Clear(); + } + else + { + var model = creator._designTimeModel.Model; state.Created.Value |= await creator._cosmosClient - .CreateContainerIfNotExistsAsync(container, ct) + .CreateDatabaseIfNotExistsAsync(model.GetThroughput(), ct) .ConfigureAwait(false); - } - if (state.Created.Value) - { - await creator.InsertDataAsync(ct).ConfigureAwait(false); + foreach (var container in GetContainersToCreate(model)) + { + state.Created.Value |= await creator._cosmosClient + .CreateContainerIfNotExistsAsync(container, ct) + .ConfigureAwait(false); + } + + if (state.Created.Value) + { + await creator.InsertDataAsync(ct).ConfigureAwait(false); + state.DataInserted.Value = true; + } } await creator.SeedDataAsync(state.Created.Value, ct).ConfigureAwait(false); @@ -212,7 +224,6 @@ public virtual async Task SeedDataAsync(bool created, CancellationToken cancella if (coreOptionsExtension?.AsyncSeeder is not null) { - _currentContext.Context.ChangeTracker.Clear(); await coreOptionsExtension.AsyncSeeder(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension?.Seeder is not null) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs index 5b2a8514914..ce17e5add19 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosExecutionStrategy.cs @@ -104,7 +104,6 @@ protected override bool ShouldRetryOn(Exception exception) static bool IsTransient(HttpStatusCode? statusCode) => statusCode is null - or HttpStatusCode.InternalServerError or HttpStatusCode.ServiceUnavailable or HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 92f03d00b5f..1ec0609d7b1 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -192,7 +192,12 @@ private bool MigrateImplementation( var seed = coreOptionsExtension.Seeder; if (seed != null) { - context.ChangeTracker.Clear(); + if (state.SeedingAttempted) + { + context.ChangeTracker.Clear(); + } + + state.SeedingAttempted = true; seed(context, state.AnyOperationPerformed); } else if (coreOptionsExtension.AsyncSeeder != null) @@ -329,7 +334,12 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync( var seedAsync = coreOptionsExtension.AsyncSeeder; if (seedAsync != null) { - context.ChangeTracker.Clear(); + if (state.SeedingAttempted) + { + context.ChangeTracker.Clear(); + } + + state.SeedingAttempted = true; await seedAsync(context, state.AnyOperationPerformed, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension.Seeder != null) diff --git a/src/EFCore.Relational/Migrations/MigrationExecutionState.cs b/src/EFCore.Relational/Migrations/MigrationExecutionState.cs index 2f3b3e469a4..ff3e82d15ba 100644 --- a/src/EFCore.Relational/Migrations/MigrationExecutionState.cs +++ b/src/EFCore.Relational/Migrations/MigrationExecutionState.cs @@ -32,4 +32,9 @@ public sealed class MigrationExecutionState /// The transaction that is in use. /// public IDbContextTransaction? Transaction { get; set; } + + /// + /// Indicates whether seeding has been attempted. + /// + public bool SeedingAttempted { get; set; } } diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index b5c7899cc9a..a441c2e5b27 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -1,6 +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.Runtime.CompilerServices; using System.Text; using System.Transactions; @@ -251,19 +252,23 @@ public virtual bool EnsureCreated() } return Dependencies.ExecutionStrategy.Execute( - (this, operationsPerformed), static (context, state) => + (Creator: this, Created: operationsPerformed, Retrying: new StrongBox(false)), + static (context, state) => { - var (creator, created) = state; - var coreOptionsExtension = - creator.Dependencies.ContextOptions.FindExtension(); + state.Creator.Dependencies.ContextOptions.FindExtension(); var seed = coreOptionsExtension?.Seeder; if (seed != null) { - context.ChangeTracker.Clear(); + if (state.Retrying.Value) + { + context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; using var transaction = context.Database.BeginTransaction(); - seed(context, created); + seed(context, state.Created); transaction.Commit(); } else if (coreOptionsExtension?.AsyncSeeder != null) @@ -271,7 +276,7 @@ public virtual bool EnsureCreated() throw new InvalidOperationException(CoreStrings.MissingSeeder); } - return created; + return state.Created; }, verifySucceeded: null); } @@ -306,20 +311,24 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio } return await Dependencies.ExecutionStrategy.ExecuteAsync( - (this, operationsPerformed), static async (context, state, ct) => + (Creator: this, Created: operationsPerformed, Retrying: new StrongBox(false)), + static async (context, state, ct) => { - var (creator, created) = state; - var coreOptionsExtension = - creator.Dependencies.ContextOptions.FindExtension(); + state.Creator.Dependencies.ContextOptions.FindExtension(); var seedAsync = coreOptionsExtension?.AsyncSeeder; if (seedAsync != null) { - context.ChangeTracker.Clear(); + if (state.Retrying.Value) + { + context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; var transaction = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false); await using var _ = transaction.ConfigureAwait(false); - await seedAsync(context, created, ct).ConfigureAwait(false); + await seedAsync(context, state.Created, ct).ConfigureAwait(false); await transaction.CommitAsync(ct).ConfigureAwait(false); } else if (coreOptionsExtension?.Seeder != null) @@ -327,7 +336,7 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio throw new InvalidOperationException(CoreStrings.MissingSeeder); } - return created; + return state.Created; }, verifySucceeded: null, cancellationToken).ConfigureAwait(false); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs index 648715675c6..714390b2764 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs @@ -68,7 +68,7 @@ public ReadItemPartitionKeyQueryInheritanceFixtureBase() = new Func(e => ((DerivedOnlyHierarchicalPartitionKeyEntity)e).DerivedPayload); sorters[typeof(DerivedSinglePartitionKeyEntity)] - = new Func(e => ((DerivedSinglePartitionKeyEntity)e).Id); + = new Func(e => (((DerivedSinglePartitionKeyEntity)e).Id, ((DerivedSinglePartitionKeyEntity)e).PartitionKey)); sorters[typeof(DerivedOnlySinglePartitionKeyEntity)] = new Func(e => ((DerivedOnlySinglePartitionKeyEntity)e).DerivedPayload); diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index ef86eaee6d7..8747f8ec939 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; +using System.Runtime.CompilerServices; using Azure; using Azure.Core; using Azure.ResourceManager; @@ -28,7 +29,7 @@ public class CosmosTestStore : TestStore // Databases shared across multiple test fixtures are deleted at process exit // to avoid one fixture's disposal racing with another fixture's queries. // Single-fixture databases are deleted immediately to avoid hitting emulator limits. - private static readonly HashSet _deferredDeletionStoreNames = ["Northwind", "F1Test"]; + private static readonly HashSet _deferredDeletionStoreNames = ["Northwind", "CosmosBulkExecutionTest"]; private static readonly ConcurrentDictionary _deferredStores = new(); static CosmosTestStore() @@ -286,8 +287,14 @@ private async Task EnsureDeletedAsync(DbContext context, CancellationToken public override Task CleanAsync(DbContext context, bool createTables = true) => new TestCosmosExecutionStrategy().ExecuteAsync( - (context, createTables), async (_, state, ct) => + (context, createTables, Retrying: new StrongBox(false)), async (_, state, ct) => { + if (state.Retrying.Value) + { + state.context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; await CleanAsyncImpl(state.context, state.createTables).ConfigureAwait(false); return true; }, null, default); @@ -521,9 +528,13 @@ public override async ValueTask DisposeAsync() return; } - if (_deferredStores.ContainsKey(Name)) + if (_deferredStores.TryGetValue(Name, out var canonical)) { - _storeContext.Dispose(); + if (!ReferenceEquals(this, canonical)) + { + _storeContext.Dispose(); + } + return; } From ea15b2e7892edf5b9ccfd6f436a4e35c616fed82 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Sun, 3 May 2026 09:41:36 -0700 Subject: [PATCH 09/16] Merge CosmosBulkWarningTest into CosmosBulkExecutionTest, simplify deferred deletion - Merge CosmosBulkWarningTest tests into CosmosBulkExecutionTest using the non-shared test pattern to eliminate the shared database. Delete the CosmosBulkWarningTest file. - Inline deferred deletion for the only shared database (Northwind). Add a debug assertion that throws when an unexpected database is shared across multiple fixture types. - Refactor SeedDataAsync to accept a clearChangeTracker parameter, eliminating the duplicate clear logic in EnsureCreatedAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/Internal/CosmosDatabaseCreator.cs | 19 +++--- .../TestUtilities/CosmosTestStore.cs | 14 +++-- .../Update/CosmosBulkExecutionTest.cs | 61 ++++++++++++++---- .../Update/CosmosBulkWarningTest.cs | 63 ------------------- 4 files changed, 70 insertions(+), 87 deletions(-) delete mode 100644 test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 7f2ba8e4dec..3014f96c6c8 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -62,13 +62,7 @@ public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken { var creator = state.Creator; - if (state.DataInserted.Value) - { - // On retry after InsertDataAsync succeeded, clear stale tracked entities - // from the previous seeding attempt but don't re-insert model data. - creator._currentContext.Context.ChangeTracker.Clear(); - } - else + if (!state.DataInserted.Value) { var model = creator._designTimeModel.Model; state.Created.Value |= await creator._cosmosClient @@ -89,7 +83,8 @@ public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken } } - await creator.SeedDataAsync(state.Created.Value, ct).ConfigureAwait(false); + await creator.SeedDataAsync(state.Created.Value, ct, clearChangeTracker: state.DataInserted.Value) + .ConfigureAwait(false); return state.Created.Value; }, verifySucceeded: null, cancellationToken); @@ -217,13 +212,19 @@ private IUpdateAdapter AddModelData() /// 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 async Task SeedDataAsync(bool created, CancellationToken cancellationToken = default) + public virtual async Task SeedDataAsync( + bool created, CancellationToken cancellationToken = default, bool clearChangeTracker = false) { var coreOptionsExtension = _contextOptions.FindExtension(); if (coreOptionsExtension?.AsyncSeeder is not null) { + if (clearChangeTracker) + { + _currentContext.Context.ChangeTracker.Clear(); + } + await coreOptionsExtension.AsyncSeeder(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension?.Seeder is not null) diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 8747f8ec939..eceee957f9e 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -26,10 +26,9 @@ public class CosmosTestStore : TestStore private static readonly Guid _runId = Guid.NewGuid(); private static bool? _connectionAvailable; - // Databases shared across multiple test fixtures are deleted at process exit + // The Northwind database is shared across multiple test fixtures and is deleted at process exit // to avoid one fixture's disposal racing with another fixture's queries. - // Single-fixture databases are deleted immediately to avoid hitting emulator limits. - private static readonly HashSet _deferredDeletionStoreNames = ["Northwind", "CosmosBulkExecutionTest"]; + private const string DeferredDeletionStoreName = "Northwind"; private static readonly ConcurrentDictionary _deferredStores = new(); static CosmosTestStore() @@ -90,10 +89,17 @@ private CosmosTestStore( _storeContext = new TestStoreContext(this); - if (shared && _deferredDeletionStoreNames.Contains(name)) + if (shared && name == DeferredDeletionStoreName) { _deferredStores.TryAdd(Name, this); } + else if (shared) + { + Check.DebugAssert( + !_deferredStores.ContainsKey(Name) && !_deferredStores.Values.Any(s => s.Name == Name), + $"Cosmos database '{name}' is shared across multiple fixture types. " + + "Add it to the deferred deletion list or give each fixture a unique StoreName."); + } } private static string CreateName(string name) diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs index 1ce5cc30df7..5b62952df09 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.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 Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + namespace Microsoft.EntityFrameworkCore.Update; public class CosmosBulkExecutionTest(NonSharedFixture fixture) : NonSharedModelTestBase(fixture), IClassFixture @@ -56,6 +59,53 @@ public async Task Trigger_Throws() Assert.Contains("Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", inner.Message); } + [ConditionalFact] + public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + + context.AddRange(Enumerable.Range(0, 100).Select(x => new Customer())); + await context.SaveChangesAsync(); + } + + [ConditionalFact] + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] + public virtual async Task AutoTransactionBehaviorWhenNeeded_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; + + context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); + } + + [ConditionalFact] + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] + public virtual async Task AutoTransactionBehaviorAlways_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; + + context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); + } + + private string BulkExecutionWithTransactionalBatchMessage => CoreStrings.WarningAsErrorTemplate( + CosmosEventId.BulkExecutionWithTransactionalBatch.ToString(), + CosmosResources.LogBulkExecutionWithTransactionalBatch(new TestLogger()).GenerateMessage(), + "CosmosEventId.BulkExecutionWithTransactionalBatch"); + public class CosmosBulkExecutionContext : DbContext { public CosmosBulkExecutionContext(DbContextOptions options) : base(options) @@ -76,15 +126,4 @@ public class Customer public string PartitionKey { get; set; } = "1"; } - - public class CosmosFixture : SharedStoreFixtureBase - { - protected override string StoreName - => nameof(CosmosBulkExecutionTest); - - protected override ITestStoreFactory TestStoreFactory - => CosmosTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).UseCosmos(x => x.BulkExecutionAllowed()).ConfigureWarnings(x => x.Ignore(CosmosEventId.BulkExecutionWithTransactionalBatch)); - } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs deleted file mode 100644 index 30729e11826..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs +++ /dev/null @@ -1,63 +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.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using static Microsoft.EntityFrameworkCore.Update.CosmosBulkExecutionTest; - -namespace Microsoft.EntityFrameworkCore.Update; - -public class CosmosBulkWarningTest(CosmosBulkWarningTest.ThrowingFixture fixture) : IClassFixture -{ - [ConditionalFact] - public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() - { - using var context = fixture.CreateContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - - context.AddRange(Enumerable.Range(0, 100).Select(x => new Customer())); - await context.SaveChangesAsync(); - } - - [ConditionalFact] - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] - public virtual async Task AutoTransactionBehaviorWhenNeeded_Throws() - { - using var context = fixture.CreateContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; - - context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); - } - - [ConditionalFact] - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] - public virtual async Task AutoTransactionBehaviorAlways_Throws() - { - using var context = fixture.CreateContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; - - context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); - } - - private string BulkExecutionWithTransactionalBatchMessage => CoreStrings.WarningAsErrorTemplate( - CosmosEventId.BulkExecutionWithTransactionalBatch.ToString(), - CosmosResources.LogBulkExecutionWithTransactionalBatch(new TestLogger()).GenerateMessage(), - "CosmosEventId.BulkExecutionWithTransactionalBatch"); - - public class ThrowingFixture : SharedStoreFixtureBase - { - protected override string StoreName - => nameof(CosmosBulkExecutionTest); - - protected override ITestStoreFactory TestStoreFactory - => CosmosTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).UseCosmos(x => x.BulkExecutionAllowed()); - } -} From da060dd1a66da1e50004912aa821886b725651ef Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Sun, 3 May 2026 22:31:27 -0700 Subject: [PATCH 10/16] Skip Project_inline_collection on Linux emulator The test uses assertOrder: true without an OrderBy, which produces non-deterministic results on the Linux Cosmos emulator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../EFCore.Relational.baseline.json | 106 +++++++++--------- .../PrimitiveCollectionsQueryCosmosTest.cs | 2 + 2 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index f776a93b79b..5639a9bdca0 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -7304,56 +7304,6 @@ } ] }, - { - "Type": "class Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath", - "Methods": [ - { - "Member": "StructuredJsonPath(System.Collections.Generic.IReadOnlyList segments, int[] indices);" - }, - { - "Member": "virtual System.Text.StringBuilder AppendTo(System.Text.StringBuilder builder);" - }, - { - "Member": "override string ToString();" - } - ], - "Properties": [ - { - "Member": "virtual bool IsRoot { get; }" - }, - { - "Member": "virtual int[] Indices { get; }" - }, - { - "Member": "static Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath Root { get; }" - }, - { - "Member": "virtual System.Collections.Generic.IReadOnlyList Segments { get; }" - } - ] - }, - { - "Type": "sealed class Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment", - "Methods": [ - { - "Member": "StructuredJsonPathSegment(string propertyName);" - }, - { - "Member": "override string ToString();" - } - ], - "Properties": [ - { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment Array { get; }" - }, - { - "Member": "bool IsArray { get; }" - }, - { - "Member": "string? PropertyName { get; }" - } - ] - }, { "Type": "class Microsoft.EntityFrameworkCore.Query.JsonQueryExpression : System.Linq.Expressions.Expression, Microsoft.EntityFrameworkCore.Query.IPrintableExpression", "Methods": [ @@ -7970,6 +7920,9 @@ { "Member": "int LastCommittedCommandIndex { get; set; }" }, + { + "Member": "bool SeedingAttempted { get; set; }" + }, { "Member": "Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction? Transaction { get; set; }" } @@ -16941,7 +16894,8 @@ ], "Properties": [ { - "Member": "static string BadSequenceString { get; }" + "Member": "static string BadSequenceString { get; }", + "Stage": "Obsolete" }, { "Member": "static string BadSequenceType { get; }" @@ -19928,6 +19882,56 @@ } ] }, + { + "Type": "class Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath", + "Methods": [ + { + "Member": "StructuredJsonPath(System.Collections.Generic.IReadOnlyList segments, int[] indices);" + }, + { + "Member": "virtual System.Text.StringBuilder AppendTo(System.Text.StringBuilder builder);" + }, + { + "Member": "override string ToString();" + } + ], + "Properties": [ + { + "Member": "virtual int[] Indices { get; }" + }, + { + "Member": "virtual bool IsRoot { get; }" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath Root { get; }" + }, + { + "Member": "virtual System.Collections.Generic.IReadOnlyList Segments { get; }" + } + ] + }, + { + "Type": "sealed class Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment", + "Methods": [ + { + "Member": "StructuredJsonPathSegment(string propertyName);" + }, + { + "Member": "override string ToString();" + } + ], + "Properties": [ + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment Array { get; }" + }, + { + "Member": "bool IsArray { get; }" + }, + { + "Member": "string? PropertyName { get; }" + } + ] + }, { "Type": "class Microsoft.EntityFrameworkCore.Metadata.Builders.TableBuilder : Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure", "Methods": [ diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 614a138ed0c..34e99cf208b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -2215,6 +2215,8 @@ ORDER BY c["Id"] """); } + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override async Task Project_inline_collection() { await base.Project_inline_collection(); From dac7720ccfaf14c87868acaec2664cf6b2294edc Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 4 May 2026 09:59:00 -0700 Subject: [PATCH 11/16] Convert CosmosBulkExecutionTest to use shared fixture Replace NonSharedModelTestBase with a shared BulkFixture to avoid per-test InitializeNonSharedTest overhead. Tests that required custom model or provider configuration (SessionEnabled_Throws, Trigger_Throws) were removed along with the warning-as-error tests since the default WarningAsError behavior is already exercised by CosmosBulkConcurrencyTest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Update/CosmosBulkExecutionTest.cs | 117 +++++------------- 1 file changed, 33 insertions(+), 84 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs index 5b62952df09..86a312d166e 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs @@ -2,116 +2,45 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Internal; namespace Microsoft.EntityFrameworkCore.Update; -public class CosmosBulkExecutionTest(NonSharedFixture fixture) : NonSharedModelTestBase(fixture), IClassFixture +public class CosmosBulkExecutionTest(CosmosBulkExecutionTest.BulkFixture fixture) + : IClassFixture { - protected override string NonSharedStoreName => nameof(CosmosBulkExecutionTest); - - protected override ITestStoreFactory NonSharedTestStoreFactory => CosmosTestStoreFactory.Instance; - [ConditionalFact] // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public virtual async Task DoesNotBatchSingleBatchableWrite() { - var contextFactory = await InitializeNonSharedTest(onConfiguring: (cfg) => cfg.UseCosmos(c => c.BulkExecutionAllowed()).ConfigureWarnings(x => x.Ignore(CosmosEventId.BulkExecutionWithTransactionalBatch))); - using var context = contextFactory.CreateDbContext(); + using var context = fixture.CreateContext(); - context.Add(new Customer() { PartitionKey = "4" }); + context.Add(new Customer { PartitionKey = "4" }); context.AddRange(Enumerable.Range(0, 3).Select(x => new Customer())); - context.AddRange(Enumerable.Range(0, 3).Select(x => new Customer() { PartitionKey = "2"})); - context.Add(new Customer() { PartitionKey = "3" }); + context.AddRange(Enumerable.Range(0, 3).Select(x => new Customer { PartitionKey = "2" })); + context.Add(new Customer { PartitionKey = "3" }); - ListLoggerFactory.Log.Clear(); + fixture.ListLoggerFactory.Clear(); await context.SaveChangesAsync(); - Assert.Equal(CosmosEventId.ExecutedCreateItem, ListLoggerFactory.Log[0].Id); - Assert.Equal(CosmosEventId.ExecutedCreateItem, ListLoggerFactory.Log[1].Id); - Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, ListLoggerFactory.Log[2].Id); - Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, ListLoggerFactory.Log[3].Id); - } - - [ConditionalFact] - public async Task SessionEnabled_Throws() - { - var contextFactory = await InitializeNonSharedTest(onConfiguring: (cfg) => cfg.UseCosmos(c => c.BulkExecutionAllowed().SessionTokenManagementMode(Cosmos.Infrastructure.SessionTokenManagementMode.SemiAutomatic))); - using var context = contextFactory.CreateDbContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - context.Database.UseSessionToken("0:-1#1"); - context.Add(new Customer()); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - var inner = Assert.IsType(ex.InnerException); - Assert.Contains("Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", inner.Message); - } - - [ConditionalFact] - public async Task Trigger_Throws() - { - var contextFactory = await InitializeNonSharedTest(onModelCreating: (b) => b.Entity().HasTrigger(NonSharedStoreName, Azure.Cosmos.Scripts.TriggerType.Post, Azure.Cosmos.Scripts.TriggerOperation.Create), onConfiguring: (cfg) => cfg.UseCosmos(c => c.BulkExecutionAllowed())); - using var context = contextFactory.CreateDbContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - context.Add(new Customer()); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - var inner = Assert.IsType(ex.InnerException); - Assert.Contains("Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", inner.Message); + Assert.Equal(CosmosEventId.ExecutedCreateItem, fixture.ListLoggerFactory.Log[0].Id); + Assert.Equal(CosmosEventId.ExecutedCreateItem, fixture.ListLoggerFactory.Log[1].Id); + Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, fixture.ListLoggerFactory.Log[2].Id); + Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, fixture.ListLoggerFactory.Log[3].Id); } [ConditionalFact] public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); - using var context = contextFactory.CreateDbContext(); + using var context = fixture.CreateContext(); context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context.AddRange(Enumerable.Range(0, 100).Select(x => new Customer())); await context.SaveChangesAsync(); } - [ConditionalFact] - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] - public virtual async Task AutoTransactionBehaviorWhenNeeded_Throws() + public class CosmosBulkExecutionContext(DbContextOptions options) : DbContext(options) { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); - using var context = contextFactory.CreateDbContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; - - context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); - } - - [ConditionalFact] - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] - public virtual async Task AutoTransactionBehaviorAlways_Throws() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); - using var context = contextFactory.CreateDbContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; - - context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); - } - - private string BulkExecutionWithTransactionalBatchMessage => CoreStrings.WarningAsErrorTemplate( - CosmosEventId.BulkExecutionWithTransactionalBatch.ToString(), - CosmosResources.LogBulkExecutionWithTransactionalBatch(new TestLogger()).GenerateMessage(), - "CosmosEventId.BulkExecutionWithTransactionalBatch"); - - public class CosmosBulkExecutionContext : DbContext - { - public CosmosBulkExecutionContext(DbContextOptions options) : base(options) - { - } - public DbSet Customers { get; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -126,4 +55,24 @@ public class Customer public string PartitionKey { get; set; } = "1"; } + + public class BulkFixture : SharedStoreFixtureBase + { + protected override string StoreName + => nameof(CosmosBulkExecutionTest); + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + protected override bool UsePooling + => false; + + protected override bool ShouldLogCategory(string logCategory) + => logCategory == DbLoggerCategory.Database.Command.Name; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder) + .UseCosmos(x => x.BulkExecutionAllowed()) + .ConfigureWarnings(x => x.Ignore(CosmosEventId.BulkExecutionWithTransactionalBatch)); + } } From 7d8399c9b27043de43149716267a4a3974de15fc Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 4 May 2026 11:13:15 -0700 Subject: [PATCH 12/16] Restore non-shared tests, add ProcessExit timeout, fix SeedDataAsync - Restore SessionEnabled_Throws, Trigger_Throws, and the AutoTransactionBehavior warning tests in CosmosBulkExecutionTest using InitializeNonSharedTest. Shared-fixture tests use fixture.CreateContext() for fast execution. - Add 30s timeout with CancellationToken to the ProcessExit handler to prevent indefinite hangs if Cosmos deletion stalls. - Reorder SeedDataAsync parameters: move CancellationToken to the end. Call with clearChangeTracker: state.DataInserted.Value || !state.Created.Value so the tracker is also cleared when retrying without creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/Internal/CosmosDatabaseCreator.cs | 7 +- .../TestUtilities/CosmosTestStore.cs | 37 +++++---- .../Update/CosmosBulkExecutionTest.cs | 79 ++++++++++++++++++- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 3014f96c6c8..04174713b75 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -83,7 +83,10 @@ public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken } } - await creator.SeedDataAsync(state.Created.Value, ct, clearChangeTracker: state.DataInserted.Value) + await creator.SeedDataAsync( + state.Created.Value, + clearChangeTracker: state.DataInserted.Value || !state.Created.Value, + ct) .ConfigureAwait(false); return state.Created.Value; @@ -213,7 +216,7 @@ private IUpdateAdapter AddModelData() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual async Task SeedDataAsync( - bool created, CancellationToken cancellationToken = default, bool clearChangeTracker = false) + bool created, bool clearChangeTracker = false, CancellationToken cancellationToken = default) { var coreOptionsExtension = _contextOptions.FindExtension(); diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index eceee957f9e..236193849b7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -35,22 +35,29 @@ static CosmosTestStore() { AppDomain.CurrentDomain.ProcessExit += static (_, _) => { - Task.WhenAll(_deferredStores.Select( - async entry => - { - var store = entry.Value; - try - { - store.GetTestStoreIndex(store.ServiceProvider) - .RemoveShared(store.GetType().Name + store.Name); - await store.EnsureDeletedAsync(store._storeContext).ConfigureAwait(false); - } - catch + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + try + { + Task.WhenAll(_deferredStores.Select( + async entry => { - } - - store._storeContext.Dispose(); - })).GetAwaiter().GetResult(); + var store = entry.Value; + try + { + store.GetTestStoreIndex(store.ServiceProvider) + .RemoveShared(store.GetType().Name + store.Name); + await store.EnsureDeletedAsync(store._storeContext, cts.Token).ConfigureAwait(false); + } + catch + { + } + + store._storeContext.Dispose(); + })).GetAwaiter().GetResult(); + } + catch + { + } }; } diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs index 86a312d166e..736050e4726 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs @@ -2,12 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; namespace Microsoft.EntityFrameworkCore.Update; -public class CosmosBulkExecutionTest(CosmosBulkExecutionTest.BulkFixture fixture) - : IClassFixture +public class CosmosBulkExecutionTest(NonSharedFixture nonSharedFixture, CosmosBulkExecutionTest.BulkFixture fixture) + : NonSharedModelTestBase(nonSharedFixture), IClassFixture, IClassFixture { + protected override string NonSharedStoreName => nameof(CosmosBulkExecutionTest); + + protected override ITestStoreFactory NonSharedTestStoreFactory => CosmosTestStoreFactory.Instance; + [ConditionalFact] // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] @@ -39,6 +44,76 @@ public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() await context.SaveChangesAsync(); } + [ConditionalFact] + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] + public virtual async Task AutoTransactionBehaviorWhenNeeded_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; + + context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); + } + + [ConditionalFact] + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] + public virtual async Task AutoTransactionBehaviorAlways_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; + + context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); + } + + [ConditionalFact] + public async Task SessionEnabled_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos( + c => c.BulkExecutionAllowed() + .SessionTokenManagementMode(Cosmos.Infrastructure.SessionTokenManagementMode.SemiAutomatic))); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + context.Database.UseSessionToken("0:-1#1"); + context.Add(new Customer()); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + var inner = Assert.IsType(ex.InnerException); + Assert.Contains( + "Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", + inner.Message); + } + + [ConditionalFact] + public async Task Trigger_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: b => b.Entity().HasTrigger( + NonSharedStoreName, Azure.Cosmos.Scripts.TriggerType.Post, Azure.Cosmos.Scripts.TriggerOperation.Create), + onConfiguring: cfg => cfg.UseCosmos(c => c.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + context.Add(new Customer()); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + var inner = Assert.IsType(ex.InnerException); + Assert.Contains( + "Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", + inner.Message); + } + + private string BulkExecutionWithTransactionalBatchMessage => CoreStrings.WarningAsErrorTemplate( + CosmosEventId.BulkExecutionWithTransactionalBatch.ToString(), + CosmosResources.LogBulkExecutionWithTransactionalBatch(new TestLogger()).GenerateMessage(), + "CosmosEventId.BulkExecutionWithTransactionalBatch"); + public class CosmosBulkExecutionContext(DbContextOptions options) : DbContext(options) { public DbSet Customers { get; } = null!; From 6867ca7362111faf3795926a52f6029fca8651c7 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 4 May 2026 13:03:38 -0700 Subject: [PATCH 13/16] Fix non-deterministic ordering in EmbeddedDocumentsTest Can_manipulate_embedded_collections queries Person entities without OrderBy then accesses them by index (people[0], people[1], people[2]). Cosmos returns documents in non-deterministic order, causing sporadic assertion failures. Add OrderBy(o => o.Id) to the two queries that were missing it (the final AssertState helper already had it). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs index 82032072fb4..3bd6976bbd7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs @@ -163,7 +163,7 @@ await context.AddAsync( new Person { Id = 3, Addresses = new List
{ existingAddress1Person3, existingAddress2Person3 } }); await context.SaveChangesAsync(); - var people = await context.Set().ToListAsync(); + var people = await context.Set().OrderBy(o => o.Id).ToListAsync(); Assert.Empty(people[0].Addresses); @@ -190,7 +190,7 @@ await context.AddAsync( using (var context = new EmbeddedTransportationContext(options)) { - var people = await context.Set().ToListAsync(); + var people = await context.Set().OrderBy(o => o.Id).ToListAsync(); addedAddress1 = new Address { Street = "First", From fb978e3e2b4567ad0094201a4fab3761f4909a45 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 4 May 2026 14:33:27 -0700 Subject: [PATCH 14/16] Add retry flag, warn on EnsureCreated with tracked entities CosmosDatabaseCreator: - Add separate Retrying flag to track whether the execution strategy is retrying. Clear ChangeTracker only on retry. - Remove clearChangeTracker parameter from SeedDataAsync since retry clearing is now handled by the caller. - Emit EnsureCreatedWithTrackedEntitiesWarning (throws by default) when EnsureCreatedAsync is called on a context with pending changes. - Add IDiagnosticsLogger dependency. RelationalDatabaseCreator: - Move Clear outside the seeder null-check so it runs on every retry. - Emit the same warning at the start of EnsureCreated/Async. - Add IDiagnosticsLogger to dependencies. CosmosTestStore: - Clear ChangeTracker at the start of CleanAsync so the warning doesn't fire when the test infrastructure calls EnsureCreated. New CoreEventId.EnsureCreatedWithTrackedEntitiesWarning with full logging infrastructure (resx, Designer.cs, LoggingDefinitions, CoreLoggerExtensions). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Storage/Internal/CosmosDatabaseCreator.cs | 34 +++++++++++------ .../EFCore.Relational.baseline.json | 3 ++ .../Storage/RelationalDatabaseCreator.cs | 38 +++++++++++++------ .../RelationalDatabaseCreatorDependencies.cs | 9 ++++- src/EFCore/Diagnostics/CoreEventId.cs | 15 ++++++++ .../Diagnostics/CoreLoggerExtensions.cs | 24 ++++++++++++ src/EFCore/Diagnostics/LoggingDefinitions.cs | 9 +++++ src/EFCore/EFCore.baseline.json | 16 +++++--- src/EFCore/Properties/CoreStrings.Designer.cs | 25 ++++++++++++ src/EFCore/Properties/CoreStrings.resx | 4 ++ .../TestUtilities/CosmosTestStore.cs | 5 ++- 11 files changed, 151 insertions(+), 31 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 04174713b75..00c282e6907 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -22,6 +22,7 @@ public class CosmosDatabaseCreator : IDatabaseCreator private readonly ICurrentDbContext _currentContext; private readonly IDbContextOptions _contextOptions; private readonly IExecutionStrategy _executionStrategy; + private readonly IDiagnosticsLogger _logger; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -36,7 +37,8 @@ public CosmosDatabaseCreator( IDatabase database, ICurrentDbContext currentContext, IDbContextOptions contextOptions, - IExecutionStrategy executionStrategy) + IExecutionStrategy executionStrategy, + IDiagnosticsLogger logger) { _cosmosClient = cosmosClient; _designTimeModel = designTimeModel; @@ -45,6 +47,7 @@ public CosmosDatabaseCreator( _currentContext = currentContext; _contextOptions = contextOptions; _executionStrategy = executionStrategy; + _logger = logger; } /// @@ -55,13 +58,28 @@ public CosmosDatabaseCreator( /// public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { + if (_currentContext.Context.ChangeTracker.Entries().Any( + e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) + { + _logger.EnsureCreatedWithTrackedEntitiesWarning(); + } + var created = new StrongBox(false); var dataInserted = new StrongBox(false); + var retrying = new StrongBox(false); return _executionStrategy.ExecuteAsync( - (Creator: this, Created: created, DataInserted: dataInserted), static async (_, state, ct) => + (Creator: this, Created: created, DataInserted: dataInserted, Retrying: retrying), + static async (_, state, ct) => { var creator = state.Creator; + if (state.Retrying.Value) + { + creator._currentContext.Context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; + if (!state.DataInserted.Value) { var model = creator._designTimeModel.Model; @@ -83,10 +101,7 @@ public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken } } - await creator.SeedDataAsync( - state.Created.Value, - clearChangeTracker: state.DataInserted.Value || !state.Created.Value, - ct) + await creator.SeedDataAsync(state.Created.Value, cancellationToken: ct) .ConfigureAwait(false); return state.Created.Value; @@ -216,18 +231,13 @@ private IUpdateAdapter AddModelData() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual async Task SeedDataAsync( - bool created, bool clearChangeTracker = false, CancellationToken cancellationToken = default) + bool created, CancellationToken cancellationToken = default) { var coreOptionsExtension = _contextOptions.FindExtension(); if (coreOptionsExtension?.AsyncSeeder is not null) { - if (clearChangeTracker) - { - _currentContext.Context.ChangeTracker.Clear(); - } - await coreOptionsExtension.AsyncSeeder(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension?.Seeder is not null) diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index 5639a9bdca0..9e5f52bc2f0 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -11280,6 +11280,9 @@ { "Member": "Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy ExecutionStrategy { get; }" }, + { + "Member": "Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger Logger { get; init; }" + }, { "Member": "Microsoft.EntityFrameworkCore.Migrations.IMigrationCommandExecutor MigrationCommandExecutor { get; init; }" }, diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index a441c2e5b27..3fbad717c5d 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -238,6 +238,12 @@ public virtual bool EnsureCreated() using var transactionScope = new TransactionScope( TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + if (Dependencies.CurrentContext.Context.ChangeTracker.Entries().Any( + e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) + { + Dependencies.Logger.EnsureCreatedWithTrackedEntitiesWarning(); + } + var operationsPerformed = false; if (!Exists()) { @@ -255,18 +261,19 @@ public virtual bool EnsureCreated() (Creator: this, Created: operationsPerformed, Retrying: new StrongBox(false)), static (context, state) => { + if (state.Retrying.Value) + { + context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; + var coreOptionsExtension = state.Creator.Dependencies.ContextOptions.FindExtension(); var seed = coreOptionsExtension?.Seeder; if (seed != null) { - if (state.Retrying.Value) - { - context.ChangeTracker.Clear(); - } - - state.Retrying.Value = true; using var transaction = context.Database.BeginTransaction(); seed(context, state.Created); transaction.Commit(); @@ -295,6 +302,12 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio { using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + if (Dependencies.CurrentContext.Context.ChangeTracker.Entries().Any( + e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) + { + Dependencies.Logger.EnsureCreatedWithTrackedEntitiesWarning(); + } + var operationsPerformed = false; if (!await ExistsAsync(cancellationToken).ConfigureAwait(false)) { @@ -314,18 +327,19 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio (Creator: this, Created: operationsPerformed, Retrying: new StrongBox(false)), static async (context, state, ct) => { + if (state.Retrying.Value) + { + context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; + var coreOptionsExtension = state.Creator.Dependencies.ContextOptions.FindExtension(); var seedAsync = coreOptionsExtension?.AsyncSeeder; if (seedAsync != null) { - if (state.Retrying.Value) - { - context.ChangeTracker.Clear(); - } - - state.Retrying.Value = true; var transaction = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false); await using var _ = transaction.ConfigureAwait(false); await seedAsync(context, state.Created, ct).ConfigureAwait(false); diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs index 0ada3276fec..d2d00a7af1e 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs @@ -55,7 +55,8 @@ public RelationalDatabaseCreatorDependencies( ICurrentDbContext currentContext, IDbContextOptions contextOptions, IRelationalCommandDiagnosticsLogger commandLogger, - IExceptionDetector exceptionDetector) + IExceptionDetector exceptionDetector, + IDiagnosticsLogger logger) { Connection = connection; ModelDiffer = modelDiffer; @@ -67,6 +68,7 @@ public RelationalDatabaseCreatorDependencies( ContextOptions = contextOptions; CommandLogger = commandLogger; ExceptionDetector = exceptionDetector; + Logger = logger; } /// @@ -118,4 +120,9 @@ public RelationalDatabaseCreatorDependencies( /// Gets the exception detector. /// public IExceptionDetector ExceptionDetector { get; init; } + + /// + /// Gets the logger. + /// + public IDiagnosticsLogger Logger { get; init; } } diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index 62a2cad329b..e68acbf39c9 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -91,6 +91,7 @@ private enum Id RedundantAddServicesCallWarning = CoreBaseId + 410, OldModelVersionWarning = CoreBaseId + 411, CompiledModelProviderMismatchWarning = CoreBaseId + 412, + EnsureCreatedWithTrackedEntitiesWarning = CoreBaseId + 413, // Model and ModelValidation events ShadowPropertyCreated = CoreBaseId + 600, @@ -505,6 +506,20 @@ private static EventId MakeInfraId(Id id) /// public static readonly EventId CompiledModelProviderMismatchWarning = MakeInfraId(Id.CompiledModelProviderMismatchWarning); + /// + /// + /// was called on a context that is already tracking + /// added, modified, or deleted entities. These changes would be lost if a retry occurs due to a transient failure. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId EnsureCreatedWithTrackedEntitiesWarning = MakeInfraId(Id.EnsureCreatedWithTrackedEntitiesWarning); + private static readonly string _modelPrefix = DbLoggerCategory.Model.Name + "."; private static EventId MakeModelId(Id id) diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index 171ff92845c..3b76077e276 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -270,6 +270,30 @@ private static string CompiledModelProviderMismatch(EventDefinitionBase definiti return d.GenerateMessage(p.MismatchedProviderName, p.CurrentProviderName); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + public static void EnsureCreatedWithTrackedEntitiesWarning( + this IDiagnosticsLogger diagnostics) + { + var definition = CoreResources.LogEnsureCreatedWithTrackedEntities(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new EventData( + definition, + (d, _) => ((EventDefinition)d).GenerateMessage()); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + /// /// Logs for the event. /// diff --git a/src/EFCore/Diagnostics/LoggingDefinitions.cs b/src/EFCore/Diagnostics/LoggingDefinitions.cs index 30b462e0aae..55133e004e3 100644 --- a/src/EFCore/Diagnostics/LoggingDefinitions.cs +++ b/src/EFCore/Diagnostics/LoggingDefinitions.cs @@ -115,6 +115,15 @@ public abstract class LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogCompiledModelProviderMismatch; + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase? LogEnsureCreatedWithTrackedEntities; + /// /// 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/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 8ae23aac4cd..b52c3b307fa 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -2930,6 +2930,9 @@ { "Member": "static readonly Microsoft.Extensions.Logging.EventId DuplicateDependentEntityTypeInstanceWarning" }, + { + "Member": "static readonly Microsoft.Extensions.Logging.EventId EnsureCreatedWithTrackedEntitiesWarning" + }, { "Member": "static readonly Microsoft.Extensions.Logging.EventId ExecutionStrategyRetrying" }, @@ -3178,6 +3181,9 @@ { "Member": "static void DuplicateDependentEntityTypeInstanceWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.Metadata.IEntityType dependent1, Microsoft.EntityFrameworkCore.Metadata.IEntityType dependent2);" }, + { + "Member": "static void EnsureCreatedWithTrackedEntitiesWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics);" + }, { "Member": "static void ExecutionStrategyRetrying(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, System.Collections.Generic.IReadOnlyList exceptionsEncountered, System.TimeSpan delay, bool async);" }, @@ -4151,9 +4157,6 @@ { "Member": "static string InvalidIncludeExpression(object? expression);" }, - { - "Member": "static string InvalidStructuredJsonPathIndexCount(object? indicesCount, object? arraySegmentCount);" - }, { "Member": "static string InvalidKeyValue(object? entityType, object? keyProperty);" }, @@ -4196,6 +4199,9 @@ { "Member": "static string InvalidSetTypeOwned(object? typeName, object? ownerType);" }, + { + "Member": "static string InvalidStructuredJsonPathIndexCount(object? indicesCount, object? arraySegmentCount);" + }, { "Member": "static string InvalidSwitch(object? name, object? value);" }, @@ -20777,10 +20783,10 @@ ], "Properties": [ { - "Member": "virtual string MismatchedProviderName { get; }" + "Member": "virtual string CurrentProviderName { get; }" }, { - "Member": "virtual string CurrentProviderName { get; }" + "Member": "virtual string MismatchedProviderName { get; }" } ] }, diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 5ce3819559d..bf9d212a078 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -3894,6 +3894,31 @@ public static EventDefinition LogCompiledModelProviderMismatch(I return (EventDefinition)definition; } + /// + /// 'EnsureCreated' was called on a context that is already tracking added, modified, or deleted entities. These tracked changes would be lost if a retry occurs due to a transient failure. + /// + public static EventDefinition LogEnsureCreatedWithTrackedEntities(IDiagnosticsLogger logger) + { + var definition = ((LoggingDefinitions)logger.Definitions).LogEnsureCreatedWithTrackedEntities; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((LoggingDefinitions)logger.Definitions).LogEnsureCreatedWithTrackedEntities, + logger, + static logger => new EventDefinition( + logger.Options, + CoreEventId.EnsureCreatedWithTrackedEntitiesWarning, + LogLevel.Warning, + "CoreEventId.EnsureCreatedWithTrackedEntitiesWarning", + level => LoggerMessage.Define( + level, + CoreEventId.EnsureCreatedWithTrackedEntitiesWarning, + _resourceManager.GetString("LogEnsureCreatedWithTrackedEntities")!))); + } + + return (EventDefinition)definition; + } + /// /// The unchanged property '{typePath}.{property}' was detected as changed and will be marked as modified. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see property values. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index ab03f9b8cab..673e2719b39 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -948,6 +948,10 @@ A compiled model was found but it was built for the database provider '{compiledProviderName}'. The current context is using the database provider '{currentProviderName}'. The compiled model was ignored. Regenerate the compiled model with the correct provider. Warning CoreEventId.CompiledModelProviderMismatchWarning string string + + 'EnsureCreated' was called on a context that is already tracking added, modified, or deleted entities. These tracked changes would be lost if a retry occurs due to a transient failure. Call 'EnsureCreated' before making changes to the context, or disable this warning by using 'ConfigureWarnings(w => w.Ignore(CoreEventId.EnsureCreatedWithTrackedEntitiesWarning))'. + Warning CoreEventId.EnsureCreatedWithTrackedEntitiesWarning + The unchanged property '{typePath}.{property}' was detected as changed and will be marked as modified. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see property values. Debug CoreEventId.ComplexElementPropertyChangeDetected string string diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 236193849b7..2f6502998df 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -299,7 +299,9 @@ private async Task EnsureDeletedAsync(DbContext context, CancellationToken } public override Task CleanAsync(DbContext context, bool createTables = true) - => new TestCosmosExecutionStrategy().ExecuteAsync( + { + context.ChangeTracker.Clear(); + return new TestCosmosExecutionStrategy().ExecuteAsync( (context, createTables, Retrying: new StrongBox(false)), async (_, state, ct) => { if (state.Retrying.Value) @@ -311,6 +313,7 @@ public override Task CleanAsync(DbContext context, bool createTables = true) await CleanAsyncImpl(state.context, state.createTables).ConfigureAwait(false); return true; }, null, default); + } private async Task CleanAsyncImpl(DbContext context, bool createTables) { From b6d748344b611b76f7d0bb938eba6fbb50b0c4e7 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 4 May 2026 15:38:09 -0700 Subject: [PATCH 15/16] Skip flaky Northwind query tests on Linux Cosmos emulator These tests project entities without OrderBy or project into new instances where the default entity sorter key (OrderID/ProductID) is always 0, causing non-deterministic element pairing on the Linux Cosmos emulator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Query/NorthwindMiscellaneousQueryCosmosTest.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index f3e2da296dd..c8a0bf29a30 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -2211,6 +2211,8 @@ FROM root c """); }); + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override Task Select_expression_date_add_milliseconds_below_the_range(bool async) => Fixture.NoSyncTest( async, async a => @@ -3688,6 +3690,8 @@ public override async Task Anonymous_projection_skip_take_empty_collection_First AssertSql(); } + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override Task Checked_context_with_arithmetic_does_not_fail(bool async) => Fixture.NoSyncTest( async, async a => @@ -4359,6 +4363,8 @@ FROM root c } } + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override Task Convert_to_nullable_on_nullable_value_is_ignored(bool async) => Fixture.NoSyncTest( async, async a => From 09a4dba8d1db0608068fab422160e7349ee71576 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 4 May 2026 18:22:43 -0700 Subject: [PATCH 16/16] Fix ordering in flaky tests, use non-shared fixture for warning test NorthwindMiscellaneousQueryCosmosTest: - Select_expression_date_add_milliseconds_below_the_range and Convert_to_nullable_on_nullable_value_is_ignored: override with elementSorter (e => e.OrderDate) since projecting new Order loses the OrderID used by the default sorter. - Checked_context_with_arithmetic_does_not_fail: use composite elementSorter (OrderID, ProductID) and identity Select to avoid non-deterministic pairing when multiple OrderDetails share OrderID. PrimitiveCollectionsQueryCosmosTest: - Project_inline_collection: override with elementSorter (e => e[0]) since the base test has assertOrder: true without OrderBy. CosmosBulkExecutionTest: - AutoTransactionBehaviorNever_DoesNotThrow: use non-shared fixture so the warning is not suppressed by the shared fixture's Ignore config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NorthwindMiscellaneousQueryCosmosTest.cs | 29 +++++++++++++------ .../PrimitiveCollectionsQueryCosmosTest.cs | 7 +++-- .../Update/CosmosBulkExecutionTest.cs | 4 ++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index c8a0bf29a30..fda17714951 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -2211,13 +2211,15 @@ FROM root c """); }); - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override Task Select_expression_date_add_milliseconds_below_the_range(bool async) => Fixture.NoSyncTest( async, async a => { - await base.Select_expression_date_add_milliseconds_below_the_range(a); + await AssertQuery( + a, + ss => ss.Set().Where(o => o.OrderDate != null) + .Select(o => new Order { OrderDate = o.OrderDate.Value.AddMilliseconds(-1000000000000) }), + elementSorter: e => e.OrderDate); AssertSql( """ @@ -3690,13 +3692,21 @@ public override async Task Anonymous_projection_skip_take_empty_collection_First AssertSql(); } - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override Task Checked_context_with_arithmetic_does_not_fail(bool async) => Fixture.NoSyncTest( async, async a => { - await base.Checked_context_with_arithmetic_does_not_fail(async); + checked + { + await AssertQuery( + a, + ss => ss.Set() + .Where(w => w.Quantity + 1 == 5 && w.Quantity - 1 == 3 && w.Quantity * 1 == w.Quantity) + .OrderBy(o => o.OrderID) + .Select(o => o), + elementSorter: e => (e.OrderID, e.ProductID), + elementAsserter: (e, a2) => { AssertEqual(e, a2); }); + } AssertSql( """ @@ -4363,13 +4373,14 @@ FROM root c } } - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override Task Convert_to_nullable_on_nullable_value_is_ignored(bool async) => Fixture.NoSyncTest( async, async a => { - await base.Convert_to_nullable_on_nullable_value_is_ignored(a); + await AssertQuery( + a, + ss => ss.Set().Select(o => new Order { OrderDate = o.OrderDate.Value }), + elementSorter: e => e.OrderDate); AssertSql( """ diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 34e99cf208b..f08d13fc970 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -2215,11 +2215,12 @@ ORDER BY c["Id"] """); } - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Non-deterministic ordering) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public override async Task Project_inline_collection() { - await base.Project_inline_collection(); + await AssertQuery( + ss => ss.Set().Select(x => new[] { x.String, "foo" }), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true), + elementSorter: e => e[0]); // The following should be SELECT VALUE [c["String"], "foo"], #33779 AssertSql( diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs index 736050e4726..94b9b15708d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs @@ -37,7 +37,9 @@ public virtual async Task DoesNotBatchSingleBatchableWrite() [ConditionalFact] public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() { - using var context = fixture.CreateContext(); + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context.AddRange(Enumerable.Range(0, 100).Select(x => new Customer()));