From 29d1497a3f45690ddff97b10d41cdd9bebddc79b Mon Sep 17 00:00:00 2001 From: Eric Fleming Date: Fri, 15 Nov 2019 13:36:51 -0500 Subject: [PATCH] Mediatr example (#325) * Updates based on documentation * Getting the build passing * Getting app functioning * A few cleanups to confirm it's working as expected * Fixing functional tests * Updating dockerfile for 3.0 * Functional Tests now run sequentially * Updating to latest version of moq * Adding migration for post 3.0 upgrades * Removing commented out lines * Moving address and catalogitemordered configuration in to classes that own them * Installing MediatR nuget packages * Configure MediatR in Startup * Creating GetMyOrders MediatR query and handler * Adding GetOrderDetails MediatR handler * Refactoring out default values * Added tests for GetOrderDetails mediator handler * Defaulting values on Models for now * Removing some spaces * Splitting files * Splitting out the GetOrderDetails files * Adding test for GetMyOrders * restructuing folders * Using constant --- src/Web/Controllers/OrderController.cs | 60 ++++--------------- src/Web/Features/MyOrders/GetMyOrders.cs | 16 +++++ .../Features/MyOrders/GetMyOrdersHandler.cs | 43 +++++++++++++ .../Features/OrderDetails/GetOrderDetails.cs | 17 ++++++ .../OrderDetails/GetOrderDetailsHandler.cs | 47 +++++++++++++++ src/Web/Startup.cs | 15 ++--- src/Web/ViewModels/OrderItemViewModel.cs | 7 +-- src/Web/ViewModels/OrderViewModel.cs | 10 ++-- src/Web/Web.csproj | 3 + .../OrdersTests/GetMyOrders_Should.cs | 38 ++++++++++++ .../OrdersTests/GetOrderDetails_Should.cs | 50 ++++++++++++++++ tests/UnitTests/UnitTests.csproj | 1 + 12 files changed, 240 insertions(+), 67 deletions(-) create mode 100644 src/Web/Features/MyOrders/GetMyOrders.cs create mode 100644 src/Web/Features/MyOrders/GetMyOrdersHandler.cs create mode 100644 src/Web/Features/OrderDetails/GetOrderDetails.cs create mode 100644 src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs create mode 100644 tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders_Should.cs create mode 100644 tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails_Should.cs diff --git a/src/Web/Controllers/OrderController.cs b/src/Web/Controllers/OrderController.cs index b60cd32..c876fbf 100644 --- a/src/Web/Controllers/OrderController.cs +++ b/src/Web/Controllers/OrderController.cs @@ -1,9 +1,8 @@ -using Microsoft.AspNetCore.Authorization; +using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Microsoft.eShopWeb.Web.ViewModels; -using System.Linq; +using Microsoft.eShopWeb.Web.Features.MyOrders; +using Microsoft.eShopWeb.Web.Features.OrderDetails; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Controllers @@ -13,66 +12,31 @@ namespace Microsoft.eShopWeb.Web.Controllers [Route("[controller]/[action]")] public class OrderController : Controller { - private readonly IOrderRepository _orderRepository; + private readonly IMediator _mediator; - public OrderController(IOrderRepository orderRepository) + public OrderController(IMediator mediator) { - _orderRepository = orderRepository; + _mediator = mediator; } [HttpGet()] public async Task MyOrders() { - var orders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name)); + var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name)); - var viewModel = orders - .Select(o => new OrderViewModel() - { - OrderDate = o.OrderDate, - OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel() - { - Discount = 0, - PictureUrl = oi.ItemOrdered.PictureUri, - ProductId = oi.ItemOrdered.CatalogItemId, - ProductName = oi.ItemOrdered.ProductName, - UnitPrice = oi.UnitPrice, - Units = oi.Units - }).ToList(), - OrderNumber = o.Id, - ShippingAddress = o.ShipToAddress, - Status = "Pending", - Total = o.Total() - - }); return View(viewModel); } [HttpGet("{orderId}")] public async Task Detail(int orderId) { - var customerOrders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name)); - var order = customerOrders.FirstOrDefault(o => o.Id == orderId); - if (order == null) + var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId)); + + if (viewModel == null) { return BadRequest("No such order found for this user."); } - var viewModel = new OrderViewModel() - { - OrderDate = order.OrderDate, - OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel() - { - Discount = 0, - PictureUrl = oi.ItemOrdered.PictureUri, - ProductId = oi.ItemOrdered.CatalogItemId, - ProductName = oi.ItemOrdered.ProductName, - UnitPrice = oi.UnitPrice, - Units = oi.Units - }).ToList(), - OrderNumber = order.Id, - ShippingAddress = order.ShipToAddress, - Status = "Pending", - Total = order.Total() - }; + return View(viewModel); } } diff --git a/src/Web/Features/MyOrders/GetMyOrders.cs b/src/Web/Features/MyOrders/GetMyOrders.cs new file mode 100644 index 0000000..b99a9da --- /dev/null +++ b/src/Web/Features/MyOrders/GetMyOrders.cs @@ -0,0 +1,16 @@ +using MediatR; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Collections.Generic; + +namespace Microsoft.eShopWeb.Web.Features.MyOrders +{ + public class GetMyOrders : IRequest> + { + public string UserName { get; set; } + + public GetMyOrders(string userName) + { + UserName = userName; + } + } +} diff --git a/src/Web/Features/MyOrders/GetMyOrdersHandler.cs b/src/Web/Features/MyOrders/GetMyOrdersHandler.cs new file mode 100644 index 0000000..60e8eda --- /dev/null +++ b/src/Web/Features/MyOrders/GetMyOrdersHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Specifications; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Features.MyOrders +{ + public class GetMyOrdersHandler : IRequestHandler> + { + private readonly IOrderRepository _orderRepository; + + public GetMyOrdersHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task> Handle(GetMyOrders request, CancellationToken cancellationToken) + { + var specification = new CustomerOrdersWithItemsSpecification(request.UserName); + var orders = await _orderRepository.ListAsync(specification); + + return orders.Select(o => new OrderViewModel + { + OrderDate = o.OrderDate, + OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel() + { + PictureUrl = oi.ItemOrdered.PictureUri, + ProductId = oi.ItemOrdered.CatalogItemId, + ProductName = oi.ItemOrdered.ProductName, + UnitPrice = oi.UnitPrice, + Units = oi.Units + }).ToList(), + OrderNumber = o.Id, + ShippingAddress = o.ShipToAddress, + Total = o.Total() + }); + } + } +} diff --git a/src/Web/Features/OrderDetails/GetOrderDetails.cs b/src/Web/Features/OrderDetails/GetOrderDetails.cs new file mode 100644 index 0000000..4f36ff2 --- /dev/null +++ b/src/Web/Features/OrderDetails/GetOrderDetails.cs @@ -0,0 +1,17 @@ +using MediatR; +using Microsoft.eShopWeb.Web.ViewModels; + +namespace Microsoft.eShopWeb.Web.Features.OrderDetails +{ + public class GetOrderDetails : IRequest + { + public string UserName { get; set; } + public int OrderId { get; set; } + + public GetOrderDetails(string userName, int orderId) + { + UserName = userName; + OrderId = orderId; + } + } +} diff --git a/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs b/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs new file mode 100644 index 0000000..b715a57 --- /dev/null +++ b/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Specifications; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Features.OrderDetails +{ + public class GetOrderDetailsHandler : IRequestHandler + { + private readonly IOrderRepository _orderRepository; + + public GetOrderDetailsHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task Handle(GetOrderDetails request, CancellationToken cancellationToken) + { + var customerOrders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(request.UserName)); + var order = customerOrders.FirstOrDefault(o => o.Id == request.OrderId); + + if (order == null) + { + return null; + } + + return new OrderViewModel + { + OrderDate = order.OrderDate, + OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel + { + PictureUrl = oi.ItemOrdered.PictureUri, + ProductId = oi.ItemOrdered.CatalogItemId, + ProductName = oi.ItemOrdered.ProductName, + UnitPrice = oi.UnitPrice, + Units = oi.Units + }).ToList(), + OrderNumber = order.Id, + ShippingAddress = order.ShipToAddress, + Total = order.Total() + }; + } + } +} diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index 0335d62..37077fd 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -1,4 +1,5 @@ using Ardalis.ListStartupServices; +using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; @@ -81,7 +82,9 @@ namespace Microsoft.eShopWeb.Web ConfigureCookieSettings(services); CreateIdentityIfNotCreated(services); - + + services.AddMediatR(typeof(BasketViewModelService).Assembly); + services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); services.AddScoped(); services.AddScoped(); @@ -109,13 +112,12 @@ namespace Microsoft.eShopWeb.Web options.Conventions.Add(new RouteTokenTransformerConvention( new SlugifyParameterTransformer())); - }); - services.AddControllersWithViews(); + }); services.AddRazorPages(options => { options.Conventions.AuthorizePage("/Basket/Checkout"); }); - + services.AddControllersWithViews(); services.AddControllers(); services.AddHttpContextAccessor(); @@ -207,14 +209,13 @@ namespace Microsoft.eShopWeb.Web app.UseHsts(); } - app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); - + + app.UseHttpsRedirection(); app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); - // Enable middleware to serve generated Swagger as a JSON endpoint. app.UseSwagger(); diff --git a/src/Web/ViewModels/OrderItemViewModel.cs b/src/Web/ViewModels/OrderItemViewModel.cs index ae15fe2..b45e28b 100644 --- a/src/Web/ViewModels/OrderItemViewModel.cs +++ b/src/Web/ViewModels/OrderItemViewModel.cs @@ -3,15 +3,10 @@ public class OrderItemViewModel { public int ProductId { get; set; } - public string ProductName { get; set; } - public decimal UnitPrice { get; set; } - - public decimal Discount { get; set; } - + public decimal Discount => 0; public int Units { get; set; } - public string PictureUrl { get; set; } } } diff --git a/src/Web/ViewModels/OrderViewModel.cs b/src/Web/ViewModels/OrderViewModel.cs index e4cdad1..8f8f279 100644 --- a/src/Web/ViewModels/OrderViewModel.cs +++ b/src/Web/ViewModels/OrderViewModel.cs @@ -6,15 +6,13 @@ namespace Microsoft.eShopWeb.Web.ViewModels { public class OrderViewModel { + private const string DEFAULT_STATUS = "Pending"; + public int OrderNumber { get; set; } public DateTimeOffset OrderDate { get; set; } public decimal Total { get; set; } - public string Status { get; set; } - - public Address ShippingAddress { get; set; } - + public string Status => DEFAULT_STATUS; + public Address ShippingAddress { get; set; } public List OrderItems { get; set; } = new List(); - } - } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 7bcfd3e..87f2a17 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -14,6 +14,9 @@ + + + diff --git a/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders_Should.cs b/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders_Should.cs new file mode 100644 index 0000000..e5acc76 --- /dev/null +++ b/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders_Should.cs @@ -0,0 +1,38 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Web.Features.MyOrders; +using Moq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests +{ + public class GetMyOrders_Should + { + private readonly Mock _mockOrderRepository; + + public GetMyOrders_Should() + { + var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); + var address = new Address(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()); + Order order = new Order("buyerId", address, new List { item }); + + _mockOrderRepository = new Mock(); + _mockOrderRepository.Setup(x => x.ListAsync(It.IsAny>())).ReturnsAsync(new List { order }); + } + + [Fact] + public async Task NotReturnNull_If_OrdersArePresent() + { + var request = new GetMyOrders("SomeUserName"); + + var handler = new GetMyOrdersHandler(_mockOrderRepository.Object); + + var result = await handler.Handle(request, CancellationToken.None); + + Assert.NotNull(result); + } + } +} diff --git a/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails_Should.cs b/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails_Should.cs new file mode 100644 index 0000000..a151159 --- /dev/null +++ b/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails_Should.cs @@ -0,0 +1,50 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Web.Features.OrderDetails; +using Moq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests +{ + public class GetOrderDetails_Should + { + private readonly Mock _mockOrderRepository; + + public GetOrderDetails_Should() + { + var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); + var address = new Address(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()); + Order order = new Order("buyerId", address, new List { item }); + + _mockOrderRepository = new Mock(); + _mockOrderRepository.Setup(x => x.ListAsync(It.IsAny>())).ReturnsAsync(new List { order }); + } + + [Fact] + public async Task NotBeNull_If_Order_Exists() + { + var request = new GetOrderDetails("SomeUserName", 0); + + var handler = new GetOrderDetailsHandler(_mockOrderRepository.Object); + + var result = await handler.Handle(request, CancellationToken.None); + + Assert.NotNull(result); + } + + [Fact] + public async Task BeNull_If_Order_Not_found() + { + var request = new GetOrderDetails("SomeUserName", 100); + + var handler = new GetOrderDetailsHandler(_mockOrderRepository.Object); + + var result = await handler.Handle(request, CancellationToken.None); + + Assert.Null(result); + } + } +} diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index bc747eb..8727af1 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -23,6 +23,7 @@ +