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
This commit is contained in:
Eric Fleming
2019-11-15 13:36:51 -05:00
committed by Steve Smith
parent 1bae9e68d9
commit 29d1497a3f
12 changed files with 240 additions and 67 deletions

View File

@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Authorization; using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Web.Features.MyOrders;
using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.Features.OrderDetails;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Controllers namespace Microsoft.eShopWeb.Web.Controllers
@@ -13,66 +12,31 @@ namespace Microsoft.eShopWeb.Web.Controllers
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class OrderController : Controller 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()] [HttpGet()]
public async Task<IActionResult> MyOrders() public async Task<IActionResult> 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); return View(viewModel);
} }
[HttpGet("{orderId}")] [HttpGet("{orderId}")]
public async Task<IActionResult> Detail(int orderId) public async Task<IActionResult> Detail(int orderId)
{ {
var customerOrders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name)); var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId));
var order = customerOrders.FirstOrDefault(o => o.Id == orderId);
if (order == null) if (viewModel == null)
{ {
return BadRequest("No such order found for this user."); 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); return View(viewModel);
} }
} }

View File

@@ -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<IEnumerable<OrderViewModel>>
{
public string UserName { get; set; }
public GetMyOrders(string userName)
{
UserName = userName;
}
}
}

View File

@@ -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<GetMyOrders, IEnumerable<OrderViewModel>>
{
private readonly IOrderRepository _orderRepository;
public GetMyOrdersHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<IEnumerable<OrderViewModel>> 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()
});
}
}
}

View File

@@ -0,0 +1,17 @@
using MediatR;
using Microsoft.eShopWeb.Web.ViewModels;
namespace Microsoft.eShopWeb.Web.Features.OrderDetails
{
public class GetOrderDetails : IRequest<OrderViewModel>
{
public string UserName { get; set; }
public int OrderId { get; set; }
public GetOrderDetails(string userName, int orderId)
{
UserName = userName;
OrderId = orderId;
}
}
}

View File

@@ -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<GetOrderDetails, OrderViewModel>
{
private readonly IOrderRepository _orderRepository;
public GetOrderDetailsHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<OrderViewModel> 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()
};
}
}
}

View File

@@ -1,4 +1,5 @@
using Ardalis.ListStartupServices; using Ardalis.ListStartupServices;
using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@@ -81,7 +82,9 @@ namespace Microsoft.eShopWeb.Web
ConfigureCookieSettings(services); ConfigureCookieSettings(services);
CreateIdentityIfNotCreated(services); CreateIdentityIfNotCreated(services);
services.AddMediatR(typeof(BasketViewModelService).Assembly);
services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>));
services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>(); services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();
services.AddScoped<IBasketService, BasketService>(); services.AddScoped<IBasketService, BasketService>();
@@ -109,13 +112,12 @@ namespace Microsoft.eShopWeb.Web
options.Conventions.Add(new RouteTokenTransformerConvention( options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer())); new SlugifyParameterTransformer()));
}); });
services.AddControllersWithViews();
services.AddRazorPages(options => services.AddRazorPages(options =>
{ {
options.Conventions.AuthorizePage("/Basket/Checkout"); options.Conventions.AuthorizePage("/Basket/Checkout");
}); });
services.AddControllersWithViews();
services.AddControllers(); services.AddControllers();
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
@@ -207,14 +209,13 @@ namespace Microsoft.eShopWeb.Web
app.UseHsts(); app.UseHsts();
} }
app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseHttpsRedirection();
app.UseCookiePolicy(); app.UseCookiePolicy();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
// Enable middleware to serve generated Swagger as a JSON endpoint. // Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger(); app.UseSwagger();

View File

@@ -3,15 +3,10 @@
public class OrderItemViewModel public class OrderItemViewModel
{ {
public int ProductId { get; set; } public int ProductId { get; set; }
public string ProductName { get; set; } public string ProductName { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public decimal Discount => 0;
public decimal Discount { get; set; }
public int Units { get; set; } public int Units { get; set; }
public string PictureUrl { get; set; } public string PictureUrl { get; set; }
} }
} }

View File

@@ -6,15 +6,13 @@ namespace Microsoft.eShopWeb.Web.ViewModels
{ {
public class OrderViewModel public class OrderViewModel
{ {
private const string DEFAULT_STATUS = "Pending";
public int OrderNumber { get; set; } public int OrderNumber { get; set; }
public DateTimeOffset OrderDate { get; set; } public DateTimeOffset OrderDate { get; set; }
public decimal Total { get; set; } public decimal Total { get; set; }
public string Status { get; set; } public string Status => DEFAULT_STATUS;
public Address ShippingAddress { get; set; }
public Address ShippingAddress { get; set; }
public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>(); public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>();
} }
} }

View File

@@ -14,6 +14,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" /> <PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" />
<PackageReference Include="MediatR" Version="7.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" Condition="'$(Configuration)'=='Release'" PrivateAssets="All" /> <PackageReference Include="BuildBundlerMinifier" Version="2.9.406" Condition="'$(Configuration)'=='Release'" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.3.1" /> <PackageReference Include="Microsoft.CodeAnalysis" Version="3.3.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.0" />

View File

@@ -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<IOrderRepository> _mockOrderRepository;
public GetMyOrders_Should()
{
var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10);
var address = new Address(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>());
Order order = new Order("buyerId", address, new List<OrderItem> { item });
_mockOrderRepository = new Mock<IOrderRepository>();
_mockOrderRepository.Setup(x => x.ListAsync(It.IsAny<ISpecification<Order>>())).ReturnsAsync(new List<Order> { 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);
}
}
}

View File

@@ -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<IOrderRepository> _mockOrderRepository;
public GetOrderDetails_Should()
{
var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10);
var address = new Address(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>());
Order order = new Order("buyerId", address, new List<OrderItem> { item });
_mockOrderRepository = new Mock<IOrderRepository>();
_mockOrderRepository.Setup(x => x.ListAsync(It.IsAny<ISpecification<Order>>())).ReturnsAsync(new List<Order> { 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);
}
}
}

View File

@@ -23,6 +23,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" /> <ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\..\src\Web\Web.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>