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
This commit is contained in:
Steve Smith
2017-08-07 13:25:11 -04:00
committed by GitHub
parent 084db74c77
commit d7eb59c097
41 changed files with 449 additions and 360 deletions

View File

@@ -1,7 +1,7 @@
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
public class BaseEntity<T> public class BaseEntity
{ {
public T Id { get; set; } public int Id { get; set; }
} }
} }

View File

@@ -3,25 +3,24 @@ using System.Linq;
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
public class Basket : BaseEntity<string> public class Basket : BaseEntity
{ {
public string BuyerId { get; set; } public string BuyerId { get; set; }
public List<BasketItem> Items { get; set; } = new List<BasketItem>(); public List<BasketItem> Items { get; set; } = new List<BasketItem>();
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() Items.Add(new BasketItem()
{ {
Item = item, CatalogItemId = catalogItemId,
//ProductId = productId,
Quantity = quantity, Quantity = quantity,
UnitPrice = unitPrice UnitPrice = unitPrice
}); });
return; return;
} }
var existingItem = Items.FirstOrDefault(i => i.Item.Id == item.Id); var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId);
existingItem.Quantity += quantity; existingItem.Quantity += quantity;
} }
} }

View File

@@ -1,10 +1,10 @@
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
public class BasketItem : BaseEntity<string> public class BasketItem : BaseEntity
{ {
//public int ProductId { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public int Quantity { get; set; } public int Quantity { get; set; }
public CatalogItem Item { get; set; } public int CatalogItemId { get; set; }
// public CatalogItem Item { get; set; }
} }
} }

View File

@@ -2,7 +2,7 @@
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
public class CatalogBrand : BaseEntity<int> public class CatalogBrand : BaseEntity
{ {
public string Brand { get; set; } public string Brand { get; set; }
} }

View File

@@ -1,6 +1,6 @@
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
public class CatalogItem : BaseEntity<int> public class CatalogItem : BaseEntity
{ {
public string Name { get; set; } public string Name { get; set; }
public string Description { get; set; } public string Description { get; set; }

View File

@@ -1,6 +1,6 @@
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
public class CatalogType : BaseEntity<int> public class CatalogType : BaseEntity
{ {
public string Type { get; set; } public string Type { get; set; }
} }

View File

@@ -2,6 +2,9 @@
namespace ApplicationCore.Exceptions namespace ApplicationCore.Exceptions
{ {
/// <summary>
/// Note: No longer required.
/// </summary>
public class CatalogImageMissingException : Exception public class CatalogImageMissingException : Exception
{ {
public CatalogImageMissingException(string message, public CatalogImageMissingException(string message,
@@ -14,5 +17,13 @@ namespace ApplicationCore.Exceptions
innerException: innerException) innerException: innerException)
{ {
} }
public CatalogImageMissingException() : base()
{
}
public CatalogImageMissingException(string message) : base(message)
{
}
} }
} }

View File

@@ -1,5 +1,9 @@
namespace ApplicationCore.Interfaces namespace ApplicationCore.Interfaces
{ {
/// <summary>
/// This type eliminates the need to depend directly on the ASP.NET Core logging types.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IAppLogger<T> public interface IAppLogger<T>
{ {
void LogWarning(string message, params object[] args); void LogWarning(string message, params object[] args);

View File

@@ -1,15 +0,0 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
{
public interface IBasketService
{
Task<Basket> GetBasket(string basketId);
Task<Basket> CreateBasket();
Task<Basket> CreateBasketForUser(string userId);
Task AddItemToBasket(Basket basket, int productId, int quantity);
//Task UpdateBasket(Basket basket);
}
}

View File

@@ -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<T> where T : BaseEntity
{
T GetById(int id);
List<T> List();
List<T> List(ISpecification<T> spec);
T Add(T entity);
void Update(T entity);
void Delete(T entity);
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace ApplicationCore.Interfaces
{
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
void AddInclude(Expression<Func<T, object>> includeExpression);
}
}

View File

@@ -1,4 +1,7 @@
namespace ApplicationCore.Interfaces using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
namespace ApplicationCore.Interfaces
{ {
public interface IUriComposer public interface IUriComposer

View File

@@ -7,10 +7,8 @@ namespace ApplicationCore.Services
{ {
private readonly CatalogSettings _catalogSettings; private readonly CatalogSettings _catalogSettings;
public UriComposer(CatalogSettings catalogSettings) public UriComposer(CatalogSettings catalogSettings) => _catalogSettings = catalogSettings;
{
_catalogSettings = catalogSettings;
}
public string ComposePicUri(string uriTemplate) public string ComposePicUri(string uriTemplate)
{ {
return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl); return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl);

View File

@@ -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<Basket>
{
public BasketWithItemsSpecification(int basketId)
{
BasketId = basketId;
AddInclude(b => b.Items);
}
public int BasketId { get; }
public Expression<Func<Basket, bool>> Criteria => b => b.Id == BasketId;
public List<Expression<Func<Basket, object>>> Includes { get; } = new List<Expression<Func<Basket, object>>>();
public void AddInclude(Expression<Func<Basket, object>> includeExpression)
{
Includes.Add(includeExpression);
}
}
}

View File

@@ -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<CatalogItem>
{
public CatalogFilterSpecification(int? brandId, int? typeId)
{
BrandId = brandId;
TypeId = typeId;
}
public int? BrandId { get; }
public int? TypeId { get; }
public Expression<Func<CatalogItem, bool>> Criteria =>
i => (!BrandId.HasValue || i.CatalogBrandId == BrandId) &&
(!TypeId.HasValue || i.CatalogTypeId == TypeId);
public List<Expression<Func<CatalogItem, object>>> Includes { get; } = new List<Expression<Func<CatalogItem, object>>>();
public void AddInclude(Expression<Func<CatalogItem, object>> includeExpression)
{
Includes.Add(includeExpression);
}
}
}

View File

@@ -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<T> : IRepository<T> where T : BaseEntity
{
private readonly CatalogContext _dbContext;
public EfRepository(CatalogContext dbContext)
{
_dbContext = dbContext;
}
public T GetById(int id)
{
return _dbContext.Set<T>().SingleOrDefault(e => e.Id == id);
}
public List<T> List()
{
return _dbContext.Set<T>().ToList();
}
public List<T> List(ISpecification<T> spec)
{
var queryableResultWithIncludes = spec.Includes
.Aggregate(_dbContext.Set<T>().AsQueryable(),
(current, include) => current.Include(include));
return queryableResultWithIncludes
.Where(spec.Criteria)
.ToList();
}
public T Add(T entity)
{
_dbContext.Set<T>().Add(entity);
_dbContext.SaveChanges();
return entity;
}
public void Delete(T entity)
{
_dbContext.Set<T>().Remove(entity);
_dbContext.SaveChanges();
}
public void Update(T entity)
{
_dbContext.Entry(entity).State = EntityState.Modified;
_dbContext.SaveChanges();
}
}
}

View File

@@ -22,5 +22,8 @@
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.1" /> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.1" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" /> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Services\" />
</ItemGroup>
</Project> </Project>

View File

@@ -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<Basket> 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<Basket> CreateBasket()
{
return CreateBasketForUser(null);
}
public async Task<Basket> 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();
}
}
}

View File

@@ -29,23 +29,10 @@ namespace Microsoft.eShopWeb.Controllers
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
//var user = _appUserParser.Parse(HttpContext.User); //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 // GET: /Cart/AddToCart
@@ -58,23 +45,23 @@ namespace Microsoft.eShopWeb.Controllers
} }
var basket = await GetBasketFromSessionAsync(); var basket = await GetBasketFromSessionAsync();
await _basketService.AddItemToBasket(basket, productDetails.Id, 1); await _basketService.AddItemToBasket(basket.Id, productDetails.Id, productDetails.Price, 1);
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
private async Task<Basket> GetBasketFromSessionAsync() private async Task<BasketViewModel> GetBasketFromSessionAsync()
{ {
string basketId = HttpContext.Session.GetString(_basketSessionKey); string basketId = HttpContext.Session.GetString(_basketSessionKey);
Basket basket = null; BasketViewModel basket = null;
if (basketId == null) if (basketId == null)
{ {
basket = await _basketService.CreateBasketForUser(User.Identity.Name); basket = await _basketService.CreateBasketForUser(User.Identity.Name);
HttpContext.Session.SetString(_basketSessionKey, basket.Id); HttpContext.Session.SetString(_basketSessionKey, basket.Id.ToString());
} }
else else
{ {
basket = await _basketService.GetBasket(basketId); basket = await _basketService.GetBasket(int.Parse(basketId));
} }
return basket; return basket;
} }

View File

@@ -1,57 +1,21 @@
using Microsoft.eShopWeb.Services; using Microsoft.eShopWeb.Services;
using Microsoft.eShopWeb.ViewModels;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationCore.Interfaces;
namespace Microsoft.eShopWeb.Controllers namespace Microsoft.eShopWeb.Controllers
{ {
public class CatalogController : Controller public class CatalogController : Controller
{ {
private readonly IHostingEnvironment _env;
private readonly ICatalogService _catalogService; private readonly ICatalogService _catalogService;
private readonly IImageService _imageService;
private readonly IAppLogger<CatalogController> _logger;
public CatalogController(IHostingEnvironment env, public CatalogController(ICatalogService catalogService) => _catalogService = catalogService;
ICatalogService catalogService,
IImageService imageService,
IAppLogger<CatalogController> logger)
{
_env = env;
_catalogService = catalogService;
_imageService = imageService;
_logger = logger;
}
// GET: /<controller>/ // GET: /<controller>/
public async Task<IActionResult> Index(int? brandFilterApplied, int? typesFilterApplied, int? page) public async Task<IActionResult> Index(int? brandFilterApplied, int? typesFilterApplied, int? page)
{ {
var itemsPage = 10; var itemsPage = 10;
var catalog = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied); var catalogModel = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied);
return View(catalogModel);
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);
} }
public IActionResult Error() public IActionResult Error()

View File

@@ -0,0 +1,14 @@
using Microsoft.eShopWeb.ViewModels;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
{
public interface IBasketService
{
Task<BasketViewModel> GetBasket(int basketId);
Task<BasketViewModel> CreateBasket();
Task<BasketViewModel> CreateBasketForUser(string userId);
Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity);
}
}

View File

@@ -7,7 +7,7 @@ namespace Microsoft.eShopWeb.Services
{ {
public interface ICatalogService public interface ICatalogService
{ {
Task<Catalog> GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId); Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId);
Task<IEnumerable<SelectListItem>> GetBrands(); Task<IEnumerable<SelectListItem>> GetBrands();
Task<IEnumerable<SelectListItem>> GetTypes(); Task<IEnumerable<SelectListItem>> GetTypes();
} }

View File

@@ -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<Basket> _basketRepository;
private readonly IUriComposer _uriComposer;
private readonly IRepository<CatalogItem> _itemRepository;
public BasketService(IRepository<Basket> basketRepository,
IRepository<CatalogItem> itemRepository,
IUriComposer uriComposer)
{
_basketRepository = basketRepository;
_uriComposer = uriComposer;
_itemRepository = itemRepository;
}
public async Task<BasketViewModel> 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<BasketViewModel> CreateBasket()
{
return CreateBasketForUser(null);
}
public async Task<BasketViewModel> CreateBasketForUser(string userId)
{
var basket = new Basket() { BuyerId = userId };
_basketRepository.Add(basket);
return new BasketViewModel()
{
BuyerId = basket.BuyerId,
Id = basket.Id,
Items = new List<BasketItemViewModel>()
};
}
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);
}
}
}

View File

@@ -32,7 +32,7 @@ namespace Microsoft.eShopWeb.Services
}); });
} }
public async Task<Catalog> GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId) public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId)
{ {
string cacheKey = String.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandID, typeId); string cacheKey = String.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandID, typeId);
return await _cache.GetOrCreateAsync(cacheKey, async entry => return await _cache.GetOrCreateAsync(cacheKey, async entry =>

View File

@@ -2,84 +2,88 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.eShopWeb.ViewModels; using Microsoft.eShopWeb.ViewModels;
using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Data.SqlClient;
using Dapper;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Infrastructure.Data; using ApplicationCore.Interfaces;
using System;
using ApplicationCore.Specifications;
namespace Microsoft.eShopWeb.Services namespace Microsoft.eShopWeb.Services
{ {
public class CatalogService : ICatalogService public class CatalogService : ICatalogService
{ {
private readonly CatalogContext _context;
private readonly IOptionsSnapshot<CatalogSettings> _settings;
private readonly ILogger<CatalogService> _logger; private readonly ILogger<CatalogService> _logger;
private readonly IRepository<CatalogItem> _itemRepository;
public CatalogService(CatalogContext context, private readonly IRepository<CatalogBrand> _brandRepository;
IOptionsSnapshot<CatalogSettings> settings, private readonly IRepository<CatalogType> _typeRepository;
ILoggerFactory loggerFactory) private readonly IUriComposer _uriComposer;
public CatalogService(
ILoggerFactory loggerFactory,
IRepository<CatalogItem> itemRepository,
IRepository<CatalogBrand> brandRepository,
IRepository<CatalogType> typeRepository,
IUriComposer uriComposer)
{ {
_context = context;
_settings = settings;
_logger = loggerFactory.CreateLogger<CatalogService>(); _logger = loggerFactory.CreateLogger<CatalogService>();
_itemRepository = itemRepository;
_brandRepository = brandRepository;
_typeRepository = typeRepository;
_uriComposer = uriComposer;
} }
public async Task<Catalog> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
{ {
_logger.LogInformation("GetCatalogItems called."); _logger.LogInformation("GetCatalogItems called.");
var root = (IQueryable<CatalogItem>)_context.CatalogItems;
if (typeId.HasValue) var filterSpecification = new CatalogFilterSpecification(brandId, typeId);
{ var root = _itemRepository.List(filterSpecification);
root = root.Where(ci => ci.CatalogTypeId == typeId);
}
if (brandId.HasValue) var totalItems = root.Count();
{
root = root.Where(ci => ci.CatalogBrandId == brandId);
}
var totalItems = await root var itemsOnPage = root
.LongCountAsync();
var itemsOnPage = await root
.Skip(itemsPage * pageIndex) .Skip(itemsPage * pageIndex)
.Take(itemsPage) .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<IEnumerable<SelectListItem>> GetBrands() public async Task<IEnumerable<SelectListItem>> GetBrands()
{ {
_logger.LogInformation("GetBrands called."); _logger.LogInformation("GetBrands called.");
var brands = await _context.CatalogBrands.ToListAsync(); var brands = _brandRepository.List();
//// create
//var newBrand = new CatalogBrand() { Brand = "Acme" };
//_context.Add(newBrand);
//await _context.SaveChangesAsync();
//// read and update
//var existingBrand = _context.Find<CatalogBrand>(1);
//existingBrand.Brand = "Updated Brand";
//await _context.SaveChangesAsync();
//// delete
//var brandToDelete = _context.Find<CatalogBrand>(2);
//_context.CatalogBrands.Remove(brandToDelete);
//await _context.SaveChangesAsync();
//var brandsWithItems = await _context.CatalogBrands
// .Include(b => b.Items)
// .ToListAsync();
var items = new List<SelectListItem> var items = new List<SelectListItem>
{ {
@@ -96,7 +100,7 @@ namespace Microsoft.eShopWeb.Services
public async Task<IEnumerable<SelectListItem>> GetTypes() public async Task<IEnumerable<SelectListItem>> GetTypes()
{ {
_logger.LogInformation("GetTypes called."); _logger.LogInformation("GetTypes called.");
var types = await _context.CatalogTypes.ToListAsync(); var types = _typeRepository.List();
var items = new List<SelectListItem> var items = new List<SelectListItem>
{ {
new SelectListItem() { Value = null, Text = "All", Selected = true } new SelectListItem() { Value = null, Text = "All", Selected = true }
@@ -108,27 +112,5 @@ namespace Microsoft.eShopWeb.Services
return items; return items;
} }
private List<CatalogItem> ComposePicUri(List<CatalogItem> items)
{
var baseUri = _settings.Value.CatalogBaseUrl;
items.ForEach(x =>
{
x.PictureUri = x.PictureUri.Replace("http://catalogbaseurltobereplaced", baseUri);
});
return items;
}
//public async Task<IEnumerable<CatalogType>> GetCatalogTypes()
//{
// return await _context.CatalogTypes.ToListAsync();
//}
//private readonly SqlConnection _conn;
//public async Task<IEnumerable<CatalogType>> GetCatalogTypesWithDapper()
//{
// return await _conn.QueryAsync<CatalogType>("SELECT * FROM CatalogType");
//}
} }
} }

View File

@@ -67,6 +67,8 @@ namespace Microsoft.eShopWeb
.AddEntityFrameworkStores<AppIdentityDbContext>() .AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddMemoryCache(); services.AddMemoryCache();
services.AddScoped<ICatalogService, CachedCatalogService>(); services.AddScoped<ICatalogService, CachedCatalogService>();
services.AddScoped<IBasketService, BasketService>(); services.AddScoped<IBasketService, BasketService>();
@@ -93,9 +95,7 @@ namespace Microsoft.eShopWeb
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, public void Configure(IApplicationBuilder app,
IHostingEnvironment env, IHostingEnvironment env)
ILoggerFactory loggerFactory,
UserManager<ApplicationUser> userManager)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
@@ -139,6 +139,15 @@ namespace Microsoft.eShopWeb
template: "{controller=Catalog}/{action=Index}/{id?}"); template: "{controller=Catalog}/{action=Index}/{id?}");
}); });
}
public void ConfigureDevelopment(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
UserManager<ApplicationUser> userManager)
{
Configure(app, env);
//Seed Data //Seed Data
CatalogContextSeed.SeedAsync(app, loggerFactory) CatalogContextSeed.SeedAsync(app, loggerFactory)
.Wait(); .Wait();

View File

@@ -1,15 +1,10 @@
using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.ViewModels
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.eShopWeb.ViewModels
{ {
public class BasketItemViewModel public class BasketItemViewModel
{ {
public string Id { get; set; } public int Id { get; set; }
public string ProductId { get; set; } public int CatalogItemId { get; set; }
public string ProductName { get; set; } public string ProductName { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public decimal OldUnitPrice { get; set; } public decimal OldUnitPrice { get; set; }

View File

@@ -7,6 +7,7 @@ namespace Microsoft.eShopWeb.ViewModels
public class BasketViewModel public class BasketViewModel
{ {
public int Id { get; set; }
public List<BasketItemViewModel> Items { get; set; } = new List<BasketItemViewModel>(); public List<BasketItemViewModel> Items { get; set; } = new List<BasketItemViewModel>();
public string BuyerId { get; set; } public string BuyerId { get; set; }

View File

@@ -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<CatalogItem> Data { get; set; }
}
}

View File

@@ -4,13 +4,13 @@ using System.Collections.Generic;
namespace Microsoft.eShopWeb.ViewModels namespace Microsoft.eShopWeb.ViewModels
{ {
public class CatalogIndex public class CatalogIndexViewModel
{ {
public IEnumerable<CatalogItem> CatalogItems { get; set; } public IEnumerable<CatalogItemViewModel> CatalogItems { get; set; }
public IEnumerable<SelectListItem> Brands { get; set; } public IEnumerable<SelectListItem> Brands { get; set; }
public IEnumerable<SelectListItem> Types { get; set; } public IEnumerable<SelectListItem> Types { get; set; }
public int? BrandFilterApplied { get; set; } public int? BrandFilterApplied { get; set; }
public int? TypesFilterApplied { get; set; } public int? TypesFilterApplied { get; set; }
public PaginationInfo PaginationInfo { get; set; } public PaginationInfoViewModel PaginationInfo { get; set; }
} }
} }

View File

@@ -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; }
}
}

View File

@@ -1,6 +1,4 @@
using Microsoft.eShopWeb.ApplicationCore.Entities; using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.ViewModels namespace Microsoft.eShopWeb.ViewModels
{ {

View File

@@ -1,11 +1,6 @@
using System; namespace Microsoft.eShopWeb.ViewModels
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.ViewModels
{ {
public class PaginationInfo public class PaginationInfoViewModel
{ {
public int TotalItems { get; set; } public int TotalItems { get; set; }
public int ItemsPerPage { get; set; } public int ItemsPerPage { get; set; }

View File

@@ -1,6 +1,6 @@
@{ @{
ViewData["Title"] = "Catalog"; ViewData["Title"] = "Catalog";
@model Microsoft.eShopWeb.ViewModels.CatalogIndex @model CatalogIndexViewModel
} }
<section class="esh-catalog-hero"> <section class="esh-catalog-hero">
<div class="container"> <div class="container">

View File

@@ -1,4 +1,4 @@
@model Microsoft.eShopWeb.ViewModels.PaginationInfo @model PaginationInfoViewModel
<div class="esh-pager"> <div class="esh-pager">
<div class="container"> <div class="container">

View File

@@ -1,4 +1,4 @@
@model Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem @model CatalogItemViewModel
<form asp-controller="Cart" asp-action="AddToCart"> <form asp-controller="Cart" asp-action="AddToCart">
@@ -12,11 +12,11 @@
<div class="esh-catalog-price"> <div class="esh-catalog-price">
<span>@Model.Price.ToString("N2")</span> <span>@Model.Price.ToString("N2")</span>
</div> </div>
<input type="hidden" asp-for="@Model.CatalogBrand" name="brand" /> @*<input type="hidden" asp-for="@Model.CatalogBrand" name="brand" />
<input type="hidden" asp-for="@Model.CatalogBrandId" name="brandId" /> <input type="hidden" asp-for="@Model.CatalogBrandId" name="brandId" />
<input type="hidden" asp-for="@Model.CatalogType" name="type" /> <input type="hidden" asp-for="@Model.CatalogType" name="type" />
<input type="hidden" asp-for="@Model.CatalogTypeId" name="typeId" /> <input type="hidden" asp-for="@Model.CatalogTypeId" name="typeId" />
<input type="hidden" asp-for="@Model.Description" name="description" /> <input type="hidden" asp-for="@Model.Description" name="description" />*@
<input type="hidden" asp-for="@Model.Id" name="id" /> <input type="hidden" asp-for="@Model.Id" name="id" />
<input type="hidden" asp-for="@Model.Name" name="name" /> <input type="hidden" asp-for="@Model.Name" name="name" />
<input type="hidden" asp-for="@Model.PictureUri" name="pictureUri" /> <input type="hidden" asp-for="@Model.PictureUri" name="pictureUri" />

View File

@@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="1.1.2" /> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="1.1.2" />

View File

@@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="Moq" Version="4.7.49" /> <PackageReference Include="Moq" Version="4.7.49" />

View File

@@ -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<Basket> GetTestBasketCollection()
{
return new List<Basket>()
{
new Basket() { Id = 1 },
new Basket() { Id = 2 },
new Basket() { Id = _testBasketId }
};
}
}
}

View File

@@ -8,13 +8,15 @@
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" /> <PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="Moq" Version="4.7.49" /> <PackageReference Include="Moq" Version="4.7.49" />
<PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.console" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\..\src\Web\Web.csproj" /> <ProjectReference Include="..\..\src\Web\Web.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -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<IImageService> _mockImageService = new Mock<IImageService>();
// private Mock<IAppLogger<CatalogController>> _mockLogger = new Mock<IAppLogger<CatalogController>>();
// 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<FileContentResult>(result);
// var bytes = Assert.IsType<byte[]>(fileResult.FileContents);
// }
// [Fact]
// public void ReturnsNotFoundResultGivenImageMissingException()
// {
// SetupMissingImage();
// var result = _controller.GetImage(_testImageId);
// var actionResult = Assert.IsType<NotFoundResult>(result);
// }
// [Fact]
// public void LogsWarningGivenImageMissingException()
// {
// SetupMissingImage();
// _mockLogger.Setup(l => l.LogWarning(It.IsAny<string>()))
// .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();
// }
//}
}