Basket persistence (#41)

* Renamed Cart to Basket throughout
* Implemented cookie-based anonymous basket handling and transfer to user upon login. Still need to implement transfer upon registration.
This commit is contained in:
Steve Smith
2017-08-22 13:51:08 -04:00
committed by GitHub
parent 3a95375ae7
commit ecb4889dd3
10 changed files with 173 additions and 118 deletions

View File

@@ -6,13 +6,14 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities
public class Basket : BaseEntity public class Basket : BaseEntity
{ {
public string BuyerId { get; set; } public string BuyerId { get; set; }
public List<BasketItem> Items { get; set; } = new List<BasketItem>(); private readonly List<BasketItem> _items = new List<BasketItem>();
public IEnumerable<BasketItem> Items => _items.ToList();
public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1) public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1)
{ {
if (!Items.Any(i => i.CatalogItemId == catalogItemId)) if (!Items.Any(i => i.CatalogItemId == catalogItemId))
{ {
Items.Add(new BasketItem() _items.Add(new BasketItem()
{ {
CatalogItemId = catalogItemId, CatalogItemId = catalogItemId,
Quantity = quantity, Quantity = quantity,

View File

@@ -13,10 +13,18 @@ namespace ApplicationCore.Specifications
BasketId = basketId; BasketId = basketId;
AddInclude(b => b.Items); AddInclude(b => b.Items);
} }
public BasketWithItemsSpecification(string buyerId)
{
BuyerId = buyerId;
AddInclude(b => b.Items);
}
public int BasketId { get; } public int? BasketId { get; }
public string BuyerId { get; }
public Expression<Func<Basket, bool>> Criteria => b => b.Id == BasketId; public Expression<Func<Basket, bool>> Criteria => b =>
(BasketId.HasValue && b.Id == BasketId.Value)
|| (BuyerId != null && b.BuyerId == BuyerId);
public List<Expression<Func<Basket, object>>> Includes { get; } = new List<Expression<Func<Basket, object>>>(); public List<Expression<Func<Basket, object>>> Includes { get; } = new List<Expression<Func<Basket, object>>>();

View File

@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore; using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Infrastructure.Data namespace Infrastructure.Data
{ {
@@ -14,13 +16,22 @@ namespace Infrastructure.Data
public DbSet<CatalogItem> CatalogItems { get; set; } public DbSet<CatalogItem> CatalogItems { get; set; }
public DbSet<CatalogBrand> CatalogBrands { get; set; } public DbSet<CatalogBrand> CatalogBrands { get; set; }
public DbSet<CatalogType> CatalogTypes { get; set; } public DbSet<CatalogType> CatalogTypes { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
builder.Entity<Basket>(ConfigureBasket);
builder.Entity<CatalogBrand>(ConfigureCatalogBrand); builder.Entity<CatalogBrand>(ConfigureCatalogBrand);
builder.Entity<CatalogType>(ConfigureCatalogType); builder.Entity<CatalogType>(ConfigureCatalogType);
builder.Entity<CatalogItem>(ConfigureCatalogItem); builder.Entity<CatalogItem>(ConfigureCatalogItem);
} }
private void ConfigureBasket(EntityTypeBuilder<Basket> builder)
{
var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items));
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
}
void ConfigureCatalogItem(EntityTypeBuilder<CatalogItem> builder) void ConfigureCatalogItem(EntityTypeBuilder<CatalogItem> builder)
{ {
builder.ToTable("Catalog"); builder.ToTable("Catalog");

7
src/Web/Constants.cs Normal file
View File

@@ -0,0 +1,7 @@
namespace Web
{
public static class Constants
{
public const string BASKET_COOKIENAME = "eShop";
}
}

View File

@@ -5,6 +5,10 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Infrastructure.Identity; using Infrastructure.Identity;
using System;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using ApplicationCore.Interfaces;
using Web;
namespace Microsoft.eShopWeb.Controllers namespace Microsoft.eShopWeb.Controllers
{ {
@@ -14,17 +18,18 @@ namespace Microsoft.eShopWeb.Controllers
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;
private readonly string _externalCookieScheme; private readonly string _externalCookieScheme;
private readonly IBasketService _basketService;
public AccountController( public AccountController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager, SignInManager<ApplicationUser> signInManager,
IOptions<IdentityCookieOptions> identityCookieOptions IOptions<IdentityCookieOptions> identityCookieOptions,
IBasketService basketService)
)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
_externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme; _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
_basketService = basketService;
} }
// GET: /Account/SignIn // GET: /Account/SignIn
@@ -53,6 +58,12 @@ namespace Microsoft.eShopWeb.Controllers
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded) if (result.Succeeded)
{ {
string anonymousBasketId = Request.Cookies[Constants.BASKET_COOKIENAME];
if (!String.IsNullOrEmpty(anonymousBasketId))
{
_basketService.TransferBasket(anonymousBasketId, model.Email);
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);
}
return RedirectToLocal(returnUrl); return RedirectToLocal(returnUrl);
} }
ModelState.AddModelError(string.Empty, "Invalid login attempt."); ModelState.AddModelError(string.Empty, "Invalid login attempt.");

View File

@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using ApplicationCore.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopWeb.ViewModels;
using Microsoft.AspNetCore.Identity;
using Infrastructure.Identity;
using System;
using Web;
namespace Microsoft.eShopWeb.Controllers
{
[Route("[controller]/[action]")]
public class BasketController : Controller
{
private readonly IBasketService _basketService;
private const string _basketSessionKey = "basketId";
private readonly IUriComposer _uriComposer;
private readonly SignInManager<ApplicationUser> _signInManager;
public BasketController(IBasketService basketService,
IUriComposer uriComposer,
SignInManager<ApplicationUser> signInManager)
{
_basketService = basketService;
_uriComposer = uriComposer;
_signInManager = signInManager;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var basketModel = await GetBasketViewModelAsync();
return View(basketModel);
}
// POST: /Basket/AddToBasket
[HttpPost]
public async Task<IActionResult> AddToBasket(CatalogItemViewModel productDetails)
{
if (productDetails?.Id == null)
{
return RedirectToAction("Index", "Catalog");
}
var basketViewModel = await GetBasketViewModelAsync();
await _basketService.AddItemToBasket(basketViewModel.Id, productDetails.Id, productDetails.Price, 1);
return RedirectToAction("Index");
}
[HttpPost]
public async Task<IActionResult> Checkout()
{
var basket = await GetBasketViewModelAsync();
await _basketService.Checkout(basket.Id);
return View("Checkout");
}
private async Task<BasketViewModel> GetBasketViewModelAsync()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
return await _basketService.GetOrCreateBasketForUser(User.Identity.Name);
}
string anonymousId = GetOrSetBasketCookie();
return await _basketService.GetOrCreateBasketForUser(anonymousId);
}
private string GetOrSetBasketCookie()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
return Request.Cookies[Constants.BASKET_COOKIENAME];
}
string anonymousId = Guid.NewGuid().ToString();
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append(Constants.BASKET_COOKIENAME, anonymousId, cookieOptions);
return anonymousId;
}
}
}

View File

@@ -1,72 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using ApplicationCore.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopWeb.ViewModels;
namespace Microsoft.eShopWeb.Controllers
{
[Route("[controller]/[action]")]
public class BasketController : Controller
{
private readonly IBasketService _basketService;
private const string _basketSessionKey = "basketId";
private readonly IUriComposer _uriComposer;
public BasketController(IBasketService basketService,
IUriComposer uriComposer)
{
_basketService = basketService;
_uriComposer = uriComposer;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var basketModel = await GetBasketFromSessionAsync();
return View(basketModel);
}
// POST: /Basket/AddToBasket
[HttpPost]
public async Task<IActionResult> AddToBasket(CatalogItemViewModel productDetails)
{
if (productDetails?.Id == null)
{
return RedirectToAction("Index", "Catalog");
}
var basket = await GetBasketFromSessionAsync();
await _basketService.AddItemToBasket(basket.Id, productDetails.Id, productDetails.Price, 1);
return RedirectToAction("Index");
}
[HttpPost]
public async Task<IActionResult> Checkout()
{
var basket = await GetBasketFromSessionAsync();
await _basketService.Checkout(basket.Id);
return View("Checkout");
}
private async Task<BasketViewModel> GetBasketFromSessionAsync()
{
string basketId = HttpContext.Session.GetString(_basketSessionKey);
BasketViewModel basket = null;
if (basketId == null)
{
basket = await _basketService.CreateBasketForUser(User.Identity.Name);
HttpContext.Session.SetString(_basketSessionKey, basket.Id.ToString());
}
else
{
basket = await _basketService.GetBasket(int.Parse(basketId));
}
return basket;
}
}
}

View File

@@ -5,12 +5,9 @@ namespace ApplicationCore.Interfaces
{ {
public interface IBasketService public interface IBasketService
{ {
Task<BasketViewModel> GetBasket(int basketId); Task<BasketViewModel> GetOrCreateBasketForUser(string userName);
Task<BasketViewModel> CreateBasket(); Task TransferBasket(string anonymousId, string userName);
Task<BasketViewModel> CreateBasketForUser(string userId);
Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity); Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity);
Task Checkout(int basketId); Task Checkout(int basketId);
} }
} }

View File

@@ -23,15 +23,21 @@ namespace Web.Services
_uriComposer = uriComposer; _uriComposer = uriComposer;
_itemRepository = itemRepository; _itemRepository = itemRepository;
} }
public async Task<BasketViewModel> GetBasket(int basketId)
public async Task<BasketViewModel> GetOrCreateBasketForUser(string userName)
{ {
var basketSpec = new BasketWithItemsSpecification(basketId); var basketSpec = new BasketWithItemsSpecification(userName);
var basket = _basketRepository.List(basketSpec).FirstOrDefault(); var basket = _basketRepository.List(basketSpec).FirstOrDefault();
if(basket == null) if(basket == null)
{ {
return await CreateBasket(); return await CreateBasketForUser(userName);
}
return CreateViewModelFromBasket(basket);
} }
private BasketViewModel CreateViewModelFromBasket(Basket basket)
{
var viewModel = new BasketViewModel(); var viewModel = new BasketViewModel();
viewModel.Id = basket.Id; viewModel.Id = basket.Id;
viewModel.BuyerId = basket.BuyerId; viewModel.BuyerId = basket.BuyerId;
@@ -54,11 +60,6 @@ namespace Web.Services
return viewModel; return viewModel;
} }
public Task<BasketViewModel> CreateBasket()
{
return CreateBasketForUser(null);
}
public async Task<BasketViewModel> CreateBasketForUser(string userId) public async Task<BasketViewModel> CreateBasketForUser(string userId)
{ {
var basket = new Basket() { BuyerId = userId }; var basket = new Basket() { BuyerId = userId };
@@ -89,5 +90,15 @@ namespace Web.Services
_basketRepository.Delete(basket); _basketRepository.Delete(basket);
} }
public Task TransferBasket(string anonymousId, string userName)
{
var basketSpec = new BasketWithItemsSpecification(anonymousId);
var basket = _basketRepository.List(basketSpec).FirstOrDefault();
if (basket == null) return Task.CompletedTask;
basket.BuyerId = userName;
_basketRepository.Update(basket);
return Task.CompletedTask;
}
} }
} }

View File

@@ -85,9 +85,6 @@ namespace Microsoft.eShopWeb
// Add memory cache services // Add memory cache services
services.AddMemoryCache(); services.AddMemoryCache();
// Add session related services.
services.AddSession();
services.AddMvc(); services.AddMvc();
_services = services; _services = services;
@@ -126,10 +123,7 @@ namespace Microsoft.eShopWeb
app.UseExceptionHandler("/Catalog/Error"); app.UseExceptionHandler("/Catalog/Error");
} }
app.UseSession();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseIdentity(); app.UseIdentity();
app.UseMvc(); app.UseMvc();
@@ -171,6 +165,7 @@ namespace Microsoft.eShopWeb
var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" };
userManager.CreateAsync(defaultUser, "Pass@word1").Wait(); userManager.CreateAsync(defaultUser, "Pass@word1").Wait();
} }
} }
} }