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/StateMachineTests.cs similarity index 92% rename from ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs rename to ModularMonolith/Shop.Tests.Unit/StateMachineTests.cs index 917d775..706a857 100644 --- a/ModularMonolith/Shop.Tests.Unit/WorkflowTests.cs +++ b/ModularMonolith/Shop.Tests.Unit/StateMachineTests.cs @@ -17,19 +17,17 @@ 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.StateMachines; using Xunit; namespace Shop.Tests.Unit { - public class WorkflowTests + public class StateMachineTests { [Fact] public async Task Should_Create_Order_And_Email() @@ -43,13 +41,15 @@ public async Task Should_Create_Order_And_Email() var (orderDbContext, communicationDbContext) = await CreateDatabase(connectionString); - var sender = serviceProvider.GetRequiredService(); + var stateMachine = serviceProvider.GetRequiredService(); var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - var orderId = await sender.Send(new CreateOrderRequest { CreateOrderDto = dto }); + var orderId = await stateMachine.RunAsync(dto, CancellationToken.None); //assert + 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 +79,15 @@ public async Task Should_Not_Create_Order_And_Email_On_Error() var (orderDbContext, communicationDbContext) = await CreateDatabase(connectionString); - var sender = serviceProvider.GetRequiredService(); + var stateMachine = serviceProvider.GetRequiredService(); var dto = new CreateOrderDto { Items = new[] { new OrderItemDto { Count = 1, ProductId = 1 } } }; //act - await Assert.ThrowsAsync(() => sender.Send(new CreateOrderRequest { CreateOrderDto = dto })); + var orderId = await stateMachine.RunAsync(dto, CancellationToken.None); //assert + Assert.Null(orderId); + var ordersCount = await orderDbContext.Orders.CountAsync(); var emailsCount = await communicationDbContext.Emails.CountAsync(); @@ -160,6 +162,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..59c7b30 --- /dev/null +++ b/ModularMonolith/Shop.Web/Controllers/OrdersController.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Shop.Order.UseCases.Orders.Dto; +using Shop.Web.StateMachines; + +namespace Shop.Web.Controllers +{ + [Route("api/[controller]")] + [ApiController] + internal class OrdersController : ControllerBase + { + [HttpPost] + public async Task> Post([FromBody] CreateOrderDto createOrderDto, CancellationToken cancellationToken, [FromServices]CreateOrderStateMachine stateMachine) + { + 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/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/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..015fe0d 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.StateMachines; 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(); diff --git a/ModularMonolith/Shop.Web/StateMachines/CreateOrderStateMachine.cs b/ModularMonolith/Shop.Web/StateMachines/CreateOrderStateMachine.cs new file mode 100644 index 0000000..f02fa20 --- /dev/null +++ b/ModularMonolith/Shop.Web/StateMachines/CreateOrderStateMachine.cs @@ -0,0 +1,140 @@ +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; +using Shop.Order.UseCases.Orders.Commands.CreateOrder; +using Shop.Order.UseCases.Orders.Commands.DeleteOrder; +using Shop.Order.UseCases.Orders.Dto; + +namespace Shop.Web.StateMachines +{ + internal class CreateOrderStateMachine + { + private readonly ISender _sender; + private readonly ICurrentUserService _currentUserService; + private readonly AsyncPassiveStateMachine _machine; + private CreateOrderDto _dto; + private CancellationToken _cancellationToken; + private int _orderId; + private bool _completed; + + private enum States + { + Initial, + OrderCreated, + EmailSend, + Completed, + Failed + } + + private enum Events + { + CreateOrder, + SendEmail, + Completed, + Failed + } + + public CreateOrderStateMachine(ISender sender, ICurrentUserService currentUserService) + { + _sender = sender; + _currentUserService = currentUserService; + + var builder = new StateMachineDefinitionBuilder(); + + builder + .In(States.Initial) + .On(Events.CreateOrder) + .Goto(States.OrderCreated) + .Execute(CreateOrderAsync); + + builder + .In(States.OrderCreated) + .On(Events.SendEmail) + .Goto(States.EmailSend) + .Execute(SendEmailAsync); + + builder + .In(States.EmailSend) + .On(Events.Completed) + .Goto(States.Completed) + .Execute(Complete); + + builder + .In(States.OrderCreated) + .On(Events.Failed) + .Goto(States.Failed); + + builder + .In(States.EmailSend) + .On(Events.Failed) + .Goto(States.Failed) + .Execute(DeleteOrderAsync); + + builder + .WithInitialState(States.Initial); + + _machine = builder + .Build() + .CreatePassiveStateMachine(); + } + + public async Task RunAsync(CreateOrderDto dto, CancellationToken cancellationToken) + { + _dto = dto; + _cancellationToken = cancellationToken; + + await _machine.Start(); + await _machine.Fire(Events.CreateOrder); + + return _completed ? _orderId : null; + } + + private async Task CreateOrderAsync() + { + try + { + _orderId = await _sender.Send(new CreateOrderRequest { CreateOrderDto = _dto }, _cancellationToken); + await _machine.Fire(Events.SendEmail); + } + catch + { + await _machine.Fire(Events.Failed); + } + } + + private async Task SendEmailAsync() + { + try + { + var scheduleEmailCommand = new ScheduleEmailCommand + { + OrderId = _orderId, + UserId = _currentUserService.Id, + Address = _currentUserService.Email, + Subject = "Order created", + Body = $"Your order {_orderId} created successfully" + }; + await _sender.Send(scheduleEmailCommand, _cancellationToken); + await _machine.Fire(Events.Completed); + } + catch + { + await _machine.Fire(Events.Failed); + } + } + + private async Task DeleteOrderAsync() + { + await _sender.Send(new DeleteOrderCommand { Id = _orderId }, _cancellationToken); + } + + private void Complete() + { + _completed = true; + } + } +}