diff --git a/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs b/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs new file mode 100644 index 000000000..d728d83cf --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs @@ -0,0 +1,55 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Behaviors +{ + public class TransactionBehaviour : IPipelineBehavior + { + private readonly ILogger> _logger; + private readonly OrderingContext _dbContext; + + public TransactionBehaviour(OrderingContext dbContext, ILogger> logger) + { + _dbContext = dbContext ?? throw new ArgumentException(nameof(OrderingContext)); + _logger = logger ?? throw new ArgumentException(nameof(ILogger)); + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + TResponse response = default(TResponse); + + try + { + var strategy = _dbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + _logger.LogInformation($"Begin transaction {typeof(TRequest).Name}"); + + await _dbContext.BeginTransactionAsync(); + + response = await next(); + + await _dbContext.CommitTransactionAsync(); + + _logger.LogInformation($"Committed transaction {typeof(TRequest).Name}"); + }); + + return response; + } + catch (Exception) + { + _logger.LogInformation($"Rollback transaction executed {typeof(TRequest).Name}"); + + _dbContext.RollbackTransaction(); + throw; + } + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs index b3c0201b5..602ab9cd5 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore.Storage; 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.BuildingBlocks.IntegrationEventLogEF.Utilities; using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; @@ -17,12 +18,16 @@ namespace Ordering.API.Application.IntegrationEvents private readonly Func _integrationEventLogServiceFactory; private readonly IEventBus _eventBus; private readonly OrderingContext _orderingContext; + private readonly IntegrationEventLogContext _eventLogContext; private readonly IIntegrationEventLogService _eventLogService; - public OrderingIntegrationEventService(IEventBus eventBus, OrderingContext orderingContext, - Func integrationEventLogServiceFactory) + public OrderingIntegrationEventService(IEventBus eventBus, + OrderingContext orderingContext, + IntegrationEventLogContext eventLogContext, + Func integrationEventLogServiceFactory) { _orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext)); + _eventLogContext = eventLogContext ?? throw new ArgumentNullException(nameof(eventLogContext)); _integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory)); _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); _eventLogService = _integrationEventLogServiceFactory(_orderingContext.Database.GetDbConnection()); @@ -37,14 +42,8 @@ namespace Ordering.API.Application.IntegrationEvents private 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()); - }); + await _orderingContext.SaveChangesAsync(); + await _eventLogContext.SaveChangesAsync(); } } } diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs index d0dce865f..d40853999 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -3,6 +3,7 @@ using Autofac.Core; using FluentValidation; using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using Ordering.API.Application.Behaviors; using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent; using Ordering.API.Application.Validations; using Ordering.API.Infrastructure.Behaviors; @@ -53,6 +54,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>)); builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>)); + builder.RegisterGeneric(typeof(TransactionBehaviour<,>)).As(typeof(IPipelineBehavior<,>)); } } diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 564acdfb4..f162b09f8 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -1,12 +1,14 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; using Ordering.Infrastructure; using Ordering.Infrastructure.EntityConfigurations; using System; +using System.Data; using System.Threading; using System.Threading.Tasks; @@ -23,6 +25,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure public DbSet OrderStatus { get; set; } private readonly IMediator _mediator; + private IDbContextTransaction _currentTransaction; private OrderingContext(DbContextOptions options) : base (options) { } @@ -60,7 +63,50 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure var result = await base.SaveChangesAsync(); return true; - } + } + + public async Task BeginTransactionAsync() + { + _currentTransaction = _currentTransaction ?? await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); + } + + public async Task CommitTransactionAsync() + { + try + { + await SaveChangesAsync(); + _currentTransaction?.Commit(); + } + catch + { + RollbackTransaction(); + throw; + } + finally + { + if (_currentTransaction != null) + { + _currentTransaction.Dispose(); + _currentTransaction = null; + } + } + } + + public void RollbackTransaction() + { + try + { + _currentTransaction?.Rollback(); + } + finally + { + if (_currentTransaction != null) + { + _currentTransaction.Dispose(); + _currentTransaction = null; + } + } + } } public class OrderingContextDesignFactory : IDesignTimeDbContextFactory