Skip to content

Commit

Permalink
Cosmos: Accept changes for root entities that were not modified but e…
Browse files Browse the repository at this point in the history
…nd up being changed due to ETag update (#29091)
  • Loading branch information
ajcvickers committed Sep 14, 2022
1 parent 0a2740b commit 5c46563
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 16 deletions.
2 changes: 2 additions & 0 deletions src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public override int SaveChanges(IList<IUpdateEntry> entries)
// #16707
((InternalEntityEntry)root).SetEntityState(EntityState.Modified);
#pragma warning restore EF1001 // Internal EF Core API usage.
entries.Add(root);
}

continue;
Expand Down Expand Up @@ -151,6 +152,7 @@ public override async Task<int> SaveChangesAsync(
// #16707
((InternalEntityEntry)root).SetEntityState(EntityState.Modified);
#pragma warning restore EF1001 // Internal EF Core API usage.
entries.Add(root);
}

continue;
Expand Down
100 changes: 84 additions & 16 deletions test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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<Customer>().SingleAsync();

Expand All @@ -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();

Expand All @@ -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<Customer>().SingleAsync();

Expand All @@ -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<Customer>().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);
}
}

/// <summary>
/// Runs the two actions with two different contexts and calling
/// SaveChanges such that storeChange will succeed and the store will reflect this change, and
Expand Down Expand Up @@ -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
{
Expand All @@ -209,5 +270,12 @@ public class Customer
public string Name { get; set; }

public string ETag { get; set; }

public ICollection<DummyChild> Children { get; } = new HashSet<DummyChild>();
}

public class DummyChild
{
public string Id { get; init; }
}
}

0 comments on commit 5c46563

Please sign in to comment.