From e400ad22d5ace0f8db04da34ad4cf9fce0f9c825 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 4 Apr 2022 13:36:01 +0700 Subject: [PATCH 1/4] Saga instead of compensation --- .../ScheduleEmail/ScheduleEmailCommand.cs | 13 ++ .../ScheduleEmailCommandHandler.cs | 30 ++++ .../Mappings/EmailsAutoMapperProfile.cs | 2 + .../OrderCreatedNotificationHandler.cs | 33 ---- .../Properties/AssemblyInfo.cs | 1 + .../Notifications/OrderCreatedNotification.cs | 8 - .../OrdersController.cs | 8 - .../CreateOrder/CreateOrderRequestHandler.cs | 20 +-- .../DeleteOrder/DeleteOrderCommand.cs | 9 ++ .../DeleteOrder/DeleteOrderCommandHandler.cs | 29 ++++ .../Properties/AssemblyInfo.cs | 1 + .../Shop.Tests.Unit/WorkflowTests.cs | 20 ++- .../Shop.Web/Controllers/OrdersController.cs | 23 +++ .../Shop.Web/Properties/AssemblyInfo.cs | 5 + .../Shop.Web/Sagas/CreateOrderSaga.cs | 141 ++++++++++++++++++ ModularMonolith/Shop.Web/Shop.Web.csproj | 1 + ModularMonolith/Shop.Web/Startup.cs | 3 + 17 files changed, 272 insertions(+), 75 deletions(-) create mode 100644 ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommand.cs create mode 100644 ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommandHandler.cs delete mode 100644 ModularMonolith/Communication/Shop.Communication.UseCases/Emails/NotificationHandlers/OrderCreatedNotificationHandler.cs delete mode 100644 ModularMonolith/Order/Shop.Order.Contract/Notifications/OrderCreatedNotification.cs create mode 100644 ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommand.cs create mode 100644 ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs create mode 100644 ModularMonolith/Shop.Web/Controllers/OrdersController.cs create mode 100644 ModularMonolith/Shop.Web/Properties/AssemblyInfo.cs create mode 100644 ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs diff --git a/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommand.cs b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommand.cs new file mode 100644 index 0000000..0cc74ee --- /dev/null +++ b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Shop.Communication.UseCases.Emails.Commands.ScheduleEmail +{ + internal class ScheduleEmailCommand : IRequest + { + public string Address { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + public int UserId { get; set; } + public int OrderId { get; set; } + } +} \ No newline at end of file diff --git a/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommandHandler.cs b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommandHandler.cs new file mode 100644 index 0000000..078c7a2 --- /dev/null +++ b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Commands/ScheduleEmail/ScheduleEmailCommandHandler.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using MediatR; +using Shop.Communication.DataAccess.Interfaces; +using Shop.Communication.Entities; + +namespace Shop.Communication.UseCases.Emails.Commands.ScheduleEmail +{ + internal class ScheduleEmailCommandHandler : AsyncRequestHandler + { + private readonly ICommunicationDbContext _dbContext; + private readonly IMapper _mapper; + + public ScheduleEmailCommandHandler(ICommunicationDbContext dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + protected override async Task Handle(ScheduleEmailCommand request, CancellationToken cancellationToken) + { + var email = _mapper.Map(request); + + _dbContext.Emails.Add(email); + + await _dbContext.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Mappings/EmailsAutoMapperProfile.cs b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Mappings/EmailsAutoMapperProfile.cs index c7f711f..e435583 100644 --- a/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Mappings/EmailsAutoMapperProfile.cs +++ b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/Mappings/EmailsAutoMapperProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using Shop.Communication.Entities; +using Shop.Communication.UseCases.Emails.Commands.ScheduleEmail; using Shop.Communication.UseCases.Emails.Dto; namespace Shop.Communication.UseCases.Emails.Mappings @@ -9,6 +10,7 @@ internal class EmailsAutoMapperProfile : Profile public EmailsAutoMapperProfile() { CreateMap(); + CreateMap(); } } } diff --git a/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/NotificationHandlers/OrderCreatedNotificationHandler.cs b/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/NotificationHandlers/OrderCreatedNotificationHandler.cs deleted file mode 100644 index b9495c4..0000000 --- a/ModularMonolith/Communication/Shop.Communication.UseCases/Emails/NotificationHandlers/OrderCreatedNotificationHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using Shop.Communication.DataAccess.Interfaces; -using Shop.Communication.Entities; -using Shop.Order.Contract.Notifications; - -namespace Shop.Communication.UseCases.Emails.NotificationHandlers -{ - internal class OrderCreatedNotificationHandler : INotificationHandler - { - private readonly ICommunicationDbContext _dbContext; - - public OrderCreatedNotificationHandler(ICommunicationDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task Handle(OrderCreatedNotification notification, CancellationToken cancellationToken) - { - var mail = new Email - { - Address = notification.UserEmail, - Subject = "Order created", - Body = $"Your order {notification.OrderId} created successfully", - OrderId = notification.OrderId, - UserId = notification.UserId - }; - _dbContext.Emails.Add(mail); - await _dbContext.SaveChangesAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/ModularMonolith/Communication/Shop.Communication.UseCases/Properties/AssemblyInfo.cs b/ModularMonolith/Communication/Shop.Communication.UseCases/Properties/AssemblyInfo.cs index fff10bc..53918f6 100644 --- a/ModularMonolith/Communication/Shop.Communication.UseCases/Properties/AssemblyInfo.cs +++ b/ModularMonolith/Communication/Shop.Communication.UseCases/Properties/AssemblyInfo.cs @@ -2,3 +2,4 @@ [assembly: InternalsVisibleTo("Shop.Communication.Contract.Implementation")] [assembly: InternalsVisibleTo("Shop.Communication.Controllers")] +[assembly: InternalsVisibleTo("Shop.Web")] diff --git a/ModularMonolith/Order/Shop.Order.Contract/Notifications/OrderCreatedNotification.cs b/ModularMonolith/Order/Shop.Order.Contract/Notifications/OrderCreatedNotification.cs deleted file mode 100644 index e702adc..0000000 --- a/ModularMonolith/Order/Shop.Order.Contract/Notifications/OrderCreatedNotification.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace Shop.Order.Contract.Notifications -{ - public record OrderCreatedNotification(int OrderId, int UserId, string UserEmail) : INotification - { - } -} \ No newline at end of file diff --git a/ModularMonolith/Order/Shop.Order.Controllers/OrdersController.cs b/ModularMonolith/Order/Shop.Order.Controllers/OrdersController.cs index cd6f904..203a4c5 100644 --- a/ModularMonolith/Order/Shop.Order.Controllers/OrdersController.cs +++ b/ModularMonolith/Order/Shop.Order.Controllers/OrdersController.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using MediatR; using Microsoft.AspNetCore.Mvc; -using Shop.Order.UseCases.Orders.Commands.CreateOrder; using Shop.Order.UseCases.Orders.Dto; using Shop.Order.UseCases.Orders.Queries.GetOrder; @@ -25,12 +24,5 @@ public async Task> Get(int id, CancellationToken token) { return await _mediator.Send(new GetOrderRequest { Id = id }, token); } - - // POST api/orders - [HttpPost] - public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken token) - { - return await _mediator.Send(new CreateOrderRequest { CreateOrderDto = createOrderDto }, token); - } } } diff --git a/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/CreateOrder/CreateOrderRequestHandler.cs b/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/CreateOrder/CreateOrderRequestHandler.cs index c5e6974..9055ce9 100644 --- a/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/CreateOrder/CreateOrderRequestHandler.cs +++ b/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/CreateOrder/CreateOrderRequestHandler.cs @@ -4,7 +4,6 @@ using AutoMapper; using MediatR; using Shop.Framework.UseCases.Interfaces.Services; -using Shop.Order.Contract.Notifications; using Shop.Order.DataAccess.Interfaces; namespace Shop.Order.UseCases.Orders.Commands.CreateOrder @@ -14,18 +13,15 @@ internal class CreateOrderRequestHandler : IRequestHandler Handle(CreateOrderRequest request, CancellationToken cancellationToken) @@ -38,20 +34,6 @@ public async Task Handle(CreateOrderRequest request, CancellationToken canc await _dbContext.SaveChangesAsync(cancellationToken); - try - { - await _publisher.Publish(new OrderCreatedNotification(order.Id, _currentUserService.Id, _currentUserService.Email), cancellationToken); - } - catch (Exception e) //Compensation - { - //log exception - - _dbContext.Orders.Remove(order); - await _dbContext.SaveChangesAsync(cancellationToken); - - throw; - } - return order.Id; } } diff --git a/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommand.cs b/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommand.cs new file mode 100644 index 0000000..46b8012 --- /dev/null +++ b/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommand.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace Shop.Order.UseCases.Orders.Commands.DeleteOrder +{ + internal class DeleteOrderCommand : IRequest + { + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs b/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs new file mode 100644 index 0000000..d165ad4 --- /dev/null +++ b/ModularMonolith/Order/Shop.Order.UseCases/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Shop.Framework.UseCases.Interfaces.Exceptions; +using Shop.Order.DataAccess.Interfaces; + +namespace Shop.Order.UseCases.Orders.Commands.DeleteOrder +{ + internal class DeleteOrderCommandHandler : AsyncRequestHandler + { + private readonly IOrderDbContext _dbContext; + + public DeleteOrderCommandHandler(IOrderDbContext dbContext) + { + _dbContext = dbContext; + } + + protected override async Task Handle(DeleteOrderCommand request, CancellationToken cancellationToken) + { + var order = await _dbContext.Orders.FindAsync(request.Id); + + if (order == null) throw new EntityNotFoundException(); + + _dbContext.Orders.Remove(order); + + await _dbContext.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/ModularMonolith/Order/Shop.Order.UseCases/Properties/AssemblyInfo.cs b/ModularMonolith/Order/Shop.Order.UseCases/Properties/AssemblyInfo.cs index cfbb4df..62e9520 100644 --- a/ModularMonolith/Order/Shop.Order.UseCases/Properties/AssemblyInfo.cs +++ b/ModularMonolith/Order/Shop.Order.UseCases/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Shop.Order.Controllers")] +[assembly: InternalsVisibleTo("Shop.Web")] [assembly: InternalsVisibleTo("Shop.Tests.Unit")] diff --git a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs b/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs index 917d775..8260ca1 100644 --- a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs +++ b/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs @@ -17,14 +17,12 @@ using Shop.Communication.UseCases; using Shop.Emails.Implementation; using Shop.Framework.UseCases.Implementation; -using Shop.Framework.UseCases.Interfaces.Services; using Shop.Order.Contract.Implementation; using Shop.Order.DataAccess.MsSql; using Shop.Order.UseCases; -using Shop.Order.UseCases.Orders.Commands.CreateOrder; using Shop.Order.UseCases.Orders.Dto; using Shop.Utils.Modules; -using Shop.Web.Utils; +using Shop.Web.Sagas; using Xunit; namespace Shop.Tests.Unit @@ -43,13 +41,16 @@ public async Task Should_Create_Order_And_Email() var (orderDbContext, communicationDbContext) = await CreateDatabase(connectionString); - var sender = serviceProvider.GetRequiredService(); + var saga = serviceProvider.GetRequiredService(); var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - var orderId = await sender.Send(new CreateOrderRequest { CreateOrderDto = dto }); + saga.Start(dto); //assert + var orderId = saga.GetResult(); + Assert.NotNull(orderId); + var order = await orderDbContext.Orders.FirstOrDefaultAsync(x => x.Id == orderId); var email = await communicationDbContext.Emails.FirstOrDefaultAsync(x => x.OrderId == orderId); @@ -79,13 +80,16 @@ public async Task Should_Not_Create_Order_And_Email_On_Error() var (orderDbContext, communicationDbContext) = await CreateDatabase(connectionString); - var sender = serviceProvider.GetRequiredService(); + var saga = serviceProvider.GetRequiredService(); var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - await Assert.ThrowsAsync(() => sender.Send(new CreateOrderRequest { CreateOrderDto = dto })); + saga.Start(dto); //assert + var orderId = saga.GetResult(); + Assert.Null(orderId); + var ordersCount = await orderDbContext.Orders.CountAsync(); var emailsCount = await communicationDbContext.Emails.CountAsync(); @@ -160,6 +164,8 @@ private ServiceCollection CreateServiceProvider(IConfiguration configuration) services.RegisterModule(configuration); services.RegisterModule(configuration); + services.AddScoped(); + return services; } diff --git a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs new file mode 100644 index 0000000..58ef548 --- /dev/null +++ b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Shop.Order.UseCases.Orders.Dto; +using Shop.Web.Sagas; + +namespace Shop.Web.Controllers +{ + [Route("api/[controller]")] + [ApiController] + internal class OrdersController : ControllerBase + { + [HttpPost] + public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken token, [FromServices]CreateOrderSaga saga) + { + saga.Start(createOrderDto); + var orderId = saga.GetResult(); + if (orderId == null) throw new Exception("Unable to create order"); + return orderId; + } + } +} diff --git a/ModularMonolith/Shop.Web/Properties/AssemblyInfo.cs b/ModularMonolith/Shop.Web/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..63638f1 --- /dev/null +++ b/ModularMonolith/Shop.Web/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Shop.Tests.Unit")] + + diff --git a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs b/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs new file mode 100644 index 0000000..1a9fd13 --- /dev/null +++ b/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs @@ -0,0 +1,141 @@ +using Appccelerate.StateMachine; +using Appccelerate.StateMachine.Machine; +using MediatR; +using Shop.Communication.UseCases.Emails.Commands.ScheduleEmail; +using Shop.Framework.UseCases.Interfaces.Services; +using Shop.Order.UseCases.Orders.Commands.CreateOrder; +using Shop.Order.UseCases.Orders.Commands.DeleteOrder; +using Shop.Order.UseCases.Orders.Dto; + +namespace Shop.Web.Sagas +{ + internal class CreateOrderSaga + { + private readonly ISender _sender; + private readonly ICurrentUserService _currentUserService; + private readonly PassiveStateMachine _machine; + private CreateOrderDto _dto; + private int _orderId; + private bool _completed; + + private enum States + { + Initial, + OrderCreated, + EmailSend, + Completed, + Failed + } + + private enum Events + { + CreateOrder, + SendEmail, + Completed, + Failed + } + + public CreateOrderSaga(ISender sender, ICurrentUserService currentUserService) + { + _sender = sender; + _currentUserService = currentUserService; + + var builder = new StateMachineDefinitionBuilder(); + + builder + .In(States.Initial) + .On(Events.CreateOrder) + .Goto(States.OrderCreated) + .Execute(OnCreateOrder); + + builder + .In(States.OrderCreated) + .On(Events.SendEmail) + .Goto(States.EmailSend) + .Execute(OnSendEmail); + + builder + .In(States.EmailSend) + .On(Events.Completed) + .Goto(States.Completed) + .Execute(OnCompleted); + + builder + .In(States.OrderCreated) + .On(Events.Failed) + .Goto(States.Failed); + + builder + .In(States.EmailSend) + .On(Events.Failed) + .Goto(States.Failed) + .Execute(DeleteOrder); + + builder + .WithInitialState(States.Initial); + + _machine = builder + .Build() + .CreatePassiveStateMachine(); + + _machine.Start(); + } + + public void Start(CreateOrderDto dto) + { + _dto = dto; + _machine.Fire(Events.CreateOrder); + } + + public int? GetResult() + { + if (_completed) return _orderId; + + return null; + } + + private void OnCreateOrder() + { + try + { + _orderId = _sender.Send(new CreateOrderRequest { CreateOrderDto = _dto }).GetAwaiter().GetResult(); + _machine.Fire(Events.SendEmail); + } + catch + { + _machine.Fire(Events.Failed); + } + } + + private void OnSendEmail() + { + try + { + var scheduleEmailCommand = new ScheduleEmailCommand + { + OrderId = _orderId, + UserId = _currentUserService.Id, + Address = _currentUserService.Email, + Subject = "Order created", + Body = $"Your order {_orderId} created successfully" + }; + _sender.Send(scheduleEmailCommand).Wait(); + _machine.Fire(Events.Completed); + } + catch + { + _machine.Fire(Events.Failed); + } + } + + private void DeleteOrder() + { + _sender.Send(new DeleteOrderCommand { Id = _orderId }).Wait(); + } + + private void OnCompleted() + { + _completed = true; + } + } +} diff --git a/ModularMonolith/Shop.Web/Shop.Web.csproj b/ModularMonolith/Shop.Web/Shop.Web.csproj index 1c1119a..33a1714 100644 --- a/ModularMonolith/Shop.Web/Shop.Web.csproj +++ b/ModularMonolith/Shop.Web/Shop.Web.csproj @@ -9,6 +9,7 @@ + diff --git a/ModularMonolith/Shop.Web/Startup.cs b/ModularMonolith/Shop.Web/Startup.cs index b7f1e6a..d9cd3e6 100644 --- a/ModularMonolith/Shop.Web/Startup.cs +++ b/ModularMonolith/Shop.Web/Startup.cs @@ -17,6 +17,7 @@ using Shop.Emails.Implementation; using Shop.Framework.UseCases.Implementation; using Shop.Utils.Modules; +using Shop.Web.Sagas; namespace Shop.Web { @@ -51,6 +52,8 @@ public void ConfigureServices(IServiceCollection services) services.RegisterModule(Configuration); services.RegisterModule(Configuration); + services.AddScoped(); + var sp = services.BuildServiceProvider(); var requests = sp.GetServices(); From 8a39efcb305c5e460e60fb466d8cef98744f581e Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 4 Apr 2022 14:23:49 +0700 Subject: [PATCH 2/4] Async Saga --- .../Shop.Tests.Unit/WorkflowTests.cs | 4 +- .../Shop.Web/Controllers/OrdersController.cs | 4 +- .../Shop.Web/Sagas/CreateOrderSaga.cs | 38 ++++++++++--------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs b/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs index 8260ca1..483e852 100644 --- a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs +++ b/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs @@ -45,7 +45,7 @@ public async Task Should_Create_Order_And_Email() var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - saga.Start(dto); + await saga.Start(dto, CancellationToken.None); //assert var orderId = saga.GetResult(); @@ -84,7 +84,7 @@ public async Task Should_Not_Create_Order_And_Email_On_Error() var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - saga.Start(dto); + await saga.Start(dto, CancellationToken.None); //assert var orderId = saga.GetResult(); diff --git a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs index 58ef548..0a6b7b9 100644 --- a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs +++ b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs @@ -12,9 +12,9 @@ namespace Shop.Web.Controllers internal class OrdersController : ControllerBase { [HttpPost] - public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken token, [FromServices]CreateOrderSaga saga) + public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken cancellationToken, [FromServices]CreateOrderSaga saga) { - saga.Start(createOrderDto); + await saga.Start(createOrderDto, cancellationToken); var orderId = saga.GetResult(); if (orderId == null) throw new Exception("Unable to create order"); return orderId; diff --git a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs b/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs index 1a9fd13..20ad538 100644 --- a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs +++ b/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs @@ -1,5 +1,7 @@ -using Appccelerate.StateMachine; -using Appccelerate.StateMachine.Machine; +using System.Threading; +using System.Threading.Tasks; +using Appccelerate.StateMachine; +using Appccelerate.StateMachine.AsyncMachine; using MediatR; using Shop.Communication.UseCases.Emails.Commands.ScheduleEmail; using Shop.Framework.UseCases.Interfaces.Services; @@ -13,8 +15,9 @@ internal class CreateOrderSaga { private readonly ISender _sender; private readonly ICurrentUserService _currentUserService; - private readonly PassiveStateMachine _machine; + private readonly AsyncPassiveStateMachine _machine; private CreateOrderDto _dto; + private CancellationToken _cancellationToken; private int _orderId; private bool _completed; @@ -77,14 +80,15 @@ public CreateOrderSaga(ISender sender, ICurrentUserService currentUserService) _machine = builder .Build() .CreatePassiveStateMachine(); - - _machine.Start(); } - public void Start(CreateOrderDto dto) + public async Task Start(CreateOrderDto dto, CancellationToken cancellationToken) { _dto = dto; - _machine.Fire(Events.CreateOrder); + _cancellationToken = cancellationToken; + + await _machine.Start(); + await _machine.Fire(Events.CreateOrder); } public int? GetResult() @@ -94,20 +98,20 @@ public void Start(CreateOrderDto dto) return null; } - private void OnCreateOrder() + private async Task OnCreateOrder() { try { - _orderId = _sender.Send(new CreateOrderRequest { CreateOrderDto = _dto }).GetAwaiter().GetResult(); - _machine.Fire(Events.SendEmail); + _orderId = await _sender.Send(new CreateOrderRequest { CreateOrderDto = _dto }, _cancellationToken); + await _machine.Fire(Events.SendEmail); } catch { - _machine.Fire(Events.Failed); + await _machine.Fire(Events.Failed); } } - private void OnSendEmail() + private async Task OnSendEmail() { try { @@ -119,18 +123,18 @@ private void OnSendEmail() Subject = "Order created", Body = $"Your order {_orderId} created successfully" }; - _sender.Send(scheduleEmailCommand).Wait(); - _machine.Fire(Events.Completed); + await _sender.Send(scheduleEmailCommand, _cancellationToken); + await _machine.Fire(Events.Completed); } catch { - _machine.Fire(Events.Failed); + await _machine.Fire(Events.Failed); } } - private void DeleteOrder() + private async Task DeleteOrder() { - _sender.Send(new DeleteOrderCommand { Id = _orderId }).Wait(); + await _sender.Send(new DeleteOrderCommand { Id = _orderId }, _cancellationToken); } private void OnCompleted() From e8af9c515b15dab28ae549133272adbc18ec0c3c Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 4 Apr 2022 14:49:51 +0700 Subject: [PATCH 3/4] refactor --- .../Shop.Tests.Unit/WorkflowTests.cs | 6 ++--- .../Shop.Web/Controllers/OrdersController.cs | 3 +-- .../Shop.Web/Sagas/CreateOrderSaga.cs | 25 ++++++++----------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs b/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs index 483e852..4c7344f 100644 --- a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs +++ b/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs @@ -45,10 +45,9 @@ public async Task Should_Create_Order_And_Email() var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - await saga.Start(dto, CancellationToken.None); + var orderId = await saga.RunAsync(dto, CancellationToken.None); //assert - var orderId = saga.GetResult(); Assert.NotNull(orderId); var order = await orderDbContext.Orders.FirstOrDefaultAsync(x => x.Id == orderId); @@ -84,10 +83,9 @@ public async Task Should_Not_Create_Order_And_Email_On_Error() var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - await saga.Start(dto, CancellationToken.None); + var orderId = await saga.RunAsync(dto, CancellationToken.None); //assert - var orderId = saga.GetResult(); Assert.Null(orderId); var ordersCount = await orderDbContext.Orders.CountAsync(); diff --git a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs index 0a6b7b9..9a741e9 100644 --- a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs +++ b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs @@ -14,8 +14,7 @@ internal class OrdersController : ControllerBase [HttpPost] public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken cancellationToken, [FromServices]CreateOrderSaga saga) { - await saga.Start(createOrderDto, cancellationToken); - var orderId = saga.GetResult(); + var orderId = await saga.RunAsync(createOrderDto, cancellationToken); if (orderId == null) throw new Exception("Unable to create order"); return orderId; } diff --git a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs b/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs index 20ad538..7b78b39 100644 --- a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs +++ b/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs @@ -49,19 +49,19 @@ public CreateOrderSaga(ISender sender, ICurrentUserService currentUserService) .In(States.Initial) .On(Events.CreateOrder) .Goto(States.OrderCreated) - .Execute(OnCreateOrder); + .Execute(CreateOrderAsync); builder .In(States.OrderCreated) .On(Events.SendEmail) .Goto(States.EmailSend) - .Execute(OnSendEmail); + .Execute(SendEmailAsync); builder .In(States.EmailSend) .On(Events.Completed) .Goto(States.Completed) - .Execute(OnCompleted); + .Execute(Complete); builder .In(States.OrderCreated) @@ -72,7 +72,7 @@ public CreateOrderSaga(ISender sender, ICurrentUserService currentUserService) .In(States.EmailSend) .On(Events.Failed) .Goto(States.Failed) - .Execute(DeleteOrder); + .Execute(DeleteOrderAsync); builder .WithInitialState(States.Initial); @@ -82,23 +82,18 @@ public CreateOrderSaga(ISender sender, ICurrentUserService currentUserService) .CreatePassiveStateMachine(); } - public async Task Start(CreateOrderDto dto, CancellationToken cancellationToken) + public async Task RunAsync(CreateOrderDto dto, CancellationToken cancellationToken) { _dto = dto; _cancellationToken = cancellationToken; await _machine.Start(); await _machine.Fire(Events.CreateOrder); - } - - public int? GetResult() - { - if (_completed) return _orderId; - return null; + return _completed ? _orderId : null; } - private async Task OnCreateOrder() + private async Task CreateOrderAsync() { try { @@ -111,7 +106,7 @@ private async Task OnCreateOrder() } } - private async Task OnSendEmail() + private async Task SendEmailAsync() { try { @@ -132,12 +127,12 @@ private async Task OnSendEmail() } } - private async Task DeleteOrder() + private async Task DeleteOrderAsync() { await _sender.Send(new DeleteOrderCommand { Id = _orderId }, _cancellationToken); } - private void OnCompleted() + private void Complete() { _completed = true; } From 6c1f4194eeb84d06c165ccdb4995b79fe1c2efda Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 29 May 2023 12:16:43 +0700 Subject: [PATCH 4/4] StateMachine --- .../{WorkflowTests.cs => StateMachineTests.cs} | 14 +++++++------- .../Shop.Web/Controllers/OrdersController.cs | 6 +++--- ModularMonolith/Shop.Web/Startup.cs | 4 ++-- .../CreateOrderStateMachine.cs} | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) rename ModularMonolith/Shop.Tests.Unit/{WorkflowTests.cs => StateMachineTests.cs} (93%) rename ModularMonolith/Shop.Web/{Sagas/CreateOrderSaga.cs => StateMachines/CreateOrderStateMachine.cs} (95%) diff --git a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs b/ModularMonolith/Shop.Tests.Unit/StateMachineTests.cs similarity index 93% rename from ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs rename to ModularMonolith/Shop.Tests.Unit/StateMachineTests.cs index 4c7344f..706a857 100644 --- a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs +++ b/ModularMonolith/Shop.Tests.Unit/StateMachineTests.cs @@ -22,12 +22,12 @@ using Shop.Order.UseCases; using Shop.Order.UseCases.Orders.Dto; using Shop.Utils.Modules; -using Shop.Web.Sagas; +using Shop.Web.StateMachines; using Xunit; namespace Shop.Tests.Unit { - public class WorkflowTests + public class StateMachineTests { [Fact] public async Task Should_Create_Order_And_Email() @@ -41,11 +41,11 @@ public async Task Should_Create_Order_And_Email() var (orderDbContext, communicationDbContext) = await CreateDatabase(connectionString); - var saga = serviceProvider.GetRequiredService(); + var stateMachine = serviceProvider.GetRequiredService(); var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - var orderId = await saga.RunAsync(dto, CancellationToken.None); + var orderId = await stateMachine.RunAsync(dto, CancellationToken.None); //assert Assert.NotNull(orderId); @@ -79,11 +79,11 @@ public async Task Should_Not_Create_Order_And_Email_On_Error() var (orderDbContext, communicationDbContext) = await CreateDatabase(connectionString); - var saga = serviceProvider.GetRequiredService(); + var stateMachine = serviceProvider.GetRequiredService(); var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - var orderId = await saga.RunAsync(dto, CancellationToken.None); + var orderId = await stateMachine.RunAsync(dto, CancellationToken.None); //assert Assert.Null(orderId); @@ -162,7 +162,7 @@ private ServiceCollection CreateServiceProvider(IConfiguration configuration) services.RegisterModule(configuration); services.RegisterModule(configuration); - services.AddScoped(); + services.AddScoped(); return services; } diff --git a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs index 9a741e9..59c7b30 100644 --- a/ModularMonolith/Shop.Web/Controllers/OrdersController.cs +++ b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Shop.Order.UseCases.Orders.Dto; -using Shop.Web.Sagas; +using Shop.Web.StateMachines; namespace Shop.Web.Controllers { @@ -12,9 +12,9 @@ namespace Shop.Web.Controllers internal class OrdersController : ControllerBase { [HttpPost] - public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken cancellationToken, [FromServices]CreateOrderSaga saga) + public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken cancellationToken, [FromServices]CreateOrderStateMachine stateMachine) { - var orderId = await saga.RunAsync(createOrderDto, cancellationToken); + var orderId = await stateMachine.RunAsync(createOrderDto, cancellationToken); if (orderId == null) throw new Exception("Unable to create order"); return orderId; } diff --git a/ModularMonolith/Shop.Web/Startup.cs b/ModularMonolith/Shop.Web/Startup.cs index d9cd3e6..015fe0d 100644 --- a/ModularMonolith/Shop.Web/Startup.cs +++ b/ModularMonolith/Shop.Web/Startup.cs @@ -17,7 +17,7 @@ using Shop.Emails.Implementation; using Shop.Framework.UseCases.Implementation; using Shop.Utils.Modules; -using Shop.Web.Sagas; +using Shop.Web.StateMachines; namespace Shop.Web { @@ -52,7 +52,7 @@ public void ConfigureServices(IServiceCollection services) services.RegisterModule(Configuration); services.RegisterModule(Configuration); - services.AddScoped(); + services.AddScoped(); var sp = services.BuildServiceProvider(); diff --git a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs b/ModularMonolith/Shop.Web/StateMachines/CreateOrderStateMachine.cs similarity index 95% rename from ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs rename to ModularMonolith/Shop.Web/StateMachines/CreateOrderStateMachine.cs index 7b78b39..f02fa20 100644 --- a/ModularMonolith/Shop.Web/Sagas/CreateOrderSaga.cs +++ b/ModularMonolith/Shop.Web/StateMachines/CreateOrderStateMachine.cs @@ -9,9 +9,9 @@ using Shop.Order.UseCases.Orders.Commands.DeleteOrder; using Shop.Order.UseCases.Orders.Dto; -namespace Shop.Web.Sagas +namespace Shop.Web.StateMachines { - internal class CreateOrderSaga + internal class CreateOrderStateMachine { private readonly ISender _sender; private readonly ICurrentUserService _currentUserService; @@ -38,7 +38,7 @@ private enum Events Failed } - public CreateOrderSaga(ISender sender, ICurrentUserService currentUserService) + public CreateOrderStateMachine(ISender sender, ICurrentUserService currentUserService) { _sender = sender; _currentUserService = currentUserService;