Browse Source
* 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 testsmain
committed by
GitHub
41 changed files with 449 additions and 360 deletions
@ -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; } |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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; }
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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);
|
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
@ -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 } |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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();
|
|
||||
// }
|
|
||||
//}
|
|
||||
} |
|
||||
Loading…
Reference in new issue