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 |
|||
{ |
|||
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 |
|||
{ |
|||
public class BasketItem : BaseEntity<string> |
|||
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; }
|
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
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; } |
|||
@ -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