diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/IIntegrationEventLogService.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs similarity index 60% rename from src/Services/Catalog/Catalog.API/IntegrationEvents/IIntegrationEventLogService.cs rename to src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs index 423a0eb98..ed1f74616 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/IIntegrationEventLogService.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs @@ -1,14 +1,15 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; -namespace Catalog.API.IntegrationEvents +namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services { public interface IIntegrationEventLogService { - Task SaveEventAsync(IntegrationEvent @event); + Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction); Task MarkEventAsPublishedAsync(IntegrationEvent @event); } } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/IntegrationEventLogService.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs similarity index 66% rename from src/Services/Catalog/Catalog.API/IntegrationEvents/IntegrationEventLogService.cs rename to src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs index e9859c5ae..bef74b452 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/IntegrationEventLogService.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs @@ -1,37 +1,39 @@ -using System; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; -using Microsoft.EntityFrameworkCore.Infrastructure; +using System; -namespace Catalog.API.IntegrationEvents +namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services { public class IntegrationEventLogService : IIntegrationEventLogService { private readonly IntegrationEventLogContext _integrationEventLogContext; - private readonly CatalogContext _catalogContext; + private readonly DbConnection _dbConnection; - public IntegrationEventLogService(CatalogContext catalogContext) + public IntegrationEventLogService(DbConnection dbConnection) { - _catalogContext = catalogContext; + _dbConnection = dbConnection?? throw new ArgumentNullException("dbConnection"); _integrationEventLogContext = new IntegrationEventLogContext( new DbContextOptionsBuilder() - .UseSqlServer(catalogContext.Database.GetDbConnection()) + .UseSqlServer(_dbConnection) .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)) .Options); } - public Task SaveEventAsync(IntegrationEvent @event) + public Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction) { + if(transaction == null) + { + throw new ArgumentNullException("transaction", $"A {typeof(DbTransaction).FullName} is required as a pre-requisite to save the event."); + } + var eventLogEntry = new IntegrationEventLogEntry(@event); - // as a constraint this transaction has to be done together with a catalogContext transaction - _integrationEventLogContext.Database.UseTransaction(_catalogContext.Database.CurrentTransaction.GetDbTransaction()); + _integrationEventLogContext.Database.UseTransaction(transaction); _integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); return _integrationEventLogContext.SaveChangesAsync(); diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 7eb19c63a..f4864f2cd 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -1,18 +1,20 @@ -using Catalog.API.IntegrationEvents; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Catalog.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { @@ -22,14 +24,14 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers private readonly CatalogContext _catalogContext; private readonly IOptionsSnapshot _settings; private readonly IEventBus _eventBus; - private readonly IIntegrationEventLogService _integrationEventLogService; + private readonly Func _integrationEventLogServiceFactory; - public CatalogController(CatalogContext Context, IOptionsSnapshot settings, IEventBus eventBus, IIntegrationEventLogService integrationEventLogService) + public CatalogController(CatalogContext Context, IOptionsSnapshot settings, IEventBus eventBus, Func integrationEventLogServiceFactory) { _catalogContext = Context; _settings = settings; _eventBus = eventBus; - _integrationEventLogService = integrationEventLogService; + _integrationEventLogServiceFactory = integrationEventLogServiceFactory; ((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } @@ -161,7 +163,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers //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(); - + var eventLogService = _integrationEventLogServiceFactory(_catalogContext.Database.GetDbConnection()); await strategy.ExecuteAsync(async () => { // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction @@ -172,7 +174,10 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers //Save to EventLog only if product price changed if (raiseProductPriceChangedEvent) - await _integrationEventLogService.SaveEventAsync(priceChangedEvent); + { + + await eventLogService.SaveEventAsync(priceChangedEvent, _catalogContext.Database.CurrentTransaction.GetDbTransaction()); + } transaction.Commit(); } @@ -183,9 +188,9 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers if (raiseProductPriceChangedEvent) { _eventBus.Publish(priceChangedEvent); - await _integrationEventLogService.MarkEventAsPublishedAsync(priceChangedEvent); + await eventLogService.MarkEventAsPublishedAsync(priceChangedEvent); } - + return Ok(); } diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 87986612f..7ebe47b44 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -1,6 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API { - using global::Catalog.API.IntegrationEvents; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -8,12 +7,14 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; + using System.Data.Common; using System.Data.SqlClient; using System.Reflection; @@ -40,14 +41,11 @@ public void ConfigureServices(IServiceCollection services) { - //Using the same SqlConnection for both DbContexts (CatalogContext and IntegrationEventLogContext) - var sqlConnection = new SqlConnection(Configuration["ConnectionString"]); - services.AddDbContext(options => { - options.UseSqlServer(sqlConnection, + options.UseSqlServer(Configuration["ConnectionString"], 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); @@ -58,19 +56,6 @@ //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval }); - services.AddDbContext(options => - { - 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); // Add framework services. @@ -96,8 +81,9 @@ .AllowCredentials()); }); - services.AddTransient(); - + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + var serviceProvider = services.BuildServiceProvider(); var configuration = serviceProvider.GetRequiredService>().Value; services.AddSingleton(new EventBusRabbitMQ(configuration.EventBusConnection)); @@ -105,7 +91,7 @@ services.AddMvc(); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IntegrationEventLogContext integrationEventLogContext) + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //Configure logs @@ -127,7 +113,11 @@ //Seed Data CatalogContextSeed.SeedAsync(app, loggerFactory) .Wait(); - + + var integrationEventLogContext = new IntegrationEventLogContext( + new DbContextOptionsBuilder() + .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API")) + .Options); integrationEventLogContext.Database.Migrate(); } diff --git a/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs b/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs index 57254db1f..8ed11be3a 100644 --- a/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs +++ b/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs @@ -43,7 +43,7 @@ namespace FunctionalTests.Services var itemToModify = basket.Items[2]; var oldPrice = itemToModify.UnitPrice; var newPrice = oldPrice + priceModification; - var pRes = await catalogClient.PostAsync(CatalogScenariosBase.Post.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice), UTF8Encoding.UTF8, "application/json")); + var pRes = await catalogClient.PostAsync(CatalogScenariosBase.Post.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json")); var modifiedCatalogProducts = await GetCatalogAsync(catalogClient); @@ -100,14 +100,11 @@ namespace FunctionalTests.Services return JsonConvert.DeserializeObject>(items); } - private string ChangePrice(BasketItem itemToModify, decimal newPrice) + private string ChangePrice(BasketItem itemToModify, decimal newPrice, PaginatedItemsViewModel catalogProducts) { - var item = new CatalogItem() - { - Id = int.Parse(itemToModify.ProductId), - Price = newPrice - }; - return JsonConvert.SerializeObject(item); + var catalogProduct = catalogProducts.Data.Single(pr => pr.Id == int.Parse(itemToModify.ProductId)); + catalogProduct.Price = newPrice; + return JsonConvert.SerializeObject(catalogProduct); } private CustomerBasket ComposeBasket(string customerId, IEnumerable items)