diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs index 25252236e4c..f4d8c9c1b09 100644 --- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs @@ -58,7 +58,7 @@ public async Task Should_not_throw_if_specified_region_is_right() var customer = new Customer { Id = 42, Name = "Theon" }; using var context = new CustomerContext(options); - await context.Database.EnsureCreatedAsync(); + await CosmosTestStore.DatabaseEnsureCreated(context); await context.AddAsync(customer); @@ -76,7 +76,7 @@ public async Task Should_throw_if_specified_region_is_wrong() var customer = new Customer { Id = 42, Name = "Theon" }; using var context = new CustomerContext(options); - await context.Database.EnsureCreatedAsync(); + await CosmosTestStore.DatabaseEnsureCreated(context); await context.AddAsync(customer); @@ -100,7 +100,7 @@ public async Task Should_not_throw_if_specified_connection_mode_is_right() var customer = new Customer { Id = 42, Name = "Theon" }; using var context = new CustomerContext(options); - await context.Database.EnsureCreatedAsync(); + await CosmosTestStore.DatabaseEnsureCreated(context); await context.AddAsync(customer); @@ -119,7 +119,7 @@ public async Task Should_throw_if_specified_connection_mode_is_wrong() var customer = new Customer { Id = 42, Name = "Theon" }; using var context = new CustomerContext(options); - await context.Database.EnsureCreatedAsync(); + await CosmosTestStore.DatabaseEnsureCreated(context); await context.AddAsync(customer); diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs index 79fa4918833..b1d820764f3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs @@ -1,10 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Threading; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.EntityFrameworkCore; @@ -17,15 +15,13 @@ public class CosmosSessionTokensTest(CosmosSessionTokensTest.CosmosFixture fixtu protected CosmosFixture Fixture { get; } = fixture; - private static TestSessionTokenStorage _sessionTokenStorage = null!; - [ConditionalFact] public virtual async Task AppendSessionToken_uses_AppendDefaultContainerSessionToken() { using var context = await CreateContext(); var arg = "0:-1#231"; context.Database.AppendSessionToken(arg); - Assert.Equal(arg, _sessionTokenStorage.AppendDefaultContainerSessionTokenCalls.Single()); + Assert.Equal(arg, Fixture.SessionTokenStorage.AppendDefaultContainerSessionTokenCalls.Single()); } [ConditionalFact] @@ -35,7 +31,7 @@ public virtual async Task AppendSessionTokens_uses_AppendSessionTokens() var arg = new Dictionary { { OtherContainerName, "0:-1#123" }, { nameof(CosmosSessionTokenContext), "0:-1#231" } }; context.Database.AppendSessionTokens(arg); - Assert.Equal(arg, _sessionTokenStorage.AppendSessionTokensCalls.Single()); + Assert.Equal(arg, Fixture.SessionTokenStorage.AppendSessionTokensCalls.Single()); } [ConditionalFact] @@ -44,7 +40,7 @@ public virtual async Task UseSessionToken_uses_SetDefaultContainerSessionToken() using var context = await CreateContext(); var arg = "0:-1#231"; context.Database.UseSessionToken(arg); - Assert.Equal(arg, _sessionTokenStorage.SetDefaultContainerSessionTokenCalls.Single()); + Assert.Equal(arg, Fixture.SessionTokenStorage.SetDefaultContainerSessionTokenCalls.Single()); } [ConditionalFact] @@ -54,16 +50,16 @@ public virtual async Task UseSessionTokens_uses_SetSessionTokens() var arg = new Dictionary { { OtherContainerName, "0:-1#123" }, { nameof(CosmosSessionTokenContext), "0:-1#231" } }; context.Database.UseSessionTokens(arg); - Assert.Equal(arg, _sessionTokenStorage.SetSessionTokensCalls.Single()); + Assert.Equal(arg, Fixture.SessionTokenStorage.SetSessionTokensCalls.Single()); } [ConditionalFact] public virtual async Task GetSessionTokens_uses_GetTrackedSessionTokens() { using var context = await CreateContext(); - _sessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "0:-1#123" }, { nameof(CosmosSessionTokenContext), "0:-1#231" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "0:-1#123" }, { nameof(CosmosSessionTokenContext), "0:-1#231" } }; var sessionTokens = context.Database.GetSessionTokens(); - Assert.Equal(_sessionTokenStorage.SessionTokens, sessionTokens); + Assert.Equal(Fixture.SessionTokenStorage.SessionTokens, sessionTokens); } [ConditionalFact] @@ -71,7 +67,7 @@ public virtual async Task Query_uses_session_token() { using var context = await CreateContext(); - _sessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; var exes = new List { @@ -90,7 +86,7 @@ public virtual async Task PagingQuery_uses_session_token() { using var context = await CreateContext(); - _sessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; var exes = new List() { @@ -109,7 +105,7 @@ public virtual async Task Shaped_query_uses_session_token() { using var context = await CreateContext(); - _sessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; var exes = new List() { @@ -128,7 +124,7 @@ public virtual async Task Read_item_uses_session_token() { using var context = await CreateContext(); - _sessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { OtherContainerName, "invalidtoken" }, { nameof(CosmosSessionTokenContext), "invalidtoken" } }; var exes = new List() { @@ -150,9 +146,9 @@ public virtual async Task Query_uses_TrackSessionToken() await context.Customers.ToListAsync(); await context.OtherContainerCustomers.ToListAsync(); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.First(); - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.Last(); + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.First(); + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.Last(); Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -169,9 +165,9 @@ public virtual async Task PagingQuery_uses_TrackSessionToken() await context.Customers.ToPageAsync(1, null); await context.OtherContainerCustomers.ToPageAsync(1, null); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.First(); - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.Last(); + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.First(); + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.Last(); Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -188,9 +184,9 @@ public virtual async Task Read_item_uses_TrackSessionToken() await context.Customers.FirstOrDefaultAsync(x => x.Id == "1" && x.PartitionKey == "1"); await context.OtherContainerCustomers.FirstOrDefaultAsync(x => x.Id == "1" && x.PartitionKey == "1"); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.First(); - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.Last(); + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.First(); + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.Last(); Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -207,9 +203,9 @@ public virtual async Task Read_item_enumerable_uses_TrackSessionToken() await context.Customers.Where(x => x.Id == "1" && x.PartitionKey == "1").ToListAsync(); await context.OtherContainerCustomers.Where(x => x.Id == "1" && x.PartitionKey == "1").ToListAsync(); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.First(); - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.Last(); + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.First(); + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.Last(); Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -228,9 +224,9 @@ public virtual async Task Add_AutoTransactionBehavior_Never_uses_TrackSessionTok await context.SaveChangesAsync(); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.First(); - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls.Last(); + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.First(); + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls.Last(); Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -257,8 +253,8 @@ public virtual async Task Add_AutoTransactionBehavior_Always_uses_TrackSessionTo await context.SaveChangesAsync(); - Assert.Equal(1, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var call = _sessionTokenStorage.TrackSessionTokenCalls.First(); + Assert.Equal(1, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var call = Fixture.SessionTokenStorage.TrackSessionTokenCalls.First(); if (defaultContainer) { @@ -286,17 +282,17 @@ public virtual async Task Delete_never_uses_TrackSessionToken() await context.SaveChangesAsync(); - var initialDefaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[0]; - var initialOtherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[1]; + var initialDefaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[0]; + var initialOtherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[1]; context.Customers.Remove(customer); context.OtherContainerCustomers.Remove(otherContainerCustomer); await context.SaveChangesAsync(); - Assert.Equal(4, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[2]; - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[3]; + Assert.Equal(4, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[2]; + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[3]; Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -332,7 +328,7 @@ public virtual async Task Delete_always_uses_TrackSessionToken(bool defaultConta await context.SaveChangesAsync(); context.ChangeTracker.Clear(); - var initialCall = _sessionTokenStorage.TrackSessionTokenCalls[0]; + var initialCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[0]; if (defaultContainer) { @@ -345,8 +341,8 @@ public virtual async Task Delete_always_uses_TrackSessionToken(bool defaultConta await context.SaveChangesAsync(); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var call = _sessionTokenStorage.TrackSessionTokenCalls[1]; + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var call = Fixture.SessionTokenStorage.TrackSessionTokenCalls[1]; if (defaultContainer) { @@ -377,17 +373,17 @@ public virtual async Task Update_never_uses_TrackSessionToken() await context.SaveChangesAsync(); - var initialDefaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[0]; - var initialOtherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[1]; + var initialDefaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[0]; + var initialOtherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[1]; customer.Name = "updated"; otherContainerCustomer.Name = "updated"; await context.SaveChangesAsync(); - Assert.Equal(4, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var defaultContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[2]; - var otherContainerCall = _sessionTokenStorage.TrackSessionTokenCalls[3]; + Assert.Equal(4, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var defaultContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[2]; + var otherContainerCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[3]; Assert.Equal(nameof(CosmosSessionTokenContext), defaultContainerCall.containerName); Assert.NotEmpty(defaultContainerCall.sessionToken); @@ -422,7 +418,7 @@ public virtual async Task Update_always_uses_TrackSessionToken(bool defaultConta await context.SaveChangesAsync(); context.ChangeTracker.Clear(); - var initialCall = _sessionTokenStorage.TrackSessionTokenCalls[0]; + var initialCall = Fixture.SessionTokenStorage.TrackSessionTokenCalls[0]; if (defaultContainer) { @@ -435,8 +431,8 @@ public virtual async Task Update_always_uses_TrackSessionToken(bool defaultConta await context.SaveChangesAsync(); - Assert.Equal(2, _sessionTokenStorage.TrackSessionTokenCalls.Count); - var call = _sessionTokenStorage.TrackSessionTokenCalls[1]; + Assert.Equal(2, Fixture.SessionTokenStorage.TrackSessionTokenCalls.Count); + var call = Fixture.SessionTokenStorage.TrackSessionTokenCalls[1]; if (defaultContainer) { @@ -467,7 +463,7 @@ public virtual async Task Add_uses_GetSessionToken(AutoTransactionBehavior autoT // Only way we can test this is by setting a session token that will fail the request if used.. // Only way to do this for a write is to set an invalid session token.. - _sessionTokenStorage.SessionTokens = new Dictionary { { defaultContainer ? nameof(CosmosSessionTokenContext) : OtherContainerName, "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { defaultContainer ? nameof(CosmosSessionTokenContext) : OtherContainerName, "invalidtoken" } }; if (defaultContainer) { @@ -498,7 +494,7 @@ public virtual async Task Update_uses_session_token(AutoTransactionBehavior auto var sessionTokens = context.Database.GetSessionTokens(); // Only way we can test this is by setting a session token that will fail the request if used.. // Only way to do this for a write is to set an invalid session token.. - _sessionTokenStorage.SessionTokens = new Dictionary { { defaultContainer ? nameof(CosmosSessionTokenContext) : OtherContainerName, "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { defaultContainer ? nameof(CosmosSessionTokenContext) : OtherContainerName, "invalidtoken" } }; if (defaultContainer) { @@ -529,7 +525,7 @@ public virtual async Task Delete_uses_session_token(AutoTransactionBehavior auto var sessionTokens = context.Database.GetSessionTokens(); // Only way we can test this is by setting a session token that will fail the request if used.. // Only way to do this for a write is to set an invalid session token.. - _sessionTokenStorage.SessionTokens = new Dictionary { { defaultContainer ? nameof(CosmosSessionTokenContext) : OtherContainerName, "invalidtoken" } }; + Fixture.SessionTokenStorage.SessionTokens = new Dictionary { { defaultContainer ? nameof(CosmosSessionTokenContext) : OtherContainerName, "invalidtoken" } }; if (defaultContainer) { @@ -724,21 +720,22 @@ public virtual async Task Pooled_context_uses_same_SessionTokenStorage() [ConditionalFact] public virtual async Task Pooled_context_clears_SessionTokenStorage() { - var contextFactory = await InitializeAsync(addServices: services => services.Replace(ServiceDescriptor.Singleton())); + TestSessionTokenStorage sessionTokenStorage = null!; + var contextFactory = await InitializeAsync(addServices: services => services.Replace(ServiceDescriptor.Singleton((_) => new TestSessionTokenStorageFactory((storage) => sessionTokenStorage = storage)))); DbContext contextCopy; ISessionTokenStorage sessionTokenStorageCopy; using (var context = contextFactory.CreateContext()) { contextCopy = context; sessionTokenStorageCopy = ((CosmosDatabaseWrapper)context.GetService()).SessionTokenStorage; - _sessionTokenStorage.ClearCalled = false; + sessionTokenStorage.ClearCalled = false; } using var newContext = contextFactory.CreateContext(); Assert.Same(newContext, contextCopy); Assert.Same(sessionTokenStorageCopy, ((CosmosDatabaseWrapper)newContext.GetService()).SessionTokenStorage); - Assert.True(_sessionTokenStorage.ClearCalled); + Assert.True(sessionTokenStorage.ClearCalled); } } @@ -748,17 +745,21 @@ protected async Task CreateContext() context.RemoveRange(await context.Customers.ToListAsync()); context.RemoveRange(await context.OtherContainerCustomers.ToListAsync()); await context.SaveChangesAsync(); - _sessionTokenStorage.TrackSessionTokenCalls.Clear(); + Fixture.SessionTokenStorage.TrackSessionTokenCalls.Clear(); return context; } - private class TestSessionTokenStorageFactory : ISessionTokenStorageFactory + private class TestSessionTokenStorageFactory(Action action) : ISessionTokenStorageFactory { public ISessionTokenStorage Create(DbContext _) - => _sessionTokenStorage = new(); + { + var testStorage = new TestSessionTokenStorage(); + action(testStorage); + return testStorage; + } } - private class TestSessionTokenStorage : ISessionTokenStorage + public class TestSessionTokenStorage : ISessionTokenStorage { public Dictionary SessionTokens { get; set; } = new() { { nameof(CosmosSessionTokenContext), null }, { OtherContainerName, null } }; @@ -784,6 +785,8 @@ private class TestSessionTokenStorage : ISessionTokenStorage public class CosmosFixture : SharedStoreFixtureBase { + public TestSessionTokenStorage SessionTokenStorage { get; set; } = null!; + protected override string StoreName => DatabaseName; @@ -791,7 +794,7 @@ protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance; protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection).Replace(ServiceDescriptor.Singleton()); + => base.AddServices(serviceCollection).Replace(ServiceDescriptor.Singleton((_) => new TestSessionTokenStorageFactory((s) => SessionTokenStorage = s))); } public class CosmosSessionTokenContext(DbContextOptions options) : PoolableDbContext(options) diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosTriggersTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosTriggersTest.cs index 43d82f252ff..cf9bf2dac8c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosTriggersTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosTriggersTest.cs @@ -68,8 +68,6 @@ public async Task Triggers_are_executed_on_SaveChanges() private async Task CreateTriggersInCosmosAsync(TriggersContext context) { - await context.Database.EnsureCreatedAsync(); - var cosmosClient = context.Database.GetCosmosClient(); var databaseId = context.Database.GetCosmosDatabaseId(); var database = cosmosClient.GetDatabase(databaseId); diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index 0362090e08e..f83ef9e6d37 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -910,8 +910,6 @@ public async Task Find_with_empty_resource_id_throws(bool transactionalBatch) using (var context = CreateContext(contextFactory, transactionalBatch)) { - await context.Database.EnsureCreatedAsync(); - var exception = await Assert.ThrowsAsync(async () => await context.Set().FindAsync(1, 3.15m, "")); Assert.Equal(CosmosStrings.InvalidResourceId, exception.Message); @@ -1061,8 +1059,6 @@ public async Task Can_read_with_find_with_partition_key_not_part_of_primary_key( using (var context = CreateContext(contextFactory, false)) { - await context.Database.EnsureCreatedAsync(); - await context.AddAsync(customer); await context.SaveChangesAsync(); diff --git a/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs index 9f023a5ed0f..8587ce7a491 100644 --- a/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs @@ -137,8 +137,6 @@ protected virtual async Task PartitionKeyTestAsync( await using (var innerContext = CreateContext()) { - await innerContext.Database.EnsureCreatedAsync(); - await innerContext.AddAsync(customer1); await innerContext.AddAsync(customer2); await innerContext.SaveChangesAsync(); diff --git a/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs b/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs index 7f9033382f8..9b28654eeb7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs @@ -107,8 +107,6 @@ protected virtual async Task PartitionKeyTestAsync( await using (var innerContext = CreateContext()) { - await innerContext.Database.EnsureCreatedAsync(); - await innerContext.AddAsync(customer1); await innerContext.AddAsync(customer2); await innerContext.SaveChangesAsync(); diff --git a/test/EFCore.Cosmos.FunctionalTests/Properties/TestAssemblyCondition.cs b/test/EFCore.Cosmos.FunctionalTests/Properties/TestAssemblyCondition.cs index 4bfe2bfbbc3..a67b9a075ed 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Properties/TestAssemblyCondition.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Properties/TestAssemblyCondition.cs @@ -5,5 +5,9 @@ [assembly: CosmosDbConfiguredCondition] -// Waiting on Task causes deadlocks when run in parallel -[assembly: CollectionBehavior(DisableTestParallelization = true)] +// Emulator could experience performance degradation with more than 10 concurrent containers, +// Tests have shown that the emulator will stop responding for container creation requests after ~25 containers are created. +// Some tests might create multiple containers, so we only run 3 tests in parallel to avoid hitting the limit. +// No performance improvement was found with a higher number. +// See: https://learn.microsoft.com/en-us/azure/cosmos-db/emulator#differences-between-the-emulator-and-cloud-service +[assembly: CollectionBehavior(MaxParallelThreads = 3)] diff --git a/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs b/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs index f9b74659ced..8152d6adfdf 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs @@ -17,7 +17,7 @@ public async Task EnsureCreated_returns_true_when_database_does_not_exist() using var context = new BloggingContext(testDatabase); var creator = context.GetService(); await creator.EnsureDeletedAsync(); - Assert.True(await creator.EnsureCreatedAsync()); + Assert.True(await EnsureCreatedAsync(creator)); } [ConditionalFact] @@ -28,7 +28,7 @@ public async Task EnsureCreated_returns_true_when_database_exists_but_collection using var context = new BloggingContext(testDatabase); var creator = context.GetService(); - Assert.True(await creator.EnsureCreatedAsync()); + Assert.True(await EnsureCreatedAsync(creator)); } [ConditionalTheory, MemberData(nameof(IsAsyncData))] @@ -43,7 +43,7 @@ await testDatabase.InitializeAsync( using var context = new BloggingContext(testDatabase); var creator = context.GetService(); - Assert.False(a ? await creator.EnsureCreatedAsync() : creator.EnsureCreated()); + Assert.False(a ? await EnsureCreatedAsync(creator) : creator.EnsureCreated()); }); [ConditionalTheory, MemberData(nameof(IsAsyncData))] @@ -55,7 +55,7 @@ public Task EnsureDeleted_returns_true_when_database_exists(bool async) using var context = new BloggingContext(testDatabase); var creator = context.GetService(); - Assert.True(a ? await creator.EnsureDeletedAsync() : creator.EnsureDeleted()); + Assert.True(a ? await EnsureCreatedAsync(creator) : creator.EnsureDeleted()); }); [ConditionalTheory, MemberData(nameof(IsAsyncData))] @@ -81,6 +81,25 @@ public async Task EnsureCreated_throws_for_missing_seed() (await Assert.ThrowsAsync(() => context.Database.EnsureCreatedAsync())).Message); } + private async Task EnsureCreatedAsync(IDatabaseCreator creator) + { + if (TestEnvironment.IsEmulator) + { + await CosmosTestStore.EmulatorCreateCollectionSemaphore.WaitAsync(); + } + try + { + return await creator.EnsureCreatedAsync(); + } + finally + { + if (TestEnvironment.IsEmulator) + { + CosmosTestStore.EmulatorCreateCollectionSemaphore.Release(); + } + } + } + private class BaseContext(CosmosTestStore testStore) : DbContext { private readonly string _connectionUri = testStore.ConnectionUri; diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index b4ef5fa0749..0559fd92346 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -19,6 +19,12 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; public class CosmosTestStore : TestStore { + /// + /// The emulator has race conditions when creating containers concurrently, so we need to lock around container creation. + /// + public static readonly SemaphoreSlim EmulatorCreateCollectionSemaphore = new(1); + + private static List? _createdDatabases = []; private readonly TestStoreContext _storeContext; private readonly string? _dataFilePath; private readonly Action _configureCosmos; @@ -75,9 +81,25 @@ private CosmosTestStore( } private static string CreateName(string name) - => TestEnvironment.IsEmulator || name == "Northwind" || name == "Northwind2" || name == "Northwind3" - ? name - : name + _runId; + { + // Northwind database is static and huge so we don't want to create a new one for each test run. + if (name == "Northwind" || name == "Northwind2" || name == "Northwind3") + { + return name; + } + + if (TestEnvironment.IsEmulator) + { + // We delete and recreate the database for each test run in the emulator. + // This is due to limitations in the emulator. + // Since the databases are deleted, they can't be shared across test runs. + // So we have to generate a new name for each test store, otherwhise we would have name conflicts when running tests in parallel. + // https://learn.microsoft.com/en-us/azure/cosmos-db/emulator#differences-between-the-emulator-and-cloud-service + return "EF-" + Guid.NewGuid().ToString(); + } + + return name + _runId; + } public string ConnectionUri { get; } public string AuthToken { get; } @@ -108,6 +130,7 @@ public static async ValueTask IsConnectionAvailableAsync() { _connectionSemaphore.Release(); } + } return _connectionAvailable.Value; @@ -120,6 +143,32 @@ private static async Task TryConnectAsync() { testStore = await CreateInitializedAsync("NonExistent").ConfigureAwait(false); + if (TestEnvironment.IsEmulator) + { + // Clean up old emulator databases that tests might have left behind. + // We do this because the emulator will stop responding with much more than ~10 concurrent containers, + // and test runs might have been stopped in the middle leaving databases and containers behind. + var client = testStore.CreateDefaultContext().Database.GetCosmosClient(); + using var iterator = client.GetDatabaseQueryIterator(); + + while (iterator.HasMoreResults) + { + var response = await iterator.ReadNextAsync(); + + // This code will run after the first fixtures are initialized, + // so we keep track of the databases we created in order to not delete them here. + foreach (var db in response.Where(x => x.Id.StartsWith("EF-") && !_createdDatabases!.Contains(x.Id))) + { + await client.GetDatabase(db.Id).DeleteAsync(); + } + } + + // We do not need to track created databases anymore, + // Since this code only runs once on startup. + _createdDatabases = null; + } + + return true; } catch (AggregateException aggregate) @@ -179,13 +228,34 @@ protected override async Task InitializeAsync(Func createContext, Fun } } + public static async Task DatabaseEnsureCreated(DbContext context) + { + if (TestEnvironment.IsEmulator) + { + // The emulator has a race condition when creating containers concurrently, + // so we need to lock around EnsureCreated + await EmulatorCreateCollectionSemaphore.WaitAsync(); + } + try + { + return await context.Database.EnsureCreatedAsync().ConfigureAwait(false); + } + finally + { + if (TestEnvironment.IsEmulator) + { + EmulatorCreateCollectionSemaphore.Release(); + } + } + } + private async Task CreateFromFile(DbContext context) { if (await EnsureCreatedAsync(context).ConfigureAwait(false)) { if (!TestEnvironment.UseTokenCredential) { - await context.Database.EnsureCreatedAsync().ConfigureAwait(false); + await DatabaseEnsureCreated(context).ConfigureAwait(false); } else { @@ -267,7 +337,15 @@ public async Task EnsureCreatedAsync(DbContext context, CancellationToken if (!TestEnvironment.UseTokenCredential) { var cosmosClientWrapper = context.GetService(); - return await cosmosClientWrapper.CreateDatabaseIfNotExistsAsync(null, cancellationToken).ConfigureAwait(false); + var r = await cosmosClientWrapper.CreateDatabaseIfNotExistsAsync(null, cancellationToken).ConfigureAwait(false); + if (r) + { + // Keep track of created databases, to prevent them from being deleted in cleanup function, + // that could run after the creation of this database, due to xunit creating the first test fixtures before checking the availability of the connection. + _createdDatabases?.Add(Name); + } + + return r; } var databaseAccount = await GetDBAccount(cancellationToken).ConfigureAwait(false); @@ -293,7 +371,8 @@ public async Task EnsureCreatedAsync(DbContext context, CancellationToken { sqlDatabaseCreateUpdateContent.Options = new CosmosDBCreateUpdateConfig { - Throughput = modelThroughput.Throughput, AutoscaleMaxThroughput = modelThroughput.AutoscaleMaxThroughput + Throughput = modelThroughput.Throughput, + AutoscaleMaxThroughput = modelThroughput.AutoscaleMaxThroughput }; } @@ -349,7 +428,8 @@ public override async Task CleanAsync(DbContext context, bool createTables = tru if (!TestEnvironment.UseTokenCredential) { - created = await context.Database.EnsureCreatedAsync().ConfigureAwait(false); + created = await DatabaseEnsureCreated(context).ConfigureAwait(false); + if (!created) { await SeedAsync(context).ConfigureAwait(false); @@ -406,7 +486,8 @@ private async Task CreateContainersAsync(DbContext context) { content.Options = new CosmosDBCreateUpdateConfig { - AutoscaleMaxThroughput = container.Throughput.AutoscaleMaxThroughput, Throughput = container.Throughput.Throughput + AutoscaleMaxThroughput = container.Throughput.AutoscaleMaxThroughput, + Throughput = container.Throughput.Throughput }; } @@ -556,6 +637,7 @@ private static async Task SeedAsync(DbContext context) public override async ValueTask DisposeAsync() { + // We don't delete the database if it was created from a data file, because importing it is really slow. if (_initialized && _dataFilePath == null) { @@ -569,6 +651,8 @@ public override async ValueTask DisposeAsync() GetTestStoreIndex(ServiceProvider).RemoveShared(GetType().Name + Name); } + // We always delete the database because the emulator will stop responding with much more than ~10 concurrent containers, + // https://learn.microsoft.com/en-us/azure/cosmos-db/emulator#differences-between-the-emulator-and-cloud-service await EnsureDeletedAsync(_storeContext).ConfigureAwait(false); }