diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs index 561c01b61c4..87632517098 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs @@ -78,6 +78,7 @@ public override int SaveChanges(IList entries) // #16707 ((InternalEntityEntry)root).SetEntityState(EntityState.Modified); #pragma warning restore EF1001 // Internal EF Core API usage. + entries.Add(root); } continue; @@ -151,6 +152,7 @@ public override async Task SaveChangesAsync( // #16707 ((InternalEntityEntry)root).SetEntityState(EntityState.Modified); #pragma warning restore EF1001 // Internal EF Core API usage. + entries.Add(root); } continue; diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs index 01082ad41ac..b414f9550b1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs @@ -48,14 +48,16 @@ public virtual Task Updating_then_updating_the_same_entity_results_in_DbUpdateCo [ConditionalFact] public async Task Etag_will_return_when_content_response_enabled_false() { - await using var testDatabase = CosmosTestStore.CreateInitialized(DatabaseName); + await using var testDatabase = CosmosTestStore.CreateInitialized( + DatabaseName, + o => o.ContentResponseOnWriteEnabled(false)); var customer = new Customer { Id = "4", Name = "Theon", }; - await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: false))) + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase))) { await context.Database.EnsureCreatedAsync(); @@ -64,7 +66,7 @@ public async Task Etag_will_return_when_content_response_enabled_false() await context.SaveChangesAsync(); } - await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: false))) + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase))) { var customerFromStore = await context.Set().SingleAsync(); @@ -81,14 +83,16 @@ public async Task Etag_will_return_when_content_response_enabled_false() [ConditionalFact] public async Task Etag_will_return_when_content_response_enabled_true() { - await using var testDatabase = CosmosTestStore.Create(DatabaseName); + await using var testDatabase = CosmosTestStore.CreateInitialized( + DatabaseName, + o => o.ContentResponseOnWriteEnabled()); var customer = new Customer { Id = "3", Name = "Theon", }; - await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: true))) + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase))) { await context.Database.EnsureCreatedAsync(); @@ -97,7 +101,7 @@ public async Task Etag_will_return_when_content_response_enabled_true() await context.SaveChangesAsync(); } - await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: true))) + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase))) { var customerFromStore = await context.Set().SingleAsync(); @@ -111,6 +115,70 @@ public async Task Etag_will_return_when_content_response_enabled_true() } } + [ConditionalTheory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentResponseOnWriteEnabled) + { + await using var testDatabase = CosmosTestStore.CreateInitialized( + DatabaseName, + o => + { + if (contentResponseOnWriteEnabled.HasValue) + { + o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value); + } + }); + + var customer = new Customer + { + Id = "5", + Name = "Theon", + Children = { new DummyChild { Id = "0" } } + }; + + string etag = null; + + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase))) + { + await context.Database.EnsureCreatedAsync(); + + context.Add(customer); + + await context.SaveChangesAsync(); + + etag = customer.ETag; + } + + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase))) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.NotEmpty(customerFromStore.ETag); + Assert.Equal(etag, customerFromStore.ETag); + + customerFromStore.Children.Add(new DummyChild { Id = "1" }); + + await context.SaveChangesAsync(); + + Assert.NotEmpty(customerFromStore.ETag); + Assert.NotEqual(etag, customerFromStore.ETag); + + customerFromStore.Children.Add(new DummyChild { Id = "2" }); + + Assert.NotEmpty(customerFromStore.ETag); + Assert.NotEqual(etag, customerFromStore.ETag); + + customerFromStore.Children.Add(new DummyChild { Id = "3" }); + + await context.SaveChangesAsync(); + + Assert.NotEmpty(customerFromStore.ETag); + Assert.NotEqual(etag, customerFromStore.ETag); + } + } + /// /// Runs the two actions with two different contexts and calling /// SaveChanges such that storeChange will succeed and the store will reflect this change, and @@ -186,21 +254,14 @@ protected override void OnModelCreating(ModelBuilder builder) { b.HasKey(c => c.Id); b.Property(c => c.ETag).IsETagConcurrency(); + b.OwnsMany(x => x.Children); }); } - private DbContextOptions CreateOptions(CosmosTestStore testDatabase, bool enableContentResponseOnWrite) - { - var optionsBuilder = new DbContextOptionsBuilder(); - - new DbContextOptionsBuilder().UseCosmos( - testDatabase.ConnectionString, testDatabase.Name, - b => b.ApplyConfiguration().ContentResponseOnWriteEnabled(enabled: enableContentResponseOnWrite)); - - return testDatabase.AddProviderOptions(optionsBuilder) + private DbContextOptions CreateOptions(CosmosTestStore testDatabase) + => testDatabase.AddProviderOptions(new DbContextOptionsBuilder()) .EnableDetailedErrors() .Options; - } public class Customer { @@ -209,5 +270,12 @@ public class Customer public string Name { get; set; } public string ETag { get; set; } + + public ICollection Children { get; } = new HashSet(); + } + + public class DummyChild + { + public string Id { get; init; } } }