From 89816b7641e1c06f869d890c0c60b1cb498d3dd5 Mon Sep 17 00:00:00 2001 From: Umit Kavala Date: Mon, 16 Nov 2020 06:47:07 +0100 Subject: [PATCH] Perf: unset EnableContentResponseOnWrite Fixes #22999 --- .../CosmosDbContextOptionsBuilder.cs | 9 ++ .../Internal/CosmosDbOptionExtension.cs | 25 ++++++ .../Internal/CosmosSingletonOptions.cs | 13 ++- .../Internal/ICosmosSingletonOptions.cs | 8 ++ .../Storage/Internal/CosmosClientWrapper.cs | 9 +- .../CosmosConcurrencyTest.cs | 89 ++++++++++++++++++- .../CosmosDbContextOptionsExtensionsTests.cs | 16 ++++ 7 files changed, 163 insertions(+), 6 deletions(-) diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index c8c42564b38..cd80c6f81f2 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -120,6 +120,15 @@ 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 = 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/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..6d969cc5813 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 @@ -416,6 +418,7 @@ public virtual async Task DeleteItemOnceAsync( { var entry = parameters.Entry; var items = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); + var itemRequestOptions = CreateItemRequestOptions(entry); var partitionKey = CreatePartitionKey(entry); @@ -427,7 +430,7 @@ public virtual async Task DeleteItemOnceAsync( return response.StatusCode == HttpStatusCode.NoContent; } - private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry) + private ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry) { var etagProperty = entry.EntityType.GetETagProperty(); if (etagProperty == null) @@ -442,7 +445,9 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry) etag = converter.ConvertToProvider(etag); } - return new ItemRequestOptions { IfMatchEtag = (string)etag }; + var enabledContentResponse = _enableContentResponseOnWrite ?? entry.EntityType.FindProperty(StoreKeyConvention.JObjectPropertyName)?.ValueGenerated == ValueGenerated.OnAddOrUpdate; + + return new ItemRequestOptions { IfMatchEtag = (string)etag, EnableContentResponseOnWrite = enabledContentResponse }; } private static PartitionKey CreatePartitionKey(IUpdateEntry entry) diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs index 9acafc60c39..b685fc89c85 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,18 @@ 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; } diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index 311d18b9ada..e213b321cad 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -191,5 +191,21 @@ 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); + } } } +