Feature/respect encapsulation (#349)

* resolve osbsolete method

* put all properties as private, align unit test

* fix version of version in MD, add instruction to install ef tool

* fix url stored
This commit is contained in:
Cédric Michel
2020-02-03 20:47:59 +01:00
committed by GitHub
parent 288d827821
commit 3e228035c0
22 changed files with 162 additions and 99 deletions

View File

@@ -16,7 +16,7 @@ The **eShopOnWeb** sample is related to the [eShopOnContainers](https://github.c
The goal for this sample is to demonstrate some of the principles and patterns described in the [eBook](https://aka.ms/webappebook). It is not meant to be an eCommerce reference application, and as such it does not implement many features that would be obvious and/or essential to a real eCommerce application.
> ### VERSIONS
> #### The `master` branch is currently running ASP.NET Core 2.2.
> #### The `master` branch is currently running ASP.NET Core 3.1.
> #### Older versions are tagged.
## Topics (eBook TOC)
@@ -58,6 +58,10 @@ You can also run the samples in Docker (see below).
```
1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance.
1. Ensure the tool EF was already installed. You can find some help [here](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet)
```
dotnet tool install --global dotnet-ef
```
1. Open a command prompt in the Web folder and execute the following commands:

View File

@@ -2,8 +2,8 @@
{
// This can easily be modified to be BaseEntity<T> and public T Id to support different key types.
// Using non-generic integer types for simplicity and to ease caching logic
public class BaseEntity
public abstract class BaseEntity
{
public int Id { get; set; }
public virtual int Id { get; protected set; }
}
}

View File

@@ -6,29 +6,34 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate
{
public class Basket : BaseEntity, IAggregateRoot
{
public string BuyerId { get; set; }
public string BuyerId { get; private set; }
private readonly List<BasketItem> _items = new List<BasketItem>();
public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();
public Basket(string buyerId)
{
BuyerId = buyerId;
}
public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1)
{
if (!Items.Any(i => i.CatalogItemId == catalogItemId))
{
_items.Add(new BasketItem()
{
CatalogItemId = catalogItemId,
Quantity = quantity,
UnitPrice = unitPrice
});
_items.Add(new BasketItem(catalogItemId, quantity, unitPrice));
return;
}
var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId);
existingItem.Quantity += quantity;
existingItem.AddQuantity(quantity);
}
public void RemoveEmptyItems()
{
_items.RemoveAll(i => i.Quantity == 0);
}
public void SetNewBuyerId(string buyerId)
{
BuyerId = buyerId;
}
}
}

View File

@@ -2,9 +2,27 @@
{
public class BasketItem : BaseEntity
{
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public int CatalogItemId { get; set; }
public decimal UnitPrice { get; private set; }
public int Quantity { get; private set; }
public int CatalogItemId { get; private set; }
public int BasketId { get; private set; }
public BasketItem(int catalogItemId, int quantity, decimal unitPrice)
{
CatalogItemId = catalogItemId;
Quantity = quantity;
UnitPrice = unitPrice;
}
public void AddQuantity(int quantity)
{
Quantity += quantity;
}
public void SetNewQuantity(int quantity)
{
Quantity = quantity;
}
}
}

View File

@@ -2,8 +2,8 @@
{
public class PaymentMethod : BaseEntity
{
public string Alias { get; set; }
public string CardId { get; set; } // actual card data must be stored in a PCI compliant system, like Stripe
public string Last4 { get; set; }
public string Alias { get; private set; }
public string CardId { get; private set; } // actual card data must be stored in a PCI compliant system, like Stripe
public string Last4 { get; private set; }
}
}

View File

@@ -4,6 +4,10 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities
{
public class CatalogBrand : BaseEntity, IAggregateRoot
{
public string Brand { get; set; }
public string Brand { get; private set; }
public CatalogBrand(string brand)
{
Brand = brand;
}
}
}

View File

@@ -1,16 +1,34 @@
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
namespace Microsoft.eShopWeb.ApplicationCore.Entities
{
public class CatalogItem : BaseEntity, IAggregateRoot
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string PictureUri { get; set; }
public int CatalogTypeId { get; set; }
public CatalogType CatalogType { get; set; }
public int CatalogBrandId { get; set; }
public CatalogBrand CatalogBrand { get; set; }
public string Name { get; private set; }
public string Description { get; private set; }
public decimal Price { get; private set; }
public string PictureUri { get; private set; }
public int CatalogTypeId { get; private set; }
public CatalogType CatalogType { get; private set; }
public int CatalogBrandId { get; private set; }
public CatalogBrand CatalogBrand { get; private set; }
public CatalogItem(int catalogTypeId, int catalogBrandId, string description, string name, decimal price, string pictureUri)
{
CatalogTypeId = catalogTypeId;
CatalogBrandId = catalogBrandId;
Description = description;
Name = name;
Price = price;
PictureUri = pictureUri;
}
public void Update(string name, decimal price)
{
Guard.Against.NullOrEmpty(name, nameof(name));
Name = name;
Price = price;
}
}
}

View File

@@ -4,6 +4,10 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities
{
public class CatalogType : BaseEntity, IAggregateRoot
{
public string Type { get; set; }
public string Type { get; private set; }
public CatalogType(string type)
{
Type = type;
}
}
}

View File

@@ -4,15 +4,15 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate
{
public class Address // ValueObject
{
public String Street { get; private set; }
public string Street { get; private set; }
public String City { get; private set; }
public string City { get; private set; }
public String State { get; private set; }
public string State { get; private set; }
public String Country { get; private set; }
public string Country { get; private set; }
public String ZipCode { get; private set; }
public string ZipCode { get; private set; }
private Address() { }

View File

@@ -59,8 +59,8 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
{
if (quantities.TryGetValue(item.Id.ToString(), out var quantity))
{
if(_logger != null) _logger.LogInformation($"Updating quantity of item ID:{item.Id} to {quantity}.");
item.Quantity = quantity;
if (_logger != null) _logger.LogInformation($"Updating quantity of item ID:{item.Id} to {quantity}.");
item.SetNewQuantity(quantity);
}
}
basket.RemoveEmptyItems();
@@ -74,7 +74,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
var basketSpec = new BasketWithItemsSpecification(anonymousId);
var basket = (await _basketRepository.ListAsync(basketSpec)).FirstOrDefault();
if (basket == null) return;
basket.BuyerId = userName;
basket.SetNewBuyerId(userName);
await _basketRepository.UpdateAsync(basket);
}
}

View File

@@ -11,14 +11,17 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
public class OrderService : IOrderService
{
private readonly IAsyncRepository<Order> _orderRepository;
private readonly IUriComposer _uriComposer;
private readonly IAsyncRepository<Basket> _basketRepository;
private readonly IAsyncRepository<CatalogItem> _itemRepository;
public OrderService(IAsyncRepository<Basket> basketRepository,
IAsyncRepository<CatalogItem> itemRepository,
IAsyncRepository<Order> orderRepository)
IAsyncRepository<Order> orderRepository,
IUriComposer uriComposer)
{
_orderRepository = orderRepository;
_uriComposer = uriComposer;
_basketRepository = basketRepository;
_itemRepository = itemRepository;
}
@@ -31,7 +34,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
foreach (var item in basket.Items)
{
var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name,_uriComposer.ComposePicUri(catalogItem.PictureUri));
var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity);
items.Add(orderItem);
}

View File

@@ -59,11 +59,11 @@ namespace Microsoft.eShopWeb.Infrastructure.Data
{
return new List<CatalogBrand>()
{
new CatalogBrand() { Brand = "Azure"},
new CatalogBrand() { Brand = ".NET" },
new CatalogBrand() { Brand = "Visual Studio" },
new CatalogBrand() { Brand = "SQL Server" },
new CatalogBrand() { Brand = "Other" }
new CatalogBrand("Azure"),
new CatalogBrand(".NET"),
new CatalogBrand("Visual Studio"),
new CatalogBrand("SQL Server"),
new CatalogBrand("Other")
};
}
@@ -71,10 +71,10 @@ namespace Microsoft.eShopWeb.Infrastructure.Data
{
return new List<CatalogType>()
{
new CatalogType() { Type = "Mug"},
new CatalogType() { Type = "T-Shirt" },
new CatalogType() { Type = "Sheet" },
new CatalogType() { Type = "USB Memory Stick" }
new CatalogType("Mug"),
new CatalogType("T-Shirt"),
new CatalogType("Sheet"),
new CatalogType("USB Memory Stick")
};
}
@@ -82,18 +82,18 @@ namespace Microsoft.eShopWeb.Infrastructure.Data
{
return new List<CatalogItem>()
{
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Sweatshirt", Name = ".NET Bot Black Sweatshirt", Price = 19.5M, PictureUri = "http://catalogbaseurltobereplaced/images/products/1.png" },
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureUri = "http://catalogbaseurltobereplaced/images/products/2.png" },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/3.png" },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation Sweatshirt", Name = ".NET Foundation Sweatshirt", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/4.png" },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=5, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureUri = "http://catalogbaseurltobereplaced/images/products/5.png" },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Sweatshirt", Name = ".NET Blue Sweatshirt", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/6.png" },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/7.png" },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Sweatshirt", Name = "Kudu Purple Sweatshirt", Price = 8.5M, PictureUri = "http://catalogbaseurltobereplaced/images/products/8.png" },
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/9.png" },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/10.png" },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://catalogbaseurltobereplaced/images/products/11.png" },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureUri = "http://catalogbaseurltobereplaced/images/products/12.png" }
new CatalogItem(2,2, ".NET Bot Black Sweatshirt", ".NET Bot Black Sweatshirt", 19.5M, "http://catalogbaseurltobereplaced/images/products/1.png"),
new CatalogItem(1,2, ".NET Black & White Mug", ".NET Black & White Mug", 8.50M, "http://catalogbaseurltobereplaced/images/products/2.png"),
new CatalogItem(2,5, "Prism White T-Shirt", "Prism White T-Shirt", 12, "http://catalogbaseurltobereplaced/images/products/3.png"),
new CatalogItem(2,2, ".NET Foundation Sweatshirt", ".NET Foundation Sweatshirt", 12, "http://catalogbaseurltobereplaced/images/products/4.png"),
new CatalogItem(3,5, "Roslyn Red Sheet", "Roslyn Red Sheet", 8.5M, "http://catalogbaseurltobereplaced/images/products/5.png"),
new CatalogItem(2,2, ".NET Blue Sweatshirt", ".NET Blue Sweatshirt", 12, "http://catalogbaseurltobereplaced/images/products/6.png"),
new CatalogItem(2,5, "Roslyn Red T-Shirt", "Roslyn Red T-Shirt", 12, "http://catalogbaseurltobereplaced/images/products/7.png"),
new CatalogItem(2,5, "Kudu Purple Sweatshirt", "Kudu Purple Sweatshirt", 8.5M, "http://catalogbaseurltobereplaced/images/products/8.png"),
new CatalogItem(1,5, "Cup<T> White Mug", "Cup<T> White Mug", 12, "http://catalogbaseurltobereplaced/images/products/9.png"),
new CatalogItem(3,2, ".NET Foundation Sheet", ".NET Foundation Sheet", 12, "http://catalogbaseurltobereplaced/images/products/10.png"),
new CatalogItem(3,2, "Cup<T> Sheet", "Cup<T> Sheet", 8.5M, "http://catalogbaseurltobereplaced/images/products/11.png"),
new CatalogItem(2,5, "Prism White TShirt", "Prism White TShirt", 12, "http://catalogbaseurltobereplaced/images/products/12.png")
};
}
}

View File

@@ -11,7 +11,7 @@ namespace Microsoft.eShopWeb.Infrastructure.Data.Config
builder.HasKey(ci => ci.Id);
builder.Property(ci => ci.Id)
.ForSqlServerUseSequenceHiLo("catalog_type_hilo")
.UseHiLo("catalog_type_hilo")
.IsRequired();
builder.Property(cb => cb.Type)

View File

@@ -21,7 +21,7 @@ namespace Microsoft.eShopWeb.Web.Pages.Admin
[BindProperty]
public CatalogItemViewModel CatalogModel { get; set; } = new CatalogItemViewModel();
public async Task OnGet(CatalogItemViewModel catalogModel)
public void OnGet(CatalogItemViewModel catalogModel)
{
CatalogModel = catalogModel;
}

View File

@@ -48,7 +48,7 @@ namespace Microsoft.eShopWeb.Web.Services
private async Task<BasketViewModel> CreateBasketForUser(string userId)
{
var basket = new Basket() { BuyerId = userId };
var basket = new Basket(userId);
await _basketRepository.AddAsync(basket);
return new BasketViewModel()

View File

@@ -17,15 +17,9 @@ namespace Microsoft.eShopWeb.Web.Services
public async Task UpdateCatalogItem(CatalogItemViewModel viewModel)
{
//Get existing CatalogItem
var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id);
//Build updated CatalogItem
var updatedCatalogItem = existingCatalogItem;
updatedCatalogItem.Name = viewModel.Name;
updatedCatalogItem.Price = viewModel.Price;
await _catalogItemRepository.UpdateAsync(updatedCatalogItem);
existingCatalogItem.Update(viewModel.Name, viewModel.Price);
await _catalogItemRepository.UpdateAsync(existingCatalogItem);
}
}
}

View File

@@ -48,19 +48,14 @@ namespace Microsoft.eShopWeb.Web.Services
// the implementation below using ForEach and Count. We need a List.
var itemsOnPage = await _itemRepository.ListAsync(filterPaginatedSpecification);
var totalItems = await _itemRepository.CountAsync(filterSpecification);
foreach (var itemOnPage in itemsOnPage)
{
itemOnPage.PictureUri = _uriComposer.ComposePicUri(itemOnPage.PictureUri);
}
var vm = new CatalogIndexViewModel()
{
CatalogItems = itemsOnPage.Select(i => new CatalogItemViewModel()
{
Id = i.Id,
Name = i.Name,
PictureUri = i.PictureUri,
PictureUri = _uriComposer.ComposePicUri(i.PictureUri),
Price = i.Price
}),
Brands = await GetBrands(),

View File

@@ -6,14 +6,15 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
{
public class BasketAddItem
{
private int _testCatalogItemId = 123;
private decimal _testUnitPrice = 1.23m;
private int _testQuantity = 2;
private readonly int _testCatalogItemId = 123;
private readonly decimal _testUnitPrice = 1.23m;
private readonly int _testQuantity = 2;
private readonly string _buyerId = "Test buyerId";
[Fact]
public void AddsBasketItemIfNotPresent()
{
var basket = new Basket();
var basket = new Basket(_buyerId);
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
var firstItem = basket.Items.Single();
@@ -25,7 +26,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
[Fact]
public void IncrementsQuantityOfItemIfPresent()
{
var basket = new Basket();
var basket = new Basket(_buyerId);
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
@@ -36,7 +37,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
[Fact]
public void KeepsOriginalUnitPriceIfMoreItemsAdded()
{
var basket = new Basket();
var basket = new Basket(_buyerId);
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
basket.AddItem(_testCatalogItemId, _testUnitPrice * 2, _testQuantity);
@@ -47,7 +48,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
[Fact]
public void DefaultsToQuantityOfOne()
{
var basket = new Basket();
var basket = new Basket(_buyerId);
basket.AddItem(_testCatalogItemId, _testUnitPrice);
var firstItem = basket.Items.Single();
@@ -57,7 +58,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
[Fact]
public void RemoveEmptyItems()
{
var basket = new Basket();
var basket = new Basket(_buyerId);
basket.AddItem(_testCatalogItemId, _testUnitPrice, 0);
basket.RemoveEmptyItems();

View File

@@ -9,7 +9,8 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes
{
public class DeleteBasket
{
private Mock<IAsyncRepository<Basket>> _mockBasketRepo;
private readonly string _buyerId = "Test buyerId";
private readonly Mock<IAsyncRepository<Basket>> _mockBasketRepo;
public DeleteBasket()
{
@@ -19,7 +20,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes
[Fact]
public async Task Should_InvokeBasketRepositoryDeleteAsync_Once()
{
var basket = new Basket();
var basket = new Basket(_buyerId);
basket.AddItem(1, It.IsAny<decimal>(), It.IsAny<int>());
basket.AddItem(2, It.IsAny<decimal>(), It.IsAny<int>());
_mockBasketRepo.Setup(x => x.GetByIdAsync(It.IsAny<int>()))

View File

@@ -3,12 +3,14 @@ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Moq;
namespace Microsoft.eShopWeb.UnitTests
{
public class BasketWithItems
{
private int _testBasketId = 123;
private readonly int _testBasketId = 123;
private readonly string _buyerId = "Test buyerId";
[Fact]
public void MatchesBasketWithGivenId()
@@ -37,11 +39,18 @@ namespace Microsoft.eShopWeb.UnitTests
public List<Basket> GetTestBasketCollection()
{
var basket1Mock = new Mock<Basket>(_buyerId);
basket1Mock.SetupGet(s => s.Id).Returns(1);
var basket2Mock = new Mock<Basket>(_buyerId);
basket2Mock.SetupGet(s => s.Id).Returns(2);
var basket3Mock = new Mock<Basket>(_buyerId);
basket3Mock.SetupGet(s => s.Id).Returns(_testBasketId);
return new List<Basket>()
{
new Basket() { Id = 1 },
new Basket() { Id = 2 },
new Basket() { Id = _testBasketId }
basket1Mock.Object,
basket2Mock.Object,
basket3Mock.Object
};
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Moq;
namespace Microsoft.eShopWeb.UnitTests
{
@@ -31,11 +32,11 @@ namespace Microsoft.eShopWeb.UnitTests
{
return new List<CatalogItem>()
{
new CatalogItem() { Id = 1, CatalogBrandId = 1, CatalogTypeId= 1 },
new CatalogItem() { Id = 2, CatalogBrandId = 1, CatalogTypeId= 2 },
new CatalogItem() { Id = 3, CatalogBrandId = 1, CatalogTypeId= 3 },
new CatalogItem() { Id = 4, CatalogBrandId = 2, CatalogTypeId= 1 },
new CatalogItem() { Id = 5, CatalogBrandId = 2, CatalogTypeId= 2 },
new CatalogItem(1, 1, "Description", "Name", 0, "FakePath"),
new CatalogItem(2, 1, "Description", "Name", 0, "FakePath"),
new CatalogItem(3, 1, "Description", "Name", 0, "FakePath"),
new CatalogItem(1, 2, "Description", "Name", 0, "FakePath"),
new CatalogItem(2, 2, "Description", "Name", 0, "FakePath"),
};
}
}

View File

@@ -1,11 +1,13 @@
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Moq;
namespace Microsoft.eShopWeb.UnitTests.Builders
{
public class BasketBuilder
{
private Basket _basket;
private Basket _basket;
public string BasketBuyerId => "testbuyerId@test.com";
public int BasketId => 1;
public BasketBuilder()
@@ -20,13 +22,17 @@ namespace Microsoft.eShopWeb.UnitTests.Builders
public Basket WithNoItems()
{
_basket = new Basket { BuyerId = BasketBuyerId, Id = BasketId };
var basketMock = new Mock<Basket>(BasketBuyerId);
basketMock.SetupGet(s => s.Id).Returns(BasketId);
_basket = basketMock.Object;
return _basket;
}
public Basket WithOneBasketItem()
{
_basket = new Basket { BuyerId = BasketBuyerId, Id = BasketId };
var basketMock = new Mock<Basket>(BasketBuyerId);
_basket = basketMock.Object;
_basket.AddItem(2, 3.40m, 4);
return _basket;
}