From d7eb59c097c7fbdf57486e38001a947abed83bd3 Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Mon, 7 Aug 2017 13:25:11 -0400 Subject: [PATCH] Refactoring and Adding Tests (#28) * Introducing repository and refactoring services. Changing entities to use int keys everywhere. * Refactoring application services to live in web project and only reference repositories, not EF contexts. * Cleaning up implementations * Moving logic out of CatalogController Moving entity knowledge out of viewmodels. * Implementing specification includes better for catalogservice * Cleaning up and adding specification unit tests --- src/ApplicationCore/Entities/BaseEntity.cs | 4 +- src/ApplicationCore/Entities/Basket.cs | 11 +- src/ApplicationCore/Entities/BasketItem.cs | 6 +- src/ApplicationCore/Entities/CatalogBrand.cs | 2 +- src/ApplicationCore/Entities/CatalogItem.cs | 2 +- src/ApplicationCore/Entities/CatalogType.cs | 2 +- .../CatalogImageMissingException.cs | 11 ++ src/ApplicationCore/Interfaces/IAppLogger.cs | 4 + .../Interfaces/IBasketService.cs | 15 -- src/ApplicationCore/Interfaces/IRepository.cs | 18 +++ .../Interfaces/ISpecification.cs | 13 ++ .../Interfaces/IUriComposer.cs | 5 +- src/ApplicationCore/Services/UriComposer.cs | 6 +- .../BasketWithItemsSpecification.cs | 28 ++++ .../CatalogFilterSpecification.cs | 32 +++++ src/Infrastructure/Data/EfRepository.cs | 59 ++++++++ src/Infrastructure/Infrastructure.csproj | 3 + src/Infrastructure/Services/BasketService.cs | 63 --------- src/Web/Controllers/CartController.cs | 27 +--- src/Web/Controllers/CatalogController.cs | 42 +----- src/Web/Interfaces/IBasketService.cs | 14 ++ src/Web/Interfaces/ICatalogService.cs | 2 +- src/Web/Services/BasketService.cs | 86 ++++++++++++ src/Web/Services/CachedCatalogService.cs | 2 +- src/Web/Services/CatalogService.cs | 130 ++++++++---------- src/Web/Startup.cs | 15 +- src/Web/ViewModels/BasketItemViewModel.cs | 11 +- src/Web/ViewModels/BasketViewModel.cs | 1 + src/Web/ViewModels/Catalog.cs | 13 -- ...talogIndex.cs => CatalogIndexViewModel.cs} | 6 +- src/Web/ViewModels/CatalogItemViewModel.cs | 14 ++ src/Web/ViewModels/LoginViewModel.cs | 4 +- ...tionInfo.cs => PaginationInfoViewModel.cs} | 9 +- src/Web/Views/Catalog/Index.cshtml | 2 +- src/Web/Views/Catalog/_pagination.cshtml | 2 +- src/Web/Views/Catalog/_product.cshtml | 6 +- tests/FunctionalTests/FunctionalTests.csproj | 2 +- .../IntegrationTests/IntegrationTests.csproj | 2 +- .../BasketWithItemsSpecification.cs | 48 +++++++ tests/UnitTests/UnitTests.csproj | 4 +- .../Controllers/CatalogControllerGetImage.cs | 83 ----------- 41 files changed, 449 insertions(+), 360 deletions(-) delete mode 100644 src/ApplicationCore/Interfaces/IBasketService.cs create mode 100644 src/ApplicationCore/Interfaces/IRepository.cs create mode 100644 src/ApplicationCore/Interfaces/ISpecification.cs create mode 100644 src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs create mode 100644 src/ApplicationCore/Specifications/CatalogFilterSpecification.cs create mode 100644 src/Infrastructure/Data/EfRepository.cs delete mode 100644 src/Infrastructure/Services/BasketService.cs create mode 100644 src/Web/Interfaces/IBasketService.cs create mode 100644 src/Web/Services/BasketService.cs delete mode 100644 src/Web/ViewModels/Catalog.cs rename src/Web/ViewModels/{CatalogIndex.cs => CatalogIndexViewModel.cs} (69%) create mode 100644 src/Web/ViewModels/CatalogItemViewModel.cs rename src/Web/ViewModels/{PaginationInfo.cs => PaginationInfoViewModel.cs} (61%) create mode 100644 tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs delete mode 100644 tests/UnitTests/Web/Controllers/CatalogControllerGetImage.cs diff --git a/src/ApplicationCore/Entities/BaseEntity.cs b/src/ApplicationCore/Entities/BaseEntity.cs index fdc1cf7..b021b65 100644 --- a/src/ApplicationCore/Entities/BaseEntity.cs +++ b/src/ApplicationCore/Entities/BaseEntity.cs @@ -1,7 +1,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities { - public class BaseEntity + public class BaseEntity { - public T Id { get; set; } + public int Id { get; set; } } } diff --git a/src/ApplicationCore/Entities/Basket.cs b/src/ApplicationCore/Entities/Basket.cs index c313e1d..a682beb 100644 --- a/src/ApplicationCore/Entities/Basket.cs +++ b/src/ApplicationCore/Entities/Basket.cs @@ -3,25 +3,24 @@ using System.Linq; namespace Microsoft.eShopWeb.ApplicationCore.Entities { - public class Basket : BaseEntity + public class Basket : BaseEntity { public string BuyerId { get; set; } public List Items { get; set; } = new List(); - public void AddItem(CatalogItem item, decimal unitPrice, int quantity = 1) + public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1) { - if(!Items.Any(i => i.Item.Id == item.Id)) + if (!Items.Any(i => i.CatalogItemId == catalogItemId)) { Items.Add(new BasketItem() { - Item = item, - //ProductId = productId, + CatalogItemId = catalogItemId, Quantity = quantity, UnitPrice = unitPrice }); return; } - var existingItem = Items.FirstOrDefault(i => i.Item.Id == item.Id); + var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId); existingItem.Quantity += quantity; } } diff --git a/src/ApplicationCore/Entities/BasketItem.cs b/src/ApplicationCore/Entities/BasketItem.cs index 70ea9cf..c5f152c 100644 --- a/src/ApplicationCore/Entities/BasketItem.cs +++ b/src/ApplicationCore/Entities/BasketItem.cs @@ -1,10 +1,10 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities { - public class BasketItem : BaseEntity + public class BasketItem : BaseEntity { - //public int ProductId { get; set; } public decimal UnitPrice { get; set; } public int Quantity { get; set; } - public CatalogItem Item { get; set; } + public int CatalogItemId { get; set; } +// public CatalogItem Item { get; set; } } } diff --git a/src/ApplicationCore/Entities/CatalogBrand.cs b/src/ApplicationCore/Entities/CatalogBrand.cs index ffa6d0f..fc8be2f 100644 --- a/src/ApplicationCore/Entities/CatalogBrand.cs +++ b/src/ApplicationCore/Entities/CatalogBrand.cs @@ -2,7 +2,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities { - public class CatalogBrand : BaseEntity + public class CatalogBrand : BaseEntity { public string Brand { get; set; } } diff --git a/src/ApplicationCore/Entities/CatalogItem.cs b/src/ApplicationCore/Entities/CatalogItem.cs index 1c6b7f1..afed5d6 100644 --- a/src/ApplicationCore/Entities/CatalogItem.cs +++ b/src/ApplicationCore/Entities/CatalogItem.cs @@ -1,6 +1,6 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities { - public class CatalogItem : BaseEntity + public class CatalogItem : BaseEntity { public string Name { get; set; } public string Description { get; set; } diff --git a/src/ApplicationCore/Entities/CatalogType.cs b/src/ApplicationCore/Entities/CatalogType.cs index c6cbcc5..d1982a6 100644 --- a/src/ApplicationCore/Entities/CatalogType.cs +++ b/src/ApplicationCore/Entities/CatalogType.cs @@ -1,6 +1,6 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities { - public class CatalogType : BaseEntity + public class CatalogType : BaseEntity { public string Type { get; set; } } diff --git a/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs b/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs index ff07258..a08fbb5 100644 --- a/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs +++ b/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs @@ -2,6 +2,9 @@ namespace ApplicationCore.Exceptions { + /// + /// Note: No longer required. + /// public class CatalogImageMissingException : Exception { public CatalogImageMissingException(string message, @@ -14,5 +17,13 @@ namespace ApplicationCore.Exceptions innerException: innerException) { } + + public CatalogImageMissingException() : base() + { + } + + public CatalogImageMissingException(string message) : base(message) + { + } } } diff --git a/src/ApplicationCore/Interfaces/IAppLogger.cs b/src/ApplicationCore/Interfaces/IAppLogger.cs index 1ae198d..2f3f0bc 100644 --- a/src/ApplicationCore/Interfaces/IAppLogger.cs +++ b/src/ApplicationCore/Interfaces/IAppLogger.cs @@ -1,5 +1,9 @@ namespace ApplicationCore.Interfaces { + /// + /// This type eliminates the need to depend directly on the ASP.NET Core logging types. + /// + /// public interface IAppLogger { void LogWarning(string message, params object[] args); diff --git a/src/ApplicationCore/Interfaces/IBasketService.cs b/src/ApplicationCore/Interfaces/IBasketService.cs deleted file mode 100644 index 56d1d0c..0000000 --- a/src/ApplicationCore/Interfaces/IBasketService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using System.Threading.Tasks; - -namespace ApplicationCore.Interfaces -{ - public interface IBasketService - { - Task GetBasket(string basketId); - Task CreateBasket(); - Task CreateBasketForUser(string userId); - - Task AddItemToBasket(Basket basket, int productId, int quantity); - //Task UpdateBasket(Basket basket); - } -} diff --git a/src/ApplicationCore/Interfaces/IRepository.cs b/src/ApplicationCore/Interfaces/IRepository.cs new file mode 100644 index 0000000..a72abad --- /dev/null +++ b/src/ApplicationCore/Interfaces/IRepository.cs @@ -0,0 +1,18 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace ApplicationCore.Interfaces +{ + + public interface IRepository where T : BaseEntity + { + T GetById(int id); + List List(); + List List(ISpecification spec); + T Add(T entity); + void Update(T entity); + void Delete(T entity); + } +} diff --git a/src/ApplicationCore/Interfaces/ISpecification.cs b/src/ApplicationCore/Interfaces/ISpecification.cs new file mode 100644 index 0000000..6cff096 --- /dev/null +++ b/src/ApplicationCore/Interfaces/ISpecification.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace ApplicationCore.Interfaces +{ + public interface ISpecification + { + Expression> Criteria { get; } + List>> Includes { get; } + void AddInclude(Expression> includeExpression); + } +} diff --git a/src/ApplicationCore/Interfaces/IUriComposer.cs b/src/ApplicationCore/Interfaces/IUriComposer.cs index d642dd5..2e4c4ea 100644 --- a/src/ApplicationCore/Interfaces/IUriComposer.cs +++ b/src/ApplicationCore/Interfaces/IUriComposer.cs @@ -1,4 +1,7 @@ -namespace ApplicationCore.Interfaces +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; + +namespace ApplicationCore.Interfaces { public interface IUriComposer diff --git a/src/ApplicationCore/Services/UriComposer.cs b/src/ApplicationCore/Services/UriComposer.cs index ac1df54..eb4bb3d 100644 --- a/src/ApplicationCore/Services/UriComposer.cs +++ b/src/ApplicationCore/Services/UriComposer.cs @@ -7,10 +7,8 @@ namespace ApplicationCore.Services { private readonly CatalogSettings _catalogSettings; - public UriComposer(CatalogSettings catalogSettings) - { - _catalogSettings = catalogSettings; - } + public UriComposer(CatalogSettings catalogSettings) => _catalogSettings = catalogSettings; + public string ComposePicUri(string uriTemplate) { return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl); diff --git a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs new file mode 100644 index 0000000..1cdcc88 --- /dev/null +++ b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -0,0 +1,28 @@ +using ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System; +using System.Linq.Expressions; +using System.Collections.Generic; + +namespace ApplicationCore.Specifications +{ + public class BasketWithItemsSpecification : ISpecification + { + public BasketWithItemsSpecification(int basketId) + { + BasketId = basketId; + AddInclude(b => b.Items); + } + + public int BasketId { get; } + + public Expression> Criteria => b => b.Id == BasketId; + + public List>> Includes { get; } = new List>>(); + + public void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + } +} diff --git a/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs b/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs new file mode 100644 index 0000000..30d414e --- /dev/null +++ b/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs @@ -0,0 +1,32 @@ +using ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System; +using System.Linq.Expressions; +using System.Collections.Generic; + +namespace ApplicationCore.Specifications +{ + + public class CatalogFilterSpecification : ISpecification + { + public CatalogFilterSpecification(int? brandId, int? typeId) + { + BrandId = brandId; + TypeId = typeId; + } + + public int? BrandId { get; } + public int? TypeId { get; } + + public Expression> Criteria => + i => (!BrandId.HasValue || i.CatalogBrandId == BrandId) && + (!TypeId.HasValue || i.CatalogTypeId == TypeId); + + public List>> Includes { get; } = new List>>(); + + public void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + } +} diff --git a/src/Infrastructure/Data/EfRepository.cs b/src/Infrastructure/Data/EfRepository.cs new file mode 100644 index 0000000..9c7282f --- /dev/null +++ b/src/Infrastructure/Data/EfRepository.cs @@ -0,0 +1,59 @@ +using ApplicationCore.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; +using System.Linq; + +namespace Infrastructure.Data +{ + public class EfRepository : IRepository where T : BaseEntity + { + private readonly CatalogContext _dbContext; + + public EfRepository(CatalogContext dbContext) + { + _dbContext = dbContext; + } + + public T GetById(int id) + { + return _dbContext.Set().SingleOrDefault(e => e.Id == id); + } + + public List List() + { + return _dbContext.Set().ToList(); + } + + public List List(ISpecification spec) + { + var queryableResultWithIncludes = spec.Includes + .Aggregate(_dbContext.Set().AsQueryable(), + (current, include) => current.Include(include)); + return queryableResultWithIncludes + .Where(spec.Criteria) + .ToList(); + } + + public T Add(T entity) + { + _dbContext.Set().Add(entity); + _dbContext.SaveChanges(); + + return entity; + } + + public void Delete(T entity) + { + _dbContext.Set().Remove(entity); + _dbContext.SaveChanges(); + } + + public void Update(T entity) + { + _dbContext.Entry(entity).State = EntityState.Modified; + _dbContext.SaveChanges(); + } + + } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 7f9b47e..7c14c05 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -22,5 +22,8 @@ + + + \ No newline at end of file diff --git a/src/Infrastructure/Services/BasketService.cs b/src/Infrastructure/Services/BasketService.cs deleted file mode 100644 index 6bd8b0d..0000000 --- a/src/Infrastructure/Services/BasketService.cs +++ /dev/null @@ -1,63 +0,0 @@ -using ApplicationCore.Interfaces; -using System.Threading.Tasks; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.EntityFrameworkCore; -using System; -using Infrastructure.Data; - -namespace Web.Services -{ - public class BasketService : IBasketService - { - private readonly CatalogContext _context; - - public BasketService(CatalogContext context) - { - _context = context; - } - public async Task GetBasket(string basketId) - { - var basket = await _context.Baskets - .Include(b => b.Items) - .ThenInclude(i => i.Item) - .FirstOrDefaultAsync(b => b.Id == basketId); - if (basket == null) - { - basket = new Basket(); - _context.Baskets.Add(basket); - await _context.SaveChangesAsync(); - } - return basket; - } - - public Task CreateBasket() - { - return CreateBasketForUser(null); - } - - public async Task CreateBasketForUser(string userId) - { - var basket = new Basket(); - _context.Baskets.Add(basket); - await _context.SaveChangesAsync(); - - return basket; - } - - - //public async Task UpdateBasket(Basket basket) - //{ - // // only need to save changes here - // await _context.SaveChangesAsync(); - //} - - public async Task AddItemToBasket(Basket basket, int productId, int quantity) - { - var item = await _context.CatalogItems.FirstOrDefaultAsync(i => i.Id == productId); - - basket.AddItem(item, item.Price, quantity); - - await _context.SaveChangesAsync(); - } - } -} diff --git a/src/Web/Controllers/CartController.cs b/src/Web/Controllers/CartController.cs index d1e0119..e23b3f3 100644 --- a/src/Web/Controllers/CartController.cs +++ b/src/Web/Controllers/CartController.cs @@ -29,23 +29,10 @@ namespace Microsoft.eShopWeb.Controllers public async Task Index() { //var user = _appUserParser.Parse(HttpContext.User); - var basket = await GetBasketFromSessionAsync(); + var basketModel = await GetBasketFromSessionAsync(); - var viewModel = new BasketViewModel() - { - BuyerId = basket.BuyerId, - Items = basket.Items.Select(i => new BasketItemViewModel() - { - Id = i.Id, - UnitPrice = i.UnitPrice, - PictureUrl = _uriComposer.ComposePicUri(i.Item.PictureUri), - ProductId = i.Item.Id.ToString(), - ProductName = i.Item.Name, - Quantity = i.Quantity - }).ToList() - }; - return View(viewModel); + return View(basketModel); } // GET: /Cart/AddToCart @@ -58,23 +45,23 @@ namespace Microsoft.eShopWeb.Controllers } var basket = await GetBasketFromSessionAsync(); - await _basketService.AddItemToBasket(basket, productDetails.Id, 1); + await _basketService.AddItemToBasket(basket.Id, productDetails.Id, productDetails.Price, 1); return RedirectToAction("Index"); } - private async Task GetBasketFromSessionAsync() + private async Task GetBasketFromSessionAsync() { string basketId = HttpContext.Session.GetString(_basketSessionKey); - Basket basket = null; + BasketViewModel basket = null; if (basketId == null) { basket = await _basketService.CreateBasketForUser(User.Identity.Name); - HttpContext.Session.SetString(_basketSessionKey, basket.Id); + HttpContext.Session.SetString(_basketSessionKey, basket.Id.ToString()); } else { - basket = await _basketService.GetBasket(basketId); + basket = await _basketService.GetBasket(int.Parse(basketId)); } return basket; } diff --git a/src/Web/Controllers/CatalogController.cs b/src/Web/Controllers/CatalogController.cs index c2057c5..4b57f03 100644 --- a/src/Web/Controllers/CatalogController.cs +++ b/src/Web/Controllers/CatalogController.cs @@ -1,57 +1,21 @@ using Microsoft.eShopWeb.Services; -using Microsoft.eShopWeb.ViewModels; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; -using System; using System.Threading.Tasks; -using ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.Controllers { public class CatalogController : Controller { - private readonly IHostingEnvironment _env; private readonly ICatalogService _catalogService; - private readonly IImageService _imageService; - private readonly IAppLogger _logger; - public CatalogController(IHostingEnvironment env, - ICatalogService catalogService, - IImageService imageService, - IAppLogger logger) - { - _env = env; - _catalogService = catalogService; - _imageService = imageService; - _logger = logger; - } + public CatalogController(ICatalogService catalogService) => _catalogService = catalogService; // GET: // public async Task Index(int? brandFilterApplied, int? typesFilterApplied, int? page) { var itemsPage = 10; - var catalog = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied); - - var vm = new CatalogIndex() - { - CatalogItems = catalog.Data, - Brands = await _catalogService.GetBrands(), - Types = await _catalogService.GetTypes(), - BrandFilterApplied = brandFilterApplied ?? 0, - TypesFilterApplied = typesFilterApplied ?? 0, - PaginationInfo = new PaginationInfo() - { - ActualPage = page ?? 0, - ItemsPerPage = catalog.Data.Count, - TotalItems = catalog.Count, - TotalPages = int.Parse(Math.Ceiling(((decimal)catalog.Count / itemsPage)).ToString()) - } - }; - - vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : ""; - vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : ""; - - return View(vm); + var catalogModel = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied); + return View(catalogModel); } public IActionResult Error() diff --git a/src/Web/Interfaces/IBasketService.cs b/src/Web/Interfaces/IBasketService.cs new file mode 100644 index 0000000..c72826d --- /dev/null +++ b/src/Web/Interfaces/IBasketService.cs @@ -0,0 +1,14 @@ +using Microsoft.eShopWeb.ViewModels; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + public interface IBasketService + { + Task GetBasket(int basketId); + Task CreateBasket(); + Task CreateBasketForUser(string userId); + + Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity); + } +} diff --git a/src/Web/Interfaces/ICatalogService.cs b/src/Web/Interfaces/ICatalogService.cs index efd1188..1c3e333 100644 --- a/src/Web/Interfaces/ICatalogService.cs +++ b/src/Web/Interfaces/ICatalogService.cs @@ -7,7 +7,7 @@ namespace Microsoft.eShopWeb.Services { public interface ICatalogService { - Task GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId); + Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId); Task> GetBrands(); Task> GetTypes(); } diff --git a/src/Web/Services/BasketService.cs b/src/Web/Services/BasketService.cs new file mode 100644 index 0000000..8f0cedf --- /dev/null +++ b/src/Web/Services/BasketService.cs @@ -0,0 +1,86 @@ +using ApplicationCore.Interfaces; +using System.Threading.Tasks; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.EntityFrameworkCore; +using System; +using Infrastructure.Data; +using System.Linq; +using Microsoft.eShopWeb.ViewModels; +using System.Collections.Generic; +using ApplicationCore.Specifications; + +namespace Web.Services +{ + public class BasketService : IBasketService + { + private readonly IRepository _basketRepository; + private readonly IUriComposer _uriComposer; + private readonly IRepository _itemRepository; + + public BasketService(IRepository basketRepository, + IRepository itemRepository, + IUriComposer uriComposer) + { + _basketRepository = basketRepository; + _uriComposer = uriComposer; + _itemRepository = itemRepository; + } + public async Task GetBasket(int basketId) + { + var basketSpec = new BasketWithItemsSpecification(basketId); + var basket = _basketRepository.List(basketSpec).FirstOrDefault(); + if (basket == null) + { + return await CreateBasket(); + } + + var viewModel = new BasketViewModel(); + viewModel.Id = basket.Id; + viewModel.BuyerId = basket.BuyerId; + viewModel.Items = basket.Items.Select(i => + { + var itemModel = new BasketItemViewModel() + { + Id = i.Id, + UnitPrice = i.UnitPrice, + Quantity = i.Quantity, + CatalogItemId = i.CatalogItemId + + }; + var item = _itemRepository.GetById(i.CatalogItemId); + itemModel.PictureUrl = _uriComposer.ComposePicUri(item.PictureUri); + itemModel.ProductName = item.Name; + return itemModel; + }) + .ToList(); + return viewModel; + } + + public Task CreateBasket() + { + return CreateBasketForUser(null); + } + + public async Task CreateBasketForUser(string userId) + { + var basket = new Basket() { BuyerId = userId }; + _basketRepository.Add(basket); + + return new BasketViewModel() + { + BuyerId = basket.BuyerId, + Id = basket.Id, + Items = new List() + }; + } + + public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity) + { + var basket = _basketRepository.GetById(basketId); + + basket.AddItem(catalogItemId, price, quantity); + + _basketRepository.Update(basket); + } + } +} diff --git a/src/Web/Services/CachedCatalogService.cs b/src/Web/Services/CachedCatalogService.cs index c17daff..1e2ed96 100644 --- a/src/Web/Services/CachedCatalogService.cs +++ b/src/Web/Services/CachedCatalogService.cs @@ -32,7 +32,7 @@ namespace Microsoft.eShopWeb.Services }); } - public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId) + public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId) { string cacheKey = String.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandID, typeId); return await _cache.GetOrCreateAsync(cacheKey, async entry => diff --git a/src/Web/Services/CatalogService.cs b/src/Web/Services/CatalogService.cs index ef8e32c..1c1446f 100644 --- a/src/Web/Services/CatalogService.cs +++ b/src/Web/Services/CatalogService.cs @@ -2,84 +2,88 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using Microsoft.eShopWeb.ViewModels; using Microsoft.eShopWeb.ApplicationCore.Entities; -using System.Data.SqlClient; -using Dapper; using Microsoft.Extensions.Logging; -using Infrastructure.Data; +using ApplicationCore.Interfaces; +using System; +using ApplicationCore.Specifications; namespace Microsoft.eShopWeb.Services { public class CatalogService : ICatalogService { - private readonly CatalogContext _context; - private readonly IOptionsSnapshot _settings; private readonly ILogger _logger; - - public CatalogService(CatalogContext context, - IOptionsSnapshot settings, - ILoggerFactory loggerFactory) + private readonly IRepository _itemRepository; + private readonly IRepository _brandRepository; + private readonly IRepository _typeRepository; + private readonly IUriComposer _uriComposer; + + public CatalogService( + ILoggerFactory loggerFactory, + IRepository itemRepository, + IRepository brandRepository, + IRepository typeRepository, + IUriComposer uriComposer) { - _context = context; - _settings = settings; _logger = loggerFactory.CreateLogger(); + _itemRepository = itemRepository; + _brandRepository = brandRepository; + _typeRepository = typeRepository; + _uriComposer = uriComposer; } - public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) + public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) { _logger.LogInformation("GetCatalogItems called."); - var root = (IQueryable)_context.CatalogItems; - if (typeId.HasValue) - { - root = root.Where(ci => ci.CatalogTypeId == typeId); - } + var filterSpecification = new CatalogFilterSpecification(brandId, typeId); + var root = _itemRepository.List(filterSpecification); - if (brandId.HasValue) - { - root = root.Where(ci => ci.CatalogBrandId == brandId); - } + var totalItems = root.Count(); - var totalItems = await root - .LongCountAsync(); - - var itemsOnPage = await root + var itemsOnPage = root .Skip(itemsPage * pageIndex) .Take(itemsPage) - .ToListAsync(); + .ToList(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage.ForEach(x => + { + x.PictureUri = _uriComposer.ComposePicUri(x.PictureUri); + }); - return new Catalog() { Data = itemsOnPage, PageIndex = pageIndex, Count = (int)totalItems }; + var vm = new CatalogIndexViewModel() + { + CatalogItems = itemsOnPage.Select(i => new CatalogItemViewModel() + { + Id = i.Id, + Name = i.Name, + PictureUri = i.PictureUri, + Price = i.Price + }), + Brands = await GetBrands(), + Types = await GetTypes(), + BrandFilterApplied = brandId ?? 0, + TypesFilterApplied = typeId ?? 0, + PaginationInfo = new PaginationInfoViewModel() + { + ActualPage = pageIndex, + ItemsPerPage = itemsOnPage.Count, + TotalItems = totalItems, + TotalPages = int.Parse(Math.Ceiling(((decimal)totalItems / itemsPage)).ToString()) + } + }; + + vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : ""; + vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : ""; + + return vm; } public async Task> GetBrands() { _logger.LogInformation("GetBrands called."); - var brands = await _context.CatalogBrands.ToListAsync(); - -//// create -//var newBrand = new CatalogBrand() { Brand = "Acme" }; -//_context.Add(newBrand); -//await _context.SaveChangesAsync(); - -//// read and update -//var existingBrand = _context.Find(1); -//existingBrand.Brand = "Updated Brand"; -//await _context.SaveChangesAsync(); - -//// delete -//var brandToDelete = _context.Find(2); -//_context.CatalogBrands.Remove(brandToDelete); -//await _context.SaveChangesAsync(); - -//var brandsWithItems = await _context.CatalogBrands -// .Include(b => b.Items) -// .ToListAsync(); - + var brands = _brandRepository.List(); var items = new List { @@ -96,7 +100,7 @@ namespace Microsoft.eShopWeb.Services public async Task> GetTypes() { _logger.LogInformation("GetTypes called."); - var types = await _context.CatalogTypes.ToListAsync(); + var types = _typeRepository.List(); var items = new List { new SelectListItem() { Value = null, Text = "All", Selected = true } @@ -108,27 +112,5 @@ namespace Microsoft.eShopWeb.Services return items; } - - private List ComposePicUri(List items) - { - var baseUri = _settings.Value.CatalogBaseUrl; - items.ForEach(x => - { - x.PictureUri = x.PictureUri.Replace("http://catalogbaseurltobereplaced", baseUri); - }); - - return items; - } - - //public async Task> GetCatalogTypes() - //{ - // return await _context.CatalogTypes.ToListAsync(); - //} - - //private readonly SqlConnection _conn; - //public async Task> GetCatalogTypesWithDapper() - //{ - // return await _conn.QueryAsync("SELECT * FROM CatalogType"); - //} } } diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index 0abbd9b..a6a863e 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -67,6 +67,8 @@ namespace Microsoft.eShopWeb .AddEntityFrameworkStores() .AddDefaultTokenProviders(); + services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); + services.AddMemoryCache(); services.AddScoped(); services.AddScoped(); @@ -93,9 +95,7 @@ namespace Microsoft.eShopWeb // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, - IHostingEnvironment env, - ILoggerFactory loggerFactory, - UserManager userManager) + IHostingEnvironment env) { if (env.IsDevelopment()) { @@ -139,6 +139,15 @@ namespace Microsoft.eShopWeb template: "{controller=Catalog}/{action=Index}/{id?}"); }); + } + + public void ConfigureDevelopment(IApplicationBuilder app, + IHostingEnvironment env, + ILoggerFactory loggerFactory, + UserManager userManager) + { + Configure(app, env); + //Seed Data CatalogContextSeed.SeedAsync(app, loggerFactory) .Wait(); diff --git a/src/Web/ViewModels/BasketItemViewModel.cs b/src/Web/ViewModels/BasketItemViewModel.cs index 9256932..237d7dc 100644 --- a/src/Web/ViewModels/BasketItemViewModel.cs +++ b/src/Web/ViewModels/BasketItemViewModel.cs @@ -1,15 +1,10 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.eShopWeb.ViewModels +namespace Microsoft.eShopWeb.ViewModels { public class BasketItemViewModel { - public string Id { get; set; } - public string ProductId { get; set; } + public int Id { get; set; } + public int CatalogItemId { get; set; } public string ProductName { get; set; } public decimal UnitPrice { get; set; } public decimal OldUnitPrice { get; set; } diff --git a/src/Web/ViewModels/BasketViewModel.cs b/src/Web/ViewModels/BasketViewModel.cs index 5087568..4168cb9 100644 --- a/src/Web/ViewModels/BasketViewModel.cs +++ b/src/Web/ViewModels/BasketViewModel.cs @@ -7,6 +7,7 @@ namespace Microsoft.eShopWeb.ViewModels public class BasketViewModel { + public int Id { get; set; } public List Items { get; set; } = new List(); public string BuyerId { get; set; } diff --git a/src/Web/ViewModels/Catalog.cs b/src/Web/ViewModels/Catalog.cs deleted file mode 100644 index 492d880..0000000 --- a/src/Web/ViewModels/Catalog.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using System.Collections.Generic; - -namespace Microsoft.eShopWeb.ViewModels -{ - public class Catalog - { - public int PageIndex { get; set; } - public int PageSize { get; set; } - public int Count { get; set; } - public List Data { get; set; } - } -} diff --git a/src/Web/ViewModels/CatalogIndex.cs b/src/Web/ViewModels/CatalogIndexViewModel.cs similarity index 69% rename from src/Web/ViewModels/CatalogIndex.cs rename to src/Web/ViewModels/CatalogIndexViewModel.cs index 82975cc..e142706 100644 --- a/src/Web/ViewModels/CatalogIndex.cs +++ b/src/Web/ViewModels/CatalogIndexViewModel.cs @@ -4,13 +4,13 @@ using System.Collections.Generic; namespace Microsoft.eShopWeb.ViewModels { - public class CatalogIndex + public class CatalogIndexViewModel { - public IEnumerable CatalogItems { get; set; } + public IEnumerable CatalogItems { get; set; } public IEnumerable Brands { get; set; } public IEnumerable Types { get; set; } public int? BrandFilterApplied { get; set; } public int? TypesFilterApplied { get; set; } - public PaginationInfo PaginationInfo { get; set; } + public PaginationInfoViewModel PaginationInfo { get; set; } } } diff --git a/src/Web/ViewModels/CatalogItemViewModel.cs b/src/Web/ViewModels/CatalogItemViewModel.cs new file mode 100644 index 0000000..49f32fa --- /dev/null +++ b/src/Web/ViewModels/CatalogItemViewModel.cs @@ -0,0 +1,14 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; + +namespace Microsoft.eShopWeb.ViewModels +{ + + public class CatalogItemViewModel + { + public int Id { get; set; } + public string Name { get; set; } + public string PictureUri { get; set; } + public decimal Price { get; set; } + } +} diff --git a/src/Web/ViewModels/LoginViewModel.cs b/src/Web/ViewModels/LoginViewModel.cs index 26e2f65..e5e09be 100644 --- a/src/Web/ViewModels/LoginViewModel.cs +++ b/src/Web/ViewModels/LoginViewModel.cs @@ -1,6 +1,4 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.ViewModels { diff --git a/src/Web/ViewModels/PaginationInfo.cs b/src/Web/ViewModels/PaginationInfoViewModel.cs similarity index 61% rename from src/Web/ViewModels/PaginationInfo.cs rename to src/Web/ViewModels/PaginationInfoViewModel.cs index e343291..89e90b4 100644 --- a/src/Web/ViewModels/PaginationInfo.cs +++ b/src/Web/ViewModels/PaginationInfoViewModel.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopWeb.ViewModels +namespace Microsoft.eShopWeb.ViewModels { - public class PaginationInfo + public class PaginationInfoViewModel { public int TotalItems { get; set; } public int ItemsPerPage { get; set; } diff --git a/src/Web/Views/Catalog/Index.cshtml b/src/Web/Views/Catalog/Index.cshtml index fa99eae..228e4c6 100644 --- a/src/Web/Views/Catalog/Index.cshtml +++ b/src/Web/Views/Catalog/Index.cshtml @@ -1,6 +1,6 @@ @{ ViewData["Title"] = "Catalog"; - @model Microsoft.eShopWeb.ViewModels.CatalogIndex + @model CatalogIndexViewModel }
diff --git a/src/Web/Views/Catalog/_pagination.cshtml b/src/Web/Views/Catalog/_pagination.cshtml index 852f89b..ca94c72 100644 --- a/src/Web/Views/Catalog/_pagination.cshtml +++ b/src/Web/Views/Catalog/_pagination.cshtml @@ -1,4 +1,4 @@ -@model Microsoft.eShopWeb.ViewModels.PaginationInfo +@model PaginationInfoViewModel
diff --git a/src/Web/Views/Catalog/_product.cshtml b/src/Web/Views/Catalog/_product.cshtml index 05a1b7b..ea330d9 100644 --- a/src/Web/Views/Catalog/_product.cshtml +++ b/src/Web/Views/Catalog/_product.cshtml @@ -1,4 +1,4 @@ -@model Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem +@model CatalogItemViewModel
@@ -12,11 +12,11 @@
@Model.Price.ToString("N2")
- + @* - + *@ diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index a46954d..c649ab5 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -5,7 +5,7 @@ - + diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index c9ee0d0..eb5ec12 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -5,7 +5,7 @@ - + diff --git a/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs new file mode 100644 index 0000000..a0fea71 --- /dev/null +++ b/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -0,0 +1,48 @@ +using ApplicationCore.Specifications; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace UnitTests +{ + public class BasketWithItems + { + private int _testBasketId = 123; + + [Fact] + public void MatchesBasketWithGivenId() + { + var spec = new BasketWithItemsSpecification(_testBasketId); + + var result = GetTestBasketCollection() + .AsQueryable() + .FirstOrDefault(spec.Criteria); + + Assert.NotNull(result); + Assert.Equal(_testBasketId, result.Id); + + } + + [Fact] + public void MatchesNoBasketsIfIdNotPresent() + { + int badId = -1; + var spec = new BasketWithItemsSpecification(badId); + + Assert.False(GetTestBasketCollection() + .AsQueryable() + .Any(spec.Criteria)); + } + + public List GetTestBasketCollection() + { + return new List() + { + new Basket() { Id = 1 }, + new Basket() { Id = 2 }, + new Basket() { Id = _testBasketId } + }; + } + } +} diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index b978920..92fe017 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -8,13 +8,15 @@ - + + + diff --git a/tests/UnitTests/Web/Controllers/CatalogControllerGetImage.cs b/tests/UnitTests/Web/Controllers/CatalogControllerGetImage.cs deleted file mode 100644 index 2ec38e7..0000000 --- a/tests/UnitTests/Web/Controllers/CatalogControllerGetImage.cs +++ /dev/null @@ -1,83 +0,0 @@ -using ApplicationCore.Exceptions; -using ApplicationCore.Interfaces; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.Controllers; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace UnitTests -{ - //public class CatalogControllerGetImage - //{ - // private Mock _mockImageService = new Mock(); - // private Mock> _mockLogger = new Mock>(); - // private CatalogController _controller; - // private int _testImageId = 123; - // private byte[] _testBytes = { 0x01, 0x02, 0x03 }; - - // public CatalogControllerGetImage() - // { - // _controller = new CatalogController(null, null, _mockImageService.Object, - // _mockLogger.Object); - // } - - // [Fact] - // public void CallsImageServiceWithId() - // { - // SetupImageWithTestBytes(); - - // _controller.GetImage(_testImageId); - // _mockImageService.Verify(); - // } - - // [Fact] - // public void ReturnsFileResultWithBytesGivenSuccess() - // { - // SetupImageWithTestBytes(); - - // var result = _controller.GetImage(_testImageId); - - // var fileResult = Assert.IsType(result); - // var bytes = Assert.IsType(fileResult.FileContents); - // } - - // [Fact] - // public void ReturnsNotFoundResultGivenImageMissingException() - // { - // SetupMissingImage(); - - // var result = _controller.GetImage(_testImageId); - - // var actionResult = Assert.IsType(result); - // } - - // [Fact] - // public void LogsWarningGivenImageMissingException() - // { - // SetupMissingImage(); - // _mockLogger.Setup(l => l.LogWarning(It.IsAny())) - // .Verifiable(); - - // _controller.GetImage(_testImageId); - - // _mockLogger.Verify(); - // } - - // private void SetupMissingImage() - // { - // _mockImageService - // .Setup(i => i.GetImageBytesById(_testImageId)) - // .Throws(new CatalogImageMissingException("missing image")); - // } - - // private void SetupImageWithTestBytes() - // { - // _mockImageService - // .Setup(i => i.GetImageBytesById(_testImageId)) - // .Returns(_testBytes) - // .Verifiable(); - // } - //} -}