From 0d78461a082f95467f21af37c93e2e6a456af85e Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sun, 26 Mar 2017 18:00:04 -0700 Subject: [PATCH] Implemented EF Core DB connections resiliency with explicit retries and execution strategy when using multiple DbContexts --- .../Controllers/CatalogController.cs | 16 ++++++++--- src/Services/Catalog/Catalog.API/Startup.cs | 27 ++++++++++++++----- src/Services/Ordering/Ordering.API/Startup.cs | 10 ++++--- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 3cd220657..7eb19c63a 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -158,18 +158,26 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers //Update current product catalogItem = productToUpdate; - // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction - using (var transaction = _catalogContext.Database.BeginTransaction()) + //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): + //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + var strategy = _catalogContext.Database.CreateExecutionStrategy(); + + await strategy.ExecuteAsync(async () => { + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction + using (var transaction = _catalogContext.Database.BeginTransaction()) + { _catalogContext.CatalogItems.Update(catalogItem); await _catalogContext.SaveChangesAsync(); //Save to EventLog only if product price changed - if(raiseProductPriceChangedEvent) + if (raiseProductPriceChangedEvent) await _integrationEventLogService.SaveEventAsync(priceChangedEvent); transaction.Commit(); - } + } + }); + //Publish to Event Bus only if product price changed if (raiseProductPriceChangedEvent) diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 1598eb5a0..87986612f 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using System; using System.Data.SqlClient; using System.Reflection; @@ -39,21 +40,35 @@ public void ConfigureServices(IServiceCollection services) { + //Using the same SqlConnection for both DbContexts (CatalogContext and IntegrationEventLogContext) var sqlConnection = new SqlConnection(Configuration["ConnectionString"]); - services.AddDbContext(c => + services.AddDbContext(options => { - c.UseSqlServer(sqlConnection); + options.UseSqlServer(sqlConnection, + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); // Changing default behavior when client evaluation occurs to throw. // Default in EF Core would be to log a warning when client evaluation is performed. - c.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); + options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval }); - services.AddDbContext(c => + services.AddDbContext(options => { - c.UseSqlServer(sqlConnection, b => b.MigrationsAssembly("Catalog.API")); - c.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); + options.UseSqlServer(sqlConnection, + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + + options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); }); services.Configure(Configuration); diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 3f0840b07..92edf1974 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -52,10 +52,14 @@ services.AddEntityFrameworkSqlServer() .AddDbContext(options => { - options.UseSqlServer(Configuration["ConnectionString"], - sqlop => sqlop.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)); + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); }, - ServiceLifetime.Scoped //DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) + ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) ); services.AddSwaggerGen();