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:
@@ -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,
|
||||||
|
|||||||
@@ -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>>>();
|
||||||
|
|
||||||
|
|||||||
@@ -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
7
src/Web/Constants.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Web
|
||||||
|
{
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string BASKET_COOKIENAME = "eShop";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
|||||||
86
src/Web/Controllers/BasketController.cs
Normal file
86
src/Web/Controllers/BasketController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user