Skip to content

Commit

Permalink
Basket persistence (dotnet-architecture#41)
Browse files Browse the repository at this point in the history
* Renamed Cart to Basket throughout
* Implemented cookie-based anonymous basket handling and transfer to user upon login. Still need to implement transfer upon registration.
  • Loading branch information
ardalis authored Aug 22, 2017
1 parent 3a95375 commit ecb4889
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 114 deletions.
5 changes: 3 additions & 2 deletions src/ApplicationCore/Entities/Basket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities
public class Basket : BaseEntity
{
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)
{
if (!Items.Any(i => i.CatalogItemId == catalogItemId))
{
Items.Add(new BasketItem()
_items.Add(new BasketItem()
{
CatalogItemId = catalogItemId,
Quantity = quantity,
Expand Down
12 changes: 10 additions & 2 deletions src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ public BasketWithItemsSpecification(int basketId)
BasketId = basketId;
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>>>();

Expand Down
13 changes: 12 additions & 1 deletion src/Infrastructure/Data/CatalogContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore;
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Infrastructure.Data
{
Expand All @@ -14,13 +16,22 @@ public CatalogContext(DbContextOptions<CatalogContext> options) : base(options)
public DbSet<CatalogItem> CatalogItems { get; set; }
public DbSet<CatalogBrand> CatalogBrands { get; set; }
public DbSet<CatalogType> CatalogTypes { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Basket>(ConfigureBasket);
builder.Entity<CatalogBrand>(ConfigureCatalogBrand);
builder.Entity<CatalogType>(ConfigureCatalogType);
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)
{
builder.ToTable("Catalog");
Expand Down
7 changes: 7 additions & 0 deletions src/Web/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Web
{
public static class Constants
{
public const string BASKET_COOKIENAME = "eShop";
}
}
19 changes: 15 additions & 4 deletions src/Web/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Infrastructure.Identity;
using System;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using ApplicationCore.Interfaces;
using Web;

namespace Microsoft.eShopWeb.Controllers
{
Expand All @@ -14,20 +18,21 @@ public class AccountController : Controller
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly string _externalCookieScheme;
private readonly IBasketService _basketService;

public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityCookieOptions> identityCookieOptions

)
IOptions<IdentityCookieOptions> identityCookieOptions,
IBasketService basketService)
{
_userManager = userManager;
_signInManager = signInManager;
_externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
_basketService = basketService;
}

// GET: /Account/SignIn
// GET: /Account/SignIn
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> SignIn(string returnUrl = null)
Expand All @@ -53,6 +58,12 @@ public async Task<IActionResult> SignIn(LoginViewModel model, string returnUrl =
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
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);
}
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
Expand Down
86 changes: 86 additions & 0 deletions src/Web/Controllers/BasketController.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
72 changes: 0 additions & 72 deletions src/Web/Controllers/CartController.cs

This file was deleted.

7 changes: 2 additions & 5 deletions src/Web/Interfaces/IBasketService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ namespace ApplicationCore.Interfaces
{
public interface IBasketService
{
Task<BasketViewModel> GetBasket(int basketId);
Task<BasketViewModel> CreateBasket();
Task<BasketViewModel> CreateBasketForUser(string userId);

Task<BasketViewModel> GetOrCreateBasketForUser(string userName);
Task TransferBasket(string anonymousId, string userName);
Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity);

Task Checkout(int basketId);
}
}
55 changes: 33 additions & 22 deletions src/Web/Services/BasketService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,43 @@ public BasketService(IRepository<Basket> basketRepository,
_uriComposer = uriComposer;
_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();
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();
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 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;
})
};
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 };
Expand Down Expand Up @@ -89,5 +90,15 @@ public async Task Checkout(int basketId)

_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;
}
}
}
Loading

0 comments on commit ecb4889

Please sign in to comment.