From dbe4b00b6bca0ed921bc6de49888f1584c084679 Mon Sep 17 00:00:00 2001 From: Umit Kavala Date: Fri, 13 Nov 2020 22:32:40 +0100 Subject: [PATCH 1/5] Perf: unset EnableContentResponseOnWrite dotnet#22999 --- .../CosmosDbContextOptionsBuilder.cs | 10 ++++++++ .../Internal/CosmosDbOptionExtension.cs | 25 +++++++++++++++++++ .../Internal/CosmosSingletonOptions.cs | 13 +++++++++- .../Internal/ICosmosSingletonOptions.cs | 8 ++++++ .../Storage/Internal/CosmosClientWrapper.cs | 13 ++++++---- .../Internal/SingletonCosmosClientWrapper.cs | 5 ++++ .../CosmosDbContextOptionsExtensionsTests.cs | 17 +++++++++++++ 7 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index c8c42564b38..32a79044b5e 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -120,6 +120,16 @@ public virtual CosmosDbContextOptionsBuilder MaxTcpConnectionsPerEndpoint(int co public virtual CosmosDbContextOptionsBuilder MaxRequestsPerTcpConnection(int requestLimit) => WithOption(e => e.WithMaxRequestsPerTcpConnection(Check.NotNull(requestLimit, nameof(requestLimit)))); + /// + /// Sets the boolean to only return the headers and status code in the Cosmos DB response for write item operation + /// like Create, Upsert, Patch and Replace. Setting the option to false will cause the response to have a null resource. + /// This reduces networking and CPU load by not sending the resource back over the network and serializing it on the client. + /// + /// to have null resource + public virtual CosmosDbContextOptionsBuilder ContentResponseOnWriteEnabled(bool enabled = false) + => WithOption(e => e.ContentResponseOnWriteEnabled(Check.NotNull(enabled, nameof(enabled)))); + + /// /// Sets an option by cloning the extension used to store the settings. This ensures the builder /// does not modify options that are already in use elsewhere. diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs index 3309d3bc01a..05a95eebf07 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs @@ -39,6 +39,7 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension private int? _gatewayModeMaxConnectionLimit; private int? _maxTcpConnectionsPerEndpoint; private int? _maxRequestsPerTcpConnection; + private bool? _enableContentResponseOnWrite; private DbContextOptionsExtensionInfo _info; /// @@ -441,6 +442,30 @@ public virtual CosmosOptionsExtension WithMaxRequestsPerTcpConnection(int reques return clone; } + /// + /// 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. + /// + public virtual bool? EnableContentResponseOnWrite + => _enableContentResponseOnWrite; + + /// + /// 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. + /// + public virtual CosmosOptionsExtension ContentResponseOnWriteEnabled(bool enabled) + { + var clone = Clone(); + + clone._enableContentResponseOnWrite = enabled; + + return clone; + } + /// /// A factory for creating the default , or if none has been /// configured. diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs index d597da75d6b..4dc74bcd955 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs @@ -129,6 +129,14 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions /// public virtual int? MaxRequestsPerTcpConnection { get; private set; } + /// + /// 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. + /// + public virtual bool? EnableContentResponseOnWrite { get; private set; } + /// /// 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 @@ -153,6 +161,7 @@ public virtual void Initialize(IDbContextOptions options) GatewayModeMaxConnectionLimit = cosmosOptions.GatewayModeMaxConnectionLimit; MaxTcpConnectionsPerEndpoint = cosmosOptions.MaxTcpConnectionsPerEndpoint; MaxRequestsPerTcpConnection = cosmosOptions.MaxRequestsPerTcpConnection; + EnableContentResponseOnWrite = cosmosOptions.EnableContentResponseOnWrite; } } @@ -179,7 +188,9 @@ public virtual void Validate(IDbContextOptions options) || IdleTcpConnectionTimeout != cosmosOptions.IdleTcpConnectionTimeout || GatewayModeMaxConnectionLimit != cosmosOptions.GatewayModeMaxConnectionLimit || MaxTcpConnectionsPerEndpoint != cosmosOptions.MaxTcpConnectionsPerEndpoint - || MaxRequestsPerTcpConnection != cosmosOptions.MaxRequestsPerTcpConnection)) + || MaxRequestsPerTcpConnection != cosmosOptions.MaxRequestsPerTcpConnection + || EnableContentResponseOnWrite != cosmosOptions.EnableContentResponseOnWrite + )) { throw new InvalidOperationException( CoreStrings.SingletonOptionChanged( diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs index df14c951248..b6b3a0e8d91 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs @@ -128,5 +128,13 @@ public interface ICosmosSingletonOptions : ISingletonOptions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// int? MaxRequestsPerTcpConnection { get; } + + /// + /// 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. + /// + bool? EnableContentResponseOnWrite { get; } } } diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index 1342182e7a2..95a85227e23 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -292,7 +292,8 @@ private async Task CreateItemOnceAsync( var entry = parameters.Entry; var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var itemRequestOptions = CreateItemRequestOptions(entry); + var enableContentResponseOnWrite = ((ICosmosSingletonOptions)Client.ClientOptions).EnableContentResponseOnWrite; + var itemRequestOptions = CreateItemRequestOptions(entry, enableContentResponseOnWrite); var partitionKey = CreatePartitionKey(entry); using var response = await container.CreateItemStreamAsync(stream, partitionKey, itemRequestOptions, cancellationToken) @@ -354,7 +355,8 @@ private async Task ReplaceItemOnceAsync( var entry = parameters.Entry; var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var itemRequestOptions = CreateItemRequestOptions(entry); + var enableContentResponseOnWrite = ((ICosmosSingletonOptions)Client.ClientOptions).EnableContentResponseOnWrite; + var itemRequestOptions = CreateItemRequestOptions(entry, enableContentResponseOnWrite); var partitionKey = CreatePartitionKey(entry); using var response = await container.ReplaceItemStreamAsync( @@ -416,7 +418,8 @@ public virtual async Task DeleteItemOnceAsync( { var entry = parameters.Entry; var items = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var itemRequestOptions = CreateItemRequestOptions(entry); + var enableContentResponseOnWrite = ((ICosmosSingletonOptions)Client.ClientOptions).EnableContentResponseOnWrite; + var itemRequestOptions = CreateItemRequestOptions(entry, enableContentResponseOnWrite); var partitionKey = CreatePartitionKey(entry); using var response = await items.DeleteItemStreamAsync( @@ -427,7 +430,7 @@ public virtual async Task DeleteItemOnceAsync( return response.StatusCode == HttpStatusCode.NoContent; } - private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry) + private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, bool? enableContentResponseOnWrite) { var etagProperty = entry.EntityType.GetETagProperty(); if (etagProperty == null) @@ -442,7 +445,7 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry) etag = converter.ConvertToProvider(etag); } - return new ItemRequestOptions { IfMatchEtag = (string)etag }; + return new ItemRequestOptions { IfMatchEtag = (string)etag, EnableContentResponseOnWrite = enableContentResponseOnWrite }; } private static PartitionKey CreatePartitionKey(IUpdateEntry entry) diff --git a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs index 00a33e26fe4..217a0c520d6 100644 --- a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs @@ -96,6 +96,11 @@ public SingletonCosmosClientWrapper([NotNull] ICosmosSingletonOptions options) configuration.MaxRequestsPerTcpConnection = options.MaxRequestsPerTcpConnection.Value; } + if (options.EnableContentResponseOnWrite != null) + { + configuration.EnableTcpConnectionEndpointRediscovery = options.EnableContentResponseOnWrite.Value; + } + _options = configuration; } diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index 311d18b9ada..4560e68f372 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -191,5 +191,22 @@ public void Can_create_options_with_max_requests_per_tcp_connection() Assert.Equal(requestLimit, extension.MaxRequestsPerTcpConnection); } + + + [ConditionalFact] + public void Can_create_options_with_content_response_on_write_enabled() + { + var enabled = true; + var options = new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + o => { o.ContentResponseOnWriteEnabled(enabled); }); + + var extension = options.Options.FindExtension(); + + Assert.Equal(enabled, extension.EnableContentResponseOnWrite); + } } } + From 7f1d3ffcd66b1cced926f608be184f756a17c75c Mon Sep 17 00:00:00 2001 From: Umit Kavala Date: Sat, 14 Nov 2020 02:05:17 +0100 Subject: [PATCH 2/5] Perf: unset EnableContentResponseOnWrite dotnet#22999 Etag test added. --- .../CosmosDbContextOptionsBuilder.cs | 2 +- .../Storage/Internal/CosmosClientWrapper.cs | 20 +-- .../Internal/SingletonCosmosClientWrapper.cs | 5 - .../ContentResponseEtagTest.cs | 119 ++++++++++++++++++ 4 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index 32a79044b5e..6db7fde6da3 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -126,7 +126,7 @@ public virtual CosmosDbContextOptionsBuilder MaxRequestsPerTcpConnection(int req /// This reduces networking and CPU load by not sending the resource back over the network and serializing it on the client. /// /// to have null resource - public virtual CosmosDbContextOptionsBuilder ContentResponseOnWriteEnabled(bool enabled = false) + public virtual CosmosDbContextOptionsBuilder ContentResponseOnWriteEnabled(bool enabled = true) => WithOption(e => e.ContentResponseOnWriteEnabled(Check.NotNull(enabled, nameof(enabled)))); diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index 95a85227e23..2d6a68f8327 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -64,6 +64,7 @@ public class CosmosClientWrapper private readonly string _databaseId; private readonly IExecutionStrategyFactory _executionStrategyFactory; private readonly IDiagnosticsLogger _commandLogger; + private readonly bool? _enableContentResponseOnWrite; static CosmosClientWrapper() { @@ -89,6 +90,7 @@ public CosmosClientWrapper( _databaseId = options.DatabaseName; _executionStrategyFactory = executionStrategyFactory; _commandLogger = commandLogger; + _enableContentResponseOnWrite = options.EnableContentResponseOnWrite; } private CosmosClient Client @@ -292,8 +294,7 @@ private async Task CreateItemOnceAsync( var entry = parameters.Entry; var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var enableContentResponseOnWrite = ((ICosmosSingletonOptions)Client.ClientOptions).EnableContentResponseOnWrite; - var itemRequestOptions = CreateItemRequestOptions(entry, enableContentResponseOnWrite); + var itemRequestOptions = CreateItemRequestOptions(entry, _enableContentResponseOnWrite); var partitionKey = CreatePartitionKey(entry); using var response = await container.CreateItemStreamAsync(stream, partitionKey, itemRequestOptions, cancellationToken) @@ -355,8 +356,7 @@ private async Task ReplaceItemOnceAsync( var entry = parameters.Entry; var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var enableContentResponseOnWrite = ((ICosmosSingletonOptions)Client.ClientOptions).EnableContentResponseOnWrite; - var itemRequestOptions = CreateItemRequestOptions(entry, enableContentResponseOnWrite); + var itemRequestOptions = CreateItemRequestOptions(entry, _enableContentResponseOnWrite); var partitionKey = CreatePartitionKey(entry); using var response = await container.ReplaceItemStreamAsync( @@ -418,8 +418,8 @@ public virtual async Task DeleteItemOnceAsync( { var entry = parameters.Entry; var items = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var enableContentResponseOnWrite = ((ICosmosSingletonOptions)Client.ClientOptions).EnableContentResponseOnWrite; - var itemRequestOptions = CreateItemRequestOptions(entry, enableContentResponseOnWrite); + + var itemRequestOptions = CreateItemRequestOptions(entry, _enableContentResponseOnWrite); var partitionKey = CreatePartitionKey(entry); using var response = await items.DeleteItemStreamAsync( @@ -445,7 +445,13 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, b etag = converter.ConvertToProvider(etag); } - return new ItemRequestOptions { IfMatchEtag = (string)etag, EnableContentResponseOnWrite = enableContentResponseOnWrite }; + var jObjectProperty = entry.EntityType.FindProperty(StoreKeyConvention.JObjectPropertyName); + + var enabledContentResponse = enableContentResponseOnWrite.HasValue + ? enableContentResponseOnWrite + : jObjectProperty?.ValueGenerated == ValueGenerated.OnAddOrUpdate; + + return new ItemRequestOptions { IfMatchEtag = (string)etag, EnableContentResponseOnWrite = enabledContentResponse }; } private static PartitionKey CreatePartitionKey(IUpdateEntry entry) diff --git a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs index 217a0c520d6..00a33e26fe4 100644 --- a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs @@ -96,11 +96,6 @@ public SingletonCosmosClientWrapper([NotNull] ICosmosSingletonOptions options) configuration.MaxRequestsPerTcpConnection = options.MaxRequestsPerTcpConnection.Value; } - if (options.EnableContentResponseOnWrite != null) - { - configuration.EnableTcpConnectionEndpointRediscovery = options.EnableContentResponseOnWrite.Value; - } - _options = configuration; } diff --git a/test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs b/test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs new file mode 100644 index 00000000000..54802969620 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Cosmos +{ + public class ContentResponseEtagTest + { + [ConditionalFact] + public async Task Etag_will_return_when_content_response_enabled_false() + { + await using var testDatabase = CosmosTestStore.Create("CustomerDemo"); + + var customer = new CustomerWithEtag + { + Id = Guid.NewGuid(), + Name = "Theon", + }; + + using (var context = new CustomerContextWithContentResponse(testDatabase, false)) + { + await context.Database.EnsureCreatedAsync(); + + context.Add(customer); + + await context.SaveChangesAsync(); + } + + using (var context = new CustomerContextWithContentResponse(testDatabase, false)) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.Equal(customer.Id, customerFromStore.Id); + Assert.Equal("Theon", customerFromStore.Name); + Assert.Equal(customer.ETag, customerFromStore.ETag); + + context.Remove(customerFromStore); + + context.SaveChanges(); + } + } + + [ConditionalFact] + public async Task Etag_will_return_when_content_response_enabled_true() + { + await using var testDatabase = CosmosTestStore.Create("CustomerDemo"); + + var customer = new CustomerWithEtag + { + Id = Guid.NewGuid(), + Name = "Theon", + }; + + using (var context = new CustomerContextWithContentResponse(testDatabase, true)) + { + await context.Database.EnsureCreatedAsync(); + + context.Add(customer); + + await context.SaveChangesAsync(); + } + + using (var context = new CustomerContextWithContentResponse(testDatabase, true)) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.Equal(customer.Id, customerFromStore.Id); + Assert.Equal("Theon", customerFromStore.Name); + Assert.Equal(customer.ETag, customerFromStore.ETag); + + context.Remove(customerFromStore); + + context.SaveChanges(); + } + } + + private class CustomerContextWithContentResponse : DbContext + { + private readonly string _connectionString; + private readonly string _name; + private readonly bool _contentResponseOnWriteEnabled; + + public CustomerContextWithContentResponse(CosmosTestStore testStore, bool contentResponseOnWriteEnabled) + { + _connectionString = testStore.ConnectionString; + _name = testStore.Name; + _contentResponseOnWriteEnabled = contentResponseOnWriteEnabled; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseCosmos(_connectionString, _name, b => b.ApplyConfiguration().ContentResponseOnWriteEnabled(_contentResponseOnWriteEnabled)); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.HasKey(c => c.Id); + b.Property(c => c.ETag).IsETagConcurrency(); + }); + } + + public DbSet Customers { get; set; } + } + + private class CustomerWithEtag + { + public Guid Id { get; set; } + public string Name { get; set; } + public string ETag { get; set; } + } + } +} From e44abbbf0da0cd489c64a5c5c62a277a35921bf5 Mon Sep 17 00:00:00 2001 From: Umit Kavala Date: Sat, 14 Nov 2020 13:00:24 +0100 Subject: [PATCH 3/5] Perf: unset EnableContentResponseOnWrite dotnet#22999 refactoring on long conditions --- .../Storage/Internal/CosmosClientWrapper.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index 2d6a68f8327..6d969cc5813 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -294,7 +294,7 @@ private async Task CreateItemOnceAsync( var entry = parameters.Entry; var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var itemRequestOptions = CreateItemRequestOptions(entry, _enableContentResponseOnWrite); + var itemRequestOptions = CreateItemRequestOptions(entry); var partitionKey = CreatePartitionKey(entry); using var response = await container.CreateItemStreamAsync(stream, partitionKey, itemRequestOptions, cancellationToken) @@ -356,7 +356,7 @@ private async Task ReplaceItemOnceAsync( var entry = parameters.Entry; var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var itemRequestOptions = CreateItemRequestOptions(entry, _enableContentResponseOnWrite); + var itemRequestOptions = CreateItemRequestOptions(entry); var partitionKey = CreatePartitionKey(entry); using var response = await container.ReplaceItemStreamAsync( @@ -419,7 +419,7 @@ public virtual async Task DeleteItemOnceAsync( var entry = parameters.Entry; var items = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - var itemRequestOptions = CreateItemRequestOptions(entry, _enableContentResponseOnWrite); + var itemRequestOptions = CreateItemRequestOptions(entry); var partitionKey = CreatePartitionKey(entry); using var response = await items.DeleteItemStreamAsync( @@ -430,7 +430,7 @@ public virtual async Task DeleteItemOnceAsync( return response.StatusCode == HttpStatusCode.NoContent; } - private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, bool? enableContentResponseOnWrite) + private ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry) { var etagProperty = entry.EntityType.GetETagProperty(); if (etagProperty == null) @@ -445,11 +445,7 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, b etag = converter.ConvertToProvider(etag); } - var jObjectProperty = entry.EntityType.FindProperty(StoreKeyConvention.JObjectPropertyName); - - var enabledContentResponse = enableContentResponseOnWrite.HasValue - ? enableContentResponseOnWrite - : jObjectProperty?.ValueGenerated == ValueGenerated.OnAddOrUpdate; + var enabledContentResponse = _enableContentResponseOnWrite ?? entry.EntityType.FindProperty(StoreKeyConvention.JObjectPropertyName)?.ValueGenerated == ValueGenerated.OnAddOrUpdate; return new ItemRequestOptions { IfMatchEtag = (string)etag, EnableContentResponseOnWrite = enabledContentResponse }; } From e79d254dd3187ede80a1c36a2c97c73655868aec Mon Sep 17 00:00:00 2001 From: Umit Kavala Date: Sun, 15 Nov 2020 15:51:26 +0100 Subject: [PATCH 4/5] Perf: unset EnableContentResponseOnWrite dotnet#22999 moved ETag tests to the ConcurencyTest class --- .../ContentResponseEtagTest.cs | 119 ------------------ .../CosmosConcurrencyTest.cs | 90 ++++++++++++- 2 files changed, 87 insertions(+), 122 deletions(-) delete mode 100644 test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs diff --git a/test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs b/test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs deleted file mode 100644 index 54802969620..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/ContentResponseEtagTest.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Cosmos -{ - public class ContentResponseEtagTest - { - [ConditionalFact] - public async Task Etag_will_return_when_content_response_enabled_false() - { - await using var testDatabase = CosmosTestStore.Create("CustomerDemo"); - - var customer = new CustomerWithEtag - { - Id = Guid.NewGuid(), - Name = "Theon", - }; - - using (var context = new CustomerContextWithContentResponse(testDatabase, false)) - { - await context.Database.EnsureCreatedAsync(); - - context.Add(customer); - - await context.SaveChangesAsync(); - } - - using (var context = new CustomerContextWithContentResponse(testDatabase, false)) - { - var customerFromStore = await context.Set().SingleAsync(); - - Assert.Equal(customer.Id, customerFromStore.Id); - Assert.Equal("Theon", customerFromStore.Name); - Assert.Equal(customer.ETag, customerFromStore.ETag); - - context.Remove(customerFromStore); - - context.SaveChanges(); - } - } - - [ConditionalFact] - public async Task Etag_will_return_when_content_response_enabled_true() - { - await using var testDatabase = CosmosTestStore.Create("CustomerDemo"); - - var customer = new CustomerWithEtag - { - Id = Guid.NewGuid(), - Name = "Theon", - }; - - using (var context = new CustomerContextWithContentResponse(testDatabase, true)) - { - await context.Database.EnsureCreatedAsync(); - - context.Add(customer); - - await context.SaveChangesAsync(); - } - - using (var context = new CustomerContextWithContentResponse(testDatabase, true)) - { - var customerFromStore = await context.Set().SingleAsync(); - - Assert.Equal(customer.Id, customerFromStore.Id); - Assert.Equal("Theon", customerFromStore.Name); - Assert.Equal(customer.ETag, customerFromStore.ETag); - - context.Remove(customerFromStore); - - context.SaveChanges(); - } - } - - private class CustomerContextWithContentResponse : DbContext - { - private readonly string _connectionString; - private readonly string _name; - private readonly bool _contentResponseOnWriteEnabled; - - public CustomerContextWithContentResponse(CosmosTestStore testStore, bool contentResponseOnWriteEnabled) - { - _connectionString = testStore.ConnectionString; - _name = testStore.Name; - _contentResponseOnWriteEnabled = contentResponseOnWriteEnabled; - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseCosmos(_connectionString, _name, b => b.ApplyConfiguration().ContentResponseOnWriteEnabled(_contentResponseOnWriteEnabled)); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity( - b => - { - b.HasKey(c => c.Id); - b.Property(c => c.ETag).IsETagConcurrency(); - }); - } - - public DbSet Customers { get; set; } - } - - private class CustomerWithEtag - { - public Guid Id { get; set; } - public string Name { get; set; } - public string ETag { get; set; } - } - } -} diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs index 9acafc60c39..62932a1859c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs @@ -27,7 +27,8 @@ public virtual Task Adding_the_same_entity_twice_results_in_DbUpdateException() ctx => ctx.Customers.Add( new Customer { - Id = "1", Name = "CreatedTwice", + Id = "1", + Name = "CreatedTwice", })); } @@ -38,7 +39,8 @@ public virtual Task Updating_then_deleting_the_same_entity_results_in_DbUpdateCo ctx => ctx.Customers.Add( new Customer { - Id = "2", Name = "Added", + Id = "2", + Name = "Added", }), ctx => ctx.Customers.Single(c => c.Id == "2").Name = "Updated", ctx => ctx.Customers.Remove(ctx.Customers.Single(c => c.Id == "2"))); @@ -51,12 +53,81 @@ public virtual Task Updating_then_updating_the_same_entity_results_in_DbUpdateCo ctx => ctx.Customers.Add( new Customer { - Id = "3", Name = "Added", + Id = "3", + Name = "Added", }), ctx => ctx.Customers.Single(c => c.Id == "3").Name = "Updated", ctx => ctx.Customers.Single(c => c.Id == "3").Name = "Updated"); } + [ConditionalFact] + public async Task Etag_will_return_when_content_response_enabled_false() + { + await using var testDatabase = CosmosTestStore.CreateInitialized(DatabaseName); + + var customer = new Customer + { + Id = "4", + Name = "Theon", + }; + + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: false))) + { + await context.Database.EnsureCreatedAsync(); + + context.Add(customer); + + await context.SaveChangesAsync(); + } + + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: false))) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.Equal(customer.Id, customerFromStore.Id); + Assert.Equal("Theon", customerFromStore.Name); + Assert.Equal(customer.ETag, customerFromStore.ETag); + + context.Remove(customerFromStore); + + await context.SaveChangesAsync(); + } + } + + [ConditionalFact] + public async Task Etag_will_return_when_content_response_enabled_true() + { + await using var testDatabase = CosmosTestStore.Create(DatabaseName); + + var customer = new Customer + { + Id = "3", + Name = "Theon", + }; + + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: true))) + { + await context.Database.EnsureCreatedAsync(); + + context.Add(customer); + + await context.SaveChangesAsync(); + } + + await using (var context = new ConcurrencyContext(CreateOptions(testDatabase, enableContentResponseOnWrite: true))) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.Equal(customer.Id, customerFromStore.Id); + Assert.Equal("Theon", customerFromStore.Name); + Assert.Equal(customer.ETag, customerFromStore.ETag); + + context.Remove(customerFromStore); + + await context.SaveChangesAsync(); + } + } + /// /// Runs the two actions with two different contexts and calling /// SaveChanges such that storeChange will succeed and the store will reflect this change, and @@ -137,6 +208,19 @@ protected override void OnModelCreating(ModelBuilder builder) } } + 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) + .EnableDetailedErrors() + .Options; + } + + public class Customer { public string Id { get; set; } From 763c9ef28e976f79899b51790dffcc7ab7f74452 Mon Sep 17 00:00:00 2001 From: Umit Kavala Date: Sun, 15 Nov 2020 22:26:04 +0100 Subject: [PATCH 5/5] Perf: unset EnableContentResponseOnWrite dotnet#22999 removed extra blank lines --- .../Infrastructure/CosmosDbContextOptionsBuilder.cs | 1 - test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs | 1 - .../Extensions/CosmosDbContextOptionsExtensionsTests.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index 6db7fde6da3..cd80c6f81f2 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -129,7 +129,6 @@ public virtual CosmosDbContextOptionsBuilder MaxRequestsPerTcpConnection(int req public virtual CosmosDbContextOptionsBuilder ContentResponseOnWriteEnabled(bool enabled = true) => WithOption(e => e.ContentResponseOnWriteEnabled(Check.NotNull(enabled, nameof(enabled)))); - /// /// Sets an option by cloning the extension used to store the settings. This ensures the builder /// does not modify options that are already in use elsewhere. diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs index 62932a1859c..b685fc89c85 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs @@ -220,7 +220,6 @@ private DbContextOptions CreateOptions(CosmosTestStore testDatabase, bool enable .Options; } - public class Customer { public string Id { get; set; } diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index 4560e68f372..e213b321cad 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -192,7 +192,6 @@ public void Can_create_options_with_max_requests_per_tcp_connection() Assert.Equal(requestLimit, extension.MaxRequestsPerTcpConnection); } - [ConditionalFact] public void Can_create_options_with_content_response_on_write_enabled() {