Basket : Improve, reduce, remove duplication call to database (#610)

This commit is contained in:
Cédric Michel
2021-11-01 22:09:34 +01:00
committed by GitHub
parent 47f69eb294
commit 984740d422
9 changed files with 76 additions and 79 deletions

View File

@@ -7,17 +7,5 @@ namespace Microsoft.eShopWeb.ApplicationCore.Exceptions
public BasketNotFoundException(int basketId) : base($"No basket found with id {basketId}") public BasketNotFoundException(int basketId) : base($"No basket found with id {basketId}")
{ {
} }
protected BasketNotFoundException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
public BasketNotFoundException(string message) : base(message)
{
}
public BasketNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces namespace Microsoft.eShopWeb.ApplicationCore.Interfaces
@@ -6,8 +7,8 @@ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces
public interface IBasketService public interface IBasketService
{ {
Task TransferBasketAsync(string anonymousId, string userName); Task TransferBasketAsync(string anonymousId, string userName);
Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity = 1); Task<Basket> AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1);
Task SetQuantities(int basketId, Dictionary<string, int> quantities); Task<Basket> SetQuantities(int basketId, Dictionary<string, int> quantities);
Task DeleteBasketAsync(int basketId); Task DeleteBasketAsync(int basketId);
} }
} }

View File

@@ -19,15 +19,21 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
_logger = logger; _logger = logger;
} }
public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity = 1) public async Task<Basket> AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1)
{ {
var basketSpec = new BasketWithItemsSpecification(basketId); var basketSpec = new BasketWithItemsSpecification(username);
var basket = await _basketRepository.GetBySpecAsync(basketSpec); var basket = await _basketRepository.GetBySpecAsync(basketSpec);
Guard.Against.NullBasket(basketId, basket);
if (basket == null)
{
basket = new Basket(username);
await _basketRepository.AddAsync(basket);
}
basket.AddItem(catalogItemId, price, quantity); basket.AddItem(catalogItemId, price, quantity);
await _basketRepository.UpdateAsync(basket); await _basketRepository.UpdateAsync(basket);
return basket;
} }
public async Task DeleteBasketAsync(int basketId) public async Task DeleteBasketAsync(int basketId)
@@ -36,7 +42,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
await _basketRepository.DeleteAsync(basket); await _basketRepository.DeleteAsync(basket);
} }
public async Task SetQuantities(int basketId, Dictionary<string, int> quantities) public async Task<Basket> SetQuantities(int basketId, Dictionary<string, int> quantities)
{ {
Guard.Against.Null(quantities, nameof(quantities)); Guard.Against.Null(quantities, nameof(quantities));
var basketSpec = new BasketWithItemsSpecification(basketId); var basketSpec = new BasketWithItemsSpecification(basketId);
@@ -53,6 +59,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
} }
basket.RemoveEmptyItems(); basket.RemoveEmptyItems();
await _basketRepository.UpdateAsync(basket); await _basketRepository.UpdateAsync(basket);
return basket;
} }
public async Task TransferBasketAsync(string anonymousId, string userName) public async Task TransferBasketAsync(string anonymousId, string userName)

View File

@@ -1,4 +1,5 @@
using Microsoft.eShopWeb.Web.Pages.Basket; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Microsoft.eShopWeb.Web.Pages.Basket;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Interfaces namespace Microsoft.eShopWeb.Web.Interfaces
@@ -6,5 +7,9 @@ namespace Microsoft.eShopWeb.Web.Interfaces
public interface IBasketViewModelService public interface IBasketViewModelService
{ {
Task<BasketViewModel> GetOrCreateBasketForUser(string userName); Task<BasketViewModel> GetOrCreateBasketForUser(string userName);
Task<int> CountTotalBasketItems(string username);
Task<BasketViewModel> Map(Basket basket);
} }
} }

View File

@@ -1,9 +1,7 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.eShopWeb.Web.ViewModels;
using System; using System;
@@ -16,16 +14,12 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket
public class IndexModel : PageModel public class IndexModel : PageModel
{ {
private readonly IBasketService _basketService; private readonly IBasketService _basketService;
private readonly SignInManager<ApplicationUser> _signInManager;
private string _username = null;
private readonly IBasketViewModelService _basketViewModelService; private readonly IBasketViewModelService _basketViewModelService;
public IndexModel(IBasketService basketService, public IndexModel(IBasketService basketService,
IBasketViewModelService basketViewModelService, IBasketViewModelService basketViewModelService)
SignInManager<ApplicationUser> signInManager)
{ {
_basketService = basketService; _basketService = basketService;
_signInManager = signInManager;
_basketViewModelService = basketViewModelService; _basketViewModelService = basketViewModelService;
} }
@@ -33,7 +27,7 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket
public async Task OnGet() public async Task OnGet()
{ {
await SetBasketModelAsync(); BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName());
} }
public async Task<IActionResult> OnPost(CatalogItemViewModel productDetails) public async Task<IActionResult> OnPost(CatalogItemViewModel productDetails)
@@ -42,64 +36,58 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket
{ {
return RedirectToPage("/Index"); return RedirectToPage("/Index");
} }
await SetBasketModelAsync();
await _basketService.AddItemToBasket(BasketModel.Id, productDetails.Id, productDetails.Price); var username = GetOrSetBasketCookieAndUserName();
var basket = await _basketService.AddItemToBasket(username,
productDetails.Id, productDetails.Price);
await SetBasketModelAsync(); BasketModel = await _basketViewModelService.Map(basket);
return RedirectToPage(); return RedirectToPage();
} }
public async Task OnPostUpdate(IEnumerable<BasketItemViewModel> items) public async Task OnPostUpdate(IEnumerable<BasketItemViewModel> items)
{ {
await SetBasketModelAsync();
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return; return;
} }
var basketView = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName());
var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity); var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity);
await _basketService.SetQuantities(BasketModel.Id, updateModel); var basket = await _basketService.SetQuantities(basketView.Id, updateModel);
BasketModel = await _basketViewModelService.Map(basket);
await SetBasketModelAsync();
} }
private async Task SetBasketModelAsync() private string GetOrSetBasketCookieAndUserName()
{ {
if (_signInManager.IsSignedIn(HttpContext.User)) string userName = null;
{
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name);
}
else
{
GetOrSetBasketCookieAndUserName();
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username);
} if (Request.HttpContext.User.Identity.IsAuthenticated)
{
return Request.HttpContext.User.Identity.Name;
} }
private void GetOrSetBasketCookieAndUserName()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{ {
_username = Request.Cookies[Constants.BASKET_COOKIENAME]; userName = Request.Cookies[Constants.BASKET_COOKIENAME];
if (!Request.HttpContext.User.Identity.IsAuthenticated) if (!Request.HttpContext.User.Identity.IsAuthenticated)
{ {
if (!Guid.TryParse(_username, out var _)) if (!Guid.TryParse(userName, out var _))
{ {
_username = null; userName = null;
} }
} }
} }
if (_username != null) return; if (userName != null) return userName;
_username = Guid.NewGuid().ToString(); userName = Guid.NewGuid().ToString();
var cookieOptions = new CookieOptions { IsEssential = true }; var cookieOptions = new CookieOptions { IsEssential = true };
cookieOptions.Expires = DateTime.Today.AddYears(10); cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions); Response.Cookies.Append(Constants.BASKET_COOKIENAME, userName, cookieOptions);
return userName;
} }
} }
} }

View File

@@ -2,10 +2,8 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.Pages.Basket;
using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.eShopWeb.Web.ViewModels;
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent
@@ -22,25 +20,27 @@ namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent
_signInManager = signInManager; _signInManager = signInManager;
} }
public async Task<IViewComponentResult> InvokeAsync(string userName) public async Task<IViewComponentResult> InvokeAsync()
{ {
var vm = new BasketComponentViewModel(); var vm = new BasketComponentViewModel
vm.ItemsCount = (await GetBasketViewModelAsync()).Items.Sum(i => i.Quantity); {
ItemsCount = await CountTotalBasketItems()
};
return View(vm); return View(vm);
} }
private async Task<BasketViewModel> GetBasketViewModelAsync() private async Task<int> CountTotalBasketItems()
{ {
if (_signInManager.IsSignedIn(HttpContext.User)) if (_signInManager.IsSignedIn(HttpContext.User))
{ {
return await _basketService.GetOrCreateBasketForUser(User.Identity.Name); return await _basketService.CountTotalBasketItems(User.Identity.Name);
} }
string anonymousId = GetAnnonymousIdFromCookie(); string anonymousId = GetAnnonymousIdFromCookie();
if (anonymousId == null) if (anonymousId == null)
return new BasketViewModel(); return 0;
return await _basketService.GetOrCreateBasketForUser(anonymousId); return await _basketService.CountTotalBasketItems(anonymousId);
} }
private string GetAnnonymousIdFromCookie() private string GetAnnonymousIdFromCookie()

View File

@@ -34,18 +34,7 @@ namespace Microsoft.eShopWeb.Web.Services
{ {
return await CreateBasketForUser(userName); return await CreateBasketForUser(userName);
} }
return await CreateViewModelFromBasket(basket); var viewModel = await Map(basket);
}
private async Task<BasketViewModel> CreateViewModelFromBasket(Basket basket)
{
var viewModel = new BasketViewModel
{
Id = basket.Id,
BuyerId = basket.BuyerId,
Items = await GetBasketItems(basket.Items)
};
return viewModel; return viewModel;
} }
@@ -84,5 +73,24 @@ namespace Microsoft.eShopWeb.Web.Services
return items; return items;
} }
public async Task<BasketViewModel> Map(Basket basket)
{
return new BasketViewModel()
{
BuyerId = basket.BuyerId,
Id = basket.Id,
Items = await GetBasketItems(basket.Items)
};
}
public async Task<int> CountTotalBasketItems(string username)
{
var basketSpec = new BasketWithItemsSpecification(username);
var basket = await _basketRepository.GetBySpecAsync(basketSpec);
if (basket == null)
return 0;
return basket.Items.Sum(i => i.Quantity);
}
} }
} }

View File

@@ -35,7 +35,7 @@
</section> </section>
<section class="col-lg-1 col-xs-12"> <section class="col-lg-1 col-xs-12">
@await Component.InvokeAsync("Basket", User.Identity.Name) @await Component.InvokeAsync("Basket")
</section> </section>
} }

View File

@@ -22,7 +22,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes
var basketService = new BasketService(_mockBasketRepo.Object, null); var basketService = new BasketService(_mockBasketRepo.Object, null);
await basketService.AddItemToBasket(basket.Id, 1, 1.50m); await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m);
_mockBasketRepo.Verify(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default), Times.Once); _mockBasketRepo.Verify(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default), Times.Once);
} }
@@ -36,7 +36,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes
var basketService = new BasketService(_mockBasketRepo.Object, null); var basketService = new BasketService(_mockBasketRepo.Object, null);
await basketService.AddItemToBasket(basket.Id, 1, 1.50m); await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m);
_mockBasketRepo.Verify(x => x.UpdateAsync(basket, default), Times.Once); _mockBasketRepo.Verify(x => x.UpdateAsync(basket, default), Times.Once);
} }