diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Utilities/ResilientTransaction.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Utilities/ResilientTransaction.cs new file mode 100644 index 000000000..f8227882b --- /dev/null +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Utilities/ResilientTransaction.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities +{ + public class ResilientTransaction + { + private DbContext _context; + private ResilientTransaction(DbContext context) => + _context = context ?? throw new ArgumentNullException(nameof(context)); + + public static ResilientTransaction New (DbContext context) => + new ResilientTransaction(context); + + public async Task ExecuteAsync(Func action) + { + //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 = _context.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + using (var transaction = _context.Database.BeginTransaction()) + { + await action(); + transaction.Commit(); + } + }); + } + } +} diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs new file mode 100644 index 000000000..e35badc64 --- /dev/null +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs @@ -0,0 +1,27 @@ +using Basket.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Basket.API.IntegrationEvents.EventHandling +{ + public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IBasketRepository _repository; + public OrderStartedIntegrationEventHandler(IBasketRepository repository) + { + _repository = repository; + } + + public async Task Handle(OrderStartedIntegrationEvent @event) + { + await _repository.DeleteBasketAsync(@event.UserId.ToString()); + } + } +} + + + diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs new file mode 100644 index 000000000..3b5726e83 --- /dev/null +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -0,0 +1,19 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Basket.API.IntegrationEvents.Events +{ + // Integration Events notes: + // An Event is “something that has happened in the past”, therefore its name has to be + // An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems. + public class OrderStartedIntegrationEvent : IntegrationEvent + { + public string UserId { get; } + + public OrderStartedIntegrationEvent(string userId) => + UserId = userId; + } +} diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index 09ffc5975..11a6ebc97 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -17,6 +17,8 @@ using System; using Microsoft.Extensions.HealthChecks; using System.Threading.Tasks; using Basket.API.Infrastructure.Filters; +using Basket.API.IntegrationEvents.Events; +using Basket.API.IntegrationEvents.EventHandling; namespace Microsoft.eShopOnContainers.Services.Basket.API { @@ -91,6 +93,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API services.AddTransient(); services.AddTransient, ProductPriceChangedIntegrationEventHandler>(); + services.AddTransient, OrderStartedIntegrationEventHandler>(); var serviceProvider = services.BuildServiceProvider(); var configuration = serviceProvider.GetRequiredService>().Value; @@ -117,8 +120,10 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API .UseSwaggerUi(); var catalogPriceHandler = app.ApplicationServices.GetService>(); + var orderStartedHandler = app.ApplicationServices.GetService>(); var eventBus = app.ApplicationServices.GetRequiredService(); eventBus.Subscribe(catalogPriceHandler); + eventBus.Subscribe(orderStartedHandler); } diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index f4864f2cd..83a2de02d 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -15,6 +15,8 @@ using System.Data.Common; using System.Linq; using System.Threading.Tasks; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; +using Catalog.API.IntegrationEvents; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { @@ -23,15 +25,13 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { private readonly CatalogContext _catalogContext; private readonly IOptionsSnapshot _settings; - private readonly IEventBus _eventBus; - private readonly Func _integrationEventLogServiceFactory; + private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; - public CatalogController(CatalogContext Context, IOptionsSnapshot settings, IEventBus eventBus, Func integrationEventLogServiceFactory) + public CatalogController(CatalogContext Context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) { _catalogContext = Context; + _catalogIntegrationEventService = catalogIntegrationEventService; _settings = settings; - _eventBus = eventBus; - _integrationEventLogServiceFactory = integrationEventLogServiceFactory; ((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } @@ -145,51 +145,28 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); if (catalogItem == null) return NotFound(); - - bool raiseProductPriceChangedEvent = false; - IntegrationEvent priceChangedEvent = null; - - if (catalogItem.Price != productToUpdate.Price) raiseProductPriceChangedEvent = true; - - if (raiseProductPriceChangedEvent) // Create event if price has changed - { - var oldPrice = catalogItem.Price; - priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); - } - - //Update current product + var raiseProductPriceChangedEvent = catalogItem.Price != productToUpdate.Price; + var oldPrice = catalogItem.Price; + + // Update current product catalogItem = productToUpdate; + _catalogContext.CatalogItems.Update(catalogItem); - //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 () => + if (raiseProductPriceChangedEvent) // Save and publish integration event if price has changed { + //Create Integration Event to be published through the Event Bus + var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); + // 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) - { - - await eventLogService.SaveEventAsync(priceChangedEvent, _catalogContext.Database.CurrentTransaction.GetDbTransaction()); - } - - transaction.Commit(); - } - }); - - - //Publish to Event Bus only if product price changed - if (raiseProductPriceChangedEvent) - { - _eventBus.Publish(priceChangedEvent); - await eventLogService.MarkEventAsPublishedAsync(priceChangedEvent); + await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); + + // Publish through the Event Bus and mark the saved event as published + await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); } + else // Save updated product + { + await _catalogContext.SaveChangesAsync(); + } return Ok(); } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs new file mode 100644 index 000000000..e6e48c54b --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; +using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; +using System; +using System.Data.Common; +using System.Threading.Tasks; + +namespace Catalog.API.IntegrationEvents +{ + public class CatalogIntegrationEventService : ICatalogIntegrationEventService + { + private readonly Func _integrationEventLogServiceFactory; + private readonly IEventBus _eventBus; + private readonly CatalogContext _catalogContext; + private readonly IIntegrationEventLogService _eventLogService; + + public CatalogIntegrationEventService(IEventBus eventBus, CatalogContext catalogContext, + Func integrationEventLogServiceFactory) + { + _catalogContext = catalogContext ?? throw new ArgumentNullException(nameof(catalogContext)); + _integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory)); + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _eventLogService = _integrationEventLogServiceFactory(_catalogContext.Database.GetDbConnection()); + } + + public async Task PublishThroughEventBusAsync(IntegrationEvent evt) + { + _eventBus.Publish(evt); + await _eventLogService.MarkEventAsPublishedAsync(evt); + } + + public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) + { + //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 + await ResilientTransaction.New(_catalogContext) + .ExecuteAsync(async () => { + // Achieving atomicity between original catalog database operation and the IntegrationEventLog thanks to a local transaction + await _catalogContext.SaveChangesAsync(); + await _eventLogService.SaveEventAsync(evt, _catalogContext.Database.CurrentTransaction.GetDbTransaction()); + }); + } + } +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs new file mode 100644 index 000000000..bb958eeaa --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs @@ -0,0 +1,14 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Catalog.API.IntegrationEvents +{ + public interface ICatalogIntegrationEventService + { + Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt); + Task PublishThroughEventBusAsync(IntegrationEvent evt); + } +} diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 9ebeaf51e..cc913fca2 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -20,6 +20,7 @@ using System.IO; using System.Data.Common; using System.Reflection; + using global::Catalog.API.IntegrationEvents; public class Startup { @@ -97,10 +98,10 @@ }); services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - + sp => (DbConnection c) => new IntegrationEventLogService(c)); var serviceProvider = services.BuildServiceProvider(); var configuration = serviceProvider.GetRequiredService>().Value; + services.AddTransient(); services.AddSingleton(new EventBusRabbitMQ(configuration.EventBusConnection)); } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs index 1ac863b1e..ce55fd0c9 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs @@ -1,6 +1,8 @@ using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.Extensions.Logging; +using Ordering.API.IntegrationEvents; +using Ordering.API.IntegrationEvents.Events; using Ordering.Domain.Events; using System; using System.Threading.Tasks; @@ -11,10 +13,15 @@ namespace Ordering.API.Application.DomainEventHandlers.BuyerAndPaymentMethodVeri : IAsyncNotificationHandler { private readonly IOrderRepository _orderRepository; - private readonly ILoggerFactory _logger; - public UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler(IOrderRepository orderRepository, ILoggerFactory logger) + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + private readonly ILoggerFactory _logger; + + public UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler( + IOrderRepository orderRepository, ILoggerFactory logger, + IOrderingIntegrationEventService orderingIntegrationEventService) { _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); + _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -26,12 +33,20 @@ namespace Ordering.API.Application.DomainEventHandlers.BuyerAndPaymentMethodVeri var orderToUpdate = await _orderRepository.GetAsync(buyerPaymentMethodVerifiedEvent.OrderId); orderToUpdate.SetBuyerId(buyerPaymentMethodVerifiedEvent.Buyer.Id); orderToUpdate.SetPaymentId(buyerPaymentMethodVerifiedEvent.Payment.Id); - - await _orderRepository.UnitOfWork - .SaveEntitiesAsync(); - + + var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(buyerPaymentMethodVerifiedEvent.Buyer.IdentityGuid); + + // Using a local transaction to achieve atomicity between original Ordering database operation and + // the IntegrationEventLog. Only saving event if order has been successfully persisted to db + await _orderingIntegrationEventService + .SaveEventAndOrderingContextChangesAsync(orderStartedIntegrationEvent); + + // Publish ordering integration event and mark it as published + await _orderingIntegrationEventService + .PublishThroughEventBusAsync(orderStartedIntegrationEvent); + _logger.CreateLogger(nameof(UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler)) - .LogTrace($"Order with Id: {buyerPaymentMethodVerifiedEvent.OrderId} has been successfully updated with a payment method id: { buyerPaymentMethodVerifiedEvent.Payment.Id }"); + .LogTrace($"Order with Id: {buyerPaymentMethodVerifiedEvent.OrderId} has been successfully updated with a payment method id: { buyerPaymentMethodVerifiedEvent.Payment.Id }"); } - } + } } diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs index 4135be636..4f38dd59e 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -31,7 +31,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof builder.RegisterAssemblyTypes(typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).GetTypeInfo().Assembly) .As(o => o.GetInterfaces() .Where(i => i.IsClosedTypeOf(typeof(IAsyncNotificationHandler<>))) - .Select(i => new KeyedService("IAsyncNotificationHandler", i))).AsImplementedInterfaces(); + .Select(i => new KeyedService("IAsyncNotificationHandler", i))) + .AsImplementedInterfaces(); builder .RegisterAssemblyTypes(typeof(CreateOrderCommandValidator).GetTypeInfo().Assembly) diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.Designer.cs b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.Designer.cs new file mode 100644 index 000000000..65b6a6e05 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.Designer.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + +namespace Ordering.API.Infrastructure.IntegrationEventMigrations +{ + [DbContext(typeof(IntegrationEventLogContext))] + [Migration("20170330131634_IntegrationEventInitial")] + partial class IntegrationEventInitial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.IntegrationEventLogEntry", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd(); + + b.Property("Content") + .IsRequired(); + + b.Property("CreationTime"); + + b.Property("EventTypeName") + .IsRequired(); + + b.Property("State"); + + b.Property("TimesSent"); + + b.HasKey("EventId"); + + b.ToTable("IntegrationEventLog"); + }); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs new file mode 100644 index 000000000..9830ebf7b --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ordering.API.Infrastructure.IntegrationEventMigrations +{ + public partial class IntegrationEventInitial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "IntegrationEventLog", + columns: table => new + { + EventId = table.Column(nullable: false), + Content = table.Column(nullable: false), + CreationTime = table.Column(nullable: false), + EventTypeName = table.Column(nullable: false), + State = table.Column(nullable: false), + TimesSent = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IntegrationEventLog", x => x.EventId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "IntegrationEventLog"); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextModelSnapshot.cs b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextModelSnapshot.cs new file mode 100644 index 000000000..29bd24729 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextModelSnapshot.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + +namespace Ordering.API.Infrastructure.IntegrationEventMigrations +{ + [DbContext(typeof(IntegrationEventLogContext))] + partial class IntegrationEventLogContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.IntegrationEventLogEntry", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd(); + + b.Property("Content") + .IsRequired(); + + b.Property("CreationTime"); + + b.Property("EventTypeName") + .IsRequired(); + + b.Property("State"); + + b.Property("TimesSent"); + + b.HasKey("EventId"); + + b.ToTable("IntegrationEventLog"); + }); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs new file mode 100644 index 000000000..8184483e5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -0,0 +1,19 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.IntegrationEvents.Events +{ + // Integration Events notes: + // An Event is “something that has happened in the past”, therefore its name has to be + // An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems. + public class OrderStartedIntegrationEvent : IntegrationEvent + { + public string UserId { get; } + + public OrderStartedIntegrationEvent(string userId) => + UserId = userId; + } +} diff --git a/src/Services/Ordering/Ordering.API/IntegrationEvents/IOrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/IntegrationEvents/IOrderingIntegrationEventService.cs new file mode 100644 index 000000000..6538afbfd --- /dev/null +++ b/src/Services/Ordering/Ordering.API/IntegrationEvents/IOrderingIntegrationEventService.cs @@ -0,0 +1,14 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.IntegrationEvents +{ + public interface IOrderingIntegrationEventService + { + Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt); + Task PublishThroughEventBusAsync(IntegrationEvent evt); + } +} diff --git a/src/Services/Ordering/Ordering.API/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/IntegrationEvents/OrderingIntegrationEventService.cs new file mode 100644 index 000000000..810c07bc9 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/IntegrationEvents/OrderingIntegrationEventService.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +using System; +using System.Data.Common; +using System.Threading.Tasks; + +namespace Ordering.API.IntegrationEvents +{ + public class OrderingIntegrationEventService : IOrderingIntegrationEventService + { + private readonly Func _integrationEventLogServiceFactory; + private readonly IEventBus _eventBus; + private readonly OrderingContext _orderingContext; + private readonly IIntegrationEventLogService _eventLogService; + + public OrderingIntegrationEventService (IEventBus eventBus, OrderingContext orderingContext, + Func integrationEventLogServiceFactory) + { + _orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext)); + _integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory)); + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _eventLogService = _integrationEventLogServiceFactory(_orderingContext.Database.GetDbConnection()); + } + + public async Task PublishThroughEventBusAsync(IntegrationEvent evt) + { + _eventBus.Publish(evt); + await _eventLogService.MarkEventAsPublishedAsync(evt); + } + + public async Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt) + { + //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 + await ResilientTransaction.New(_orderingContext) + .ExecuteAsync(async () => { + // Achieving atomicity between original ordering database operation and the IntegrationEventLog thanks to a local transaction + await _orderingContext.SaveChangesAsync(); + await _eventLogService.SaveEventAsync(evt, _orderingContext.Database.CurrentTransaction.GetDbTransaction()); + }); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index 5d9f6b258..6eed3a13c 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -23,6 +23,9 @@ + + + @@ -38,16 +41,18 @@ + + - + - - + + @@ -73,4 +78,9 @@ + + + + + diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index e7a6af682..6b120eb78 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -4,6 +4,7 @@ using Autofac; using Autofac.Extensions.DependencyInjection; using global::Ordering.API.Infrastructure.Middlewares; + using global::Ordering.API.IntegrationEvents; using Infrastructure; using Infrastructure.Auth; using Infrastructure.AutofacModules; @@ -12,12 +13,17 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; + using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Ordering.Infrastructure; using System; + using System.Data.Common; using System.Reflection; public class Startup @@ -55,7 +61,7 @@ { checks.AddSqlCheck("OrderingDb", Configuration["ConnectionString"]); }); - + services.AddEntityFrameworkSqlServer() .AddDbContext(options => { @@ -95,7 +101,11 @@ // Add application services. services.AddSingleton(); services.AddTransient(); - + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + var serviceProvider = services.BuildServiceProvider(); + services.AddTransient(); + services.AddSingleton(new EventBusRabbitMQ(Configuration["EventBusConnection"])); services.AddOptions(); //configure autofac @@ -126,6 +136,12 @@ .UseSwaggerUi(); OrderingContextSeed.SeedAsync(app).Wait(); + + var integrationEventLogContext = new IntegrationEventLogContext( + new DbContextOptionsBuilder() + .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Ordering.API")) + .Options); + integrationEventLogContext.Database.Migrate(); } protected virtual void ConfigureAuth(IApplicationBuilder app) diff --git a/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj b/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj index 4ec458dbd..bb756e552 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj +++ b/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj @@ -16,8 +16,9 @@ - - + + + diff --git a/src/Web/WebMVC/Controllers/OrderController.cs b/src/Web/WebMVC/Controllers/OrderController.cs index 104ae9f9b..2755282a3 100644 --- a/src/Web/WebMVC/Controllers/OrderController.cs +++ b/src/Web/WebMVC/Controllers/OrderController.cs @@ -45,9 +45,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers var user = _appUserParser.Parse(HttpContext.User); await _orderSvc.CreateOrder(model); - //Empty basket for current user. - await _basketSvc.CleanBasket(user); - //Redirect to historic list. return RedirectToAction("Index"); } diff --git a/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts b/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts index 72114631f..8f8a10eb0 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts @@ -18,7 +18,7 @@ export class OrdersNewComponent implements OnInit { private errorReceived: Boolean; private order: IOrder; - constructor(private service: OrdersService, fb: FormBuilder, private router: Router, private basketEvents: BasketWrapperService) { + constructor(private service: OrdersService, fb: FormBuilder, private router: Router) { // Obtain user profile information this.order = service.mapBasketAndIdentityInfoNewOrder(); this.newOrderForm = fb.group({ @@ -54,10 +54,7 @@ export class OrdersNewComponent implements OnInit { return Observable.throw(errMessage); }) .subscribe(res => { - // this will emit an observable. Basket service is subscribed to this observable, and will react deleting the basket for the current user. - this.basketEvents.orderCreated(); - - this.router.navigate(['orders']); + this.router.navigate(['orders']); }); this.errorReceived = false; this.isOrderProcessing = true;