From ed5d17672deee127ae8e486b3e3900bb6d40eec4 Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Mon, 28 Aug 2017 12:15:18 -0400 Subject: [PATCH] Ardalis/upgrade1 (#44) * Upgrading to netcore 2.0 Updating repository to support async options and refactoring to use it. * Starting work on tracking customer orders feature. * Cleaning up some bugs Working on basket view component implementation * Fixing up styles, especially for basket in header. --- src/ApplicationCore/ApplicationCore.csproj | 2 +- .../Entities/BuyerAggregate/Buyer.cs | 26 ++ .../Entities/BuyerAggregate/PaymentMethod.cs | 11 + .../Entities/OrderAggregate/Address.cs | 39 +++ .../OrderAggregate/CatalogItemOrdered.cs | 13 + .../Entities/OrderAggregate/Order.cs | 44 +++ .../Interfaces/IAggregateRoot.cs | 5 + .../Interfaces/IAsyncRepository.cs | 16 + .../Interfaces/IImageService.cs | 7 - src/ApplicationCore/Interfaces/IRepository.cs | 7 +- src/Infrastructure/Data/CatalogContextSeed.cs | 29 +- src/Infrastructure/Data/EfRepository.cs | 62 +++- .../FileSystem/LocalFileImageService.cs | 30 -- .../Identity/ApplicationUser.cs | 3 +- src/Infrastructure/Infrastructure.csproj | 19 +- src/Web/Controllers/AccountController.cs | 11 +- src/Web/Interfaces/IBasketService.cs | 2 +- src/Web/Program.cs | 28 +- src/Web/Services/BasketService.cs | 26 +- src/Web/Services/CatalogService.cs | 12 +- src/Web/Startup.cs | 97 +++--- src/Web/ViewComponents/Basket.cs | 27 ++ .../ViewModels/BasketComponentViewModel.cs | 7 + .../Shared/Components/Basket/Default.cshtml | 17 + src/Web/Views/Shared/_Layout.cshtml | 18 +- src/Web/Views/Shared/_LoginPartial.cshtml | 14 +- src/Web/Web.csproj | 36 +-- src/Web/compilerconfig.json | 42 +++ src/Web/wwwroot/css/_variables.scss | 65 ++++ src/Web/wwwroot/css/app.component.css | 11 + src/Web/wwwroot/css/app.component.min.css | 1 + src/Web/wwwroot/css/app.component.scss | 23 ++ .../basket-status/basket-status.component.css | 43 +++ .../basket-status.component.min.css | 1 + .../basket-status.component.scss | 57 ++++ .../wwwroot/css/basket/basket.component.css | 49 +++ .../css/basket/basket.component.min.css | 1 + .../wwwroot/css/basket/basket.component.scss | 89 ++++++ .../wwwroot/css/catalog/catalog.component.css | 295 ++++++------------ .../css/catalog/catalog.component.min.css | 1 + .../css/catalog/catalog.component.scss | 154 +++++++++ tests/FunctionalTests/FunctionalTests.csproj | 4 +- .../LocalFileImageServiceGetImageBytesById.cs | 47 --- .../IntegrationTests/IntegrationTests.csproj | 6 +- tests/UnitTests/UnitTests.csproj | 9 +- 45 files changed, 1024 insertions(+), 482 deletions(-) create mode 100644 src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs create mode 100644 src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs create mode 100644 src/ApplicationCore/Entities/OrderAggregate/Address.cs create mode 100644 src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs create mode 100644 src/ApplicationCore/Entities/OrderAggregate/Order.cs create mode 100644 src/ApplicationCore/Interfaces/IAggregateRoot.cs create mode 100644 src/ApplicationCore/Interfaces/IAsyncRepository.cs delete mode 100644 src/ApplicationCore/Interfaces/IImageService.cs delete mode 100644 src/Infrastructure/FileSystem/LocalFileImageService.cs create mode 100644 src/Web/ViewComponents/Basket.cs create mode 100644 src/Web/ViewModels/BasketComponentViewModel.cs create mode 100644 src/Web/Views/Shared/Components/Basket/Default.cshtml create mode 100644 src/Web/compilerconfig.json create mode 100644 src/Web/wwwroot/css/_variables.scss create mode 100644 src/Web/wwwroot/css/app.component.css create mode 100644 src/Web/wwwroot/css/app.component.min.css create mode 100644 src/Web/wwwroot/css/app.component.scss create mode 100644 src/Web/wwwroot/css/basket/basket-status/basket-status.component.css create mode 100644 src/Web/wwwroot/css/basket/basket-status/basket-status.component.min.css create mode 100644 src/Web/wwwroot/css/basket/basket-status/basket-status.component.scss create mode 100644 src/Web/wwwroot/css/basket/basket.component.css create mode 100644 src/Web/wwwroot/css/basket/basket.component.min.css create mode 100644 src/Web/wwwroot/css/basket/basket.component.scss create mode 100644 src/Web/wwwroot/css/catalog/catalog.component.min.css create mode 100644 src/Web/wwwroot/css/catalog/catalog.component.scss delete mode 100644 tests/IntegrationTests/Infrastructure/File/LocalFileImageServiceGetImageBytesById.cs diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index 35fa64f..e0c49c1 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -1,7 +1,7 @@  - netstandard1.4 + netstandard2.0 diff --git a/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs new file mode 100644 index 0000000..072e12c --- /dev/null +++ b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs @@ -0,0 +1,26 @@ +using ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ApplicationCore.Entities.BuyerAggregate +{ + public class Buyer : BaseEntity, IAggregateRoot + { + public string IdentityGuid { get; private set; } + + private List _paymentMethods = new List(); + + public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); + + protected Buyer() + { + } + + public Buyer(string identity) : this() + { + IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity)); + } + } +} diff --git a/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs new file mode 100644 index 0000000..e807e1c --- /dev/null +++ b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs @@ -0,0 +1,11 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; + +namespace ApplicationCore.Entities.BuyerAggregate +{ + 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; } + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/Address.cs b/src/ApplicationCore/Entities/OrderAggregate/Address.cs new file mode 100644 index 0000000..8f395e9 --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/Address.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace ApplicationCore.Entities.OrderAggregate +{ + public class Address // ValueObject + { + public String Street { get; private set; } + + public String City { get; private set; } + + public String State { get; private set; } + + public String Country { get; private set; } + + public String ZipCode { get; private set; } + + private Address() { } + + public Address(string street, string city, string state, string country, string zipcode) + { + Street = street; + City = city; + State = state; + Country = country; + ZipCode = zipcode; + } + + //protected override IEnumerable GetAtomicValues() + //{ + // yield return Street; + // yield return City; + // yield return State; + // yield return Country; + // yield return ZipCode; + //} + + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs new file mode 100644 index 0000000..bba2327 --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs @@ -0,0 +1,13 @@ +namespace ApplicationCore.Entities.OrderAggregate +{ + /// + /// Represents the item that was ordered. If catalog item details change, details of + /// the item that was part of a completed order should not change. + /// + public class CatalogItemOrdered // ValueObject + { + public string CatalogItemId { get; private set; } + public string ProductName { get; private set; } + public string PictureUri { get; private set; } + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/Order.cs b/src/ApplicationCore/Entities/OrderAggregate/Order.cs new file mode 100644 index 0000000..4f4de52 --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/Order.cs @@ -0,0 +1,44 @@ +using ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ApplicationCore.Entities.OrderAggregate +{ + public class Order : BaseEntity, IAggregateRoot + { + public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now; + public Address ShipToAddress { get; private set; } + + // DDD Patterns comment + // Using a private collection field, better for DDD Aggregate's encapsulation + // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection, + // but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour. + private readonly List _orderItems; + + public IReadOnlyCollection OrderItems => _orderItems; + // Using List<>.AsReadOnly() + // This will create a read only wrapper around the private list so is protected against "external updates". + // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) + //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx + + } + + public class OrderItem : BaseEntity + { + public CatalogItemOrdered ItemOrdered { get; private set; } + public decimal UnitPrice { get; private set; } + public int Units { get; private set; } + + protected OrderItem() + { + } + public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) + { + ItemOrdered = itemOrdered; + UnitPrice = unitPrice; + Units = units; + } + } +} diff --git a/src/ApplicationCore/Interfaces/IAggregateRoot.cs b/src/ApplicationCore/Interfaces/IAggregateRoot.cs new file mode 100644 index 0000000..84e3a15 --- /dev/null +++ b/src/ApplicationCore/Interfaces/IAggregateRoot.cs @@ -0,0 +1,5 @@ +namespace ApplicationCore.Interfaces +{ + public interface IAggregateRoot + { } +} diff --git a/src/ApplicationCore/Interfaces/IAsyncRepository.cs b/src/ApplicationCore/Interfaces/IAsyncRepository.cs new file mode 100644 index 0000000..9bbcefd --- /dev/null +++ b/src/ApplicationCore/Interfaces/IAsyncRepository.cs @@ -0,0 +1,16 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + public interface IAsyncRepository where T : BaseEntity + { + Task GetByIdAsync(int id); + Task> ListAllAsync(); + Task> ListAsync(ISpecification spec); + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(T entity); + } +} diff --git a/src/ApplicationCore/Interfaces/IImageService.cs b/src/ApplicationCore/Interfaces/IImageService.cs deleted file mode 100644 index d2d01c7..0000000 --- a/src/ApplicationCore/Interfaces/IImageService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Interfaces -{ - public interface IImageService - { - byte[] GetImageBytesById(int id); - } -} diff --git a/src/ApplicationCore/Interfaces/IRepository.cs b/src/ApplicationCore/Interfaces/IRepository.cs index a72abad..91f670f 100644 --- a/src/ApplicationCore/Interfaces/IRepository.cs +++ b/src/ApplicationCore/Interfaces/IRepository.cs @@ -1,16 +1,13 @@ using Microsoft.eShopWeb.ApplicationCore.Entities; -using System; using System.Collections.Generic; -using System.Linq.Expressions; namespace ApplicationCore.Interfaces { - public interface IRepository where T : BaseEntity { T GetById(int id); - List List(); - List List(ISpecification spec); + IEnumerable ListAll(); + IEnumerable List(ISpecification spec); T Add(T entity); void Update(T entity); void Delete(T entity); diff --git a/src/Infrastructure/Data/CatalogContextSeed.cs b/src/Infrastructure/Data/CatalogContextSeed.cs index a7c1845..4553684 100644 --- a/src/Infrastructure/Data/CatalogContextSeed.cs +++ b/src/Infrastructure/Data/CatalogContextSeed.cs @@ -10,39 +10,38 @@ namespace Infrastructure.Data { public class CatalogContextSeed { - public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, int? retry = 0) + public static async Task SeedAsync(IApplicationBuilder applicationBuilder, + CatalogContext catalogContext, + ILoggerFactory loggerFactory, int? retry = 0) { int retryForAvailability = retry.Value; try { - var context = (CatalogContext)applicationBuilder - .ApplicationServices.GetService(typeof(CatalogContext)); - // TODO: Only run this if using a real database // context.Database.Migrate(); - if (!context.CatalogBrands.Any()) + if (!catalogContext.CatalogBrands.Any()) { - context.CatalogBrands.AddRange( + catalogContext.CatalogBrands.AddRange( GetPreconfiguredCatalogBrands()); - await context.SaveChangesAsync(); + await catalogContext.SaveChangesAsync(); } - if (!context.CatalogTypes.Any()) + if (!catalogContext.CatalogTypes.Any()) { - context.CatalogTypes.AddRange( + catalogContext.CatalogTypes.AddRange( GetPreconfiguredCatalogTypes()); - await context.SaveChangesAsync(); + await catalogContext.SaveChangesAsync(); } - if (!context.CatalogItems.Any()) + if (!catalogContext.CatalogItems.Any()) { - context.CatalogItems.AddRange( + catalogContext.CatalogItems.AddRange( GetPreconfiguredItems()); - await context.SaveChangesAsync(); + await catalogContext.SaveChangesAsync(); } } catch (Exception ex) @@ -50,9 +49,9 @@ namespace Infrastructure.Data if (retryForAvailability < 10) { retryForAvailability++; - var log = loggerFactory.CreateLogger("catalog seed"); + var log = loggerFactory.CreateLogger(); log.LogError(ex.Message); - await SeedAsync(applicationBuilder, loggerFactory, retryForAvailability); + await SeedAsync(applicationBuilder, catalogContext, loggerFactory, retryForAvailability); } } } diff --git a/src/Infrastructure/Data/EfRepository.cs b/src/Infrastructure/Data/EfRepository.cs index 9c7282f..24bdc0c 100644 --- a/src/Infrastructure/Data/EfRepository.cs +++ b/src/Infrastructure/Data/EfRepository.cs @@ -3,10 +3,16 @@ using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Entities; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Infrastructure.Data { - public class EfRepository : IRepository where T : BaseEntity + /// + /// "There's some repetition here - couldn't we have some the sync methods call the async?" + /// https://blogs.msdn.microsoft.com/pfxteam/2012/04/13/should-i-expose-synchronous-wrappers-for-asynchronous-methods/ + /// + /// + public class EfRepository : IRepository, IAsyncRepository where T : BaseEntity { private readonly CatalogContext _dbContext; @@ -17,22 +23,41 @@ namespace Infrastructure.Data public T GetById(int id) { - return _dbContext.Set().SingleOrDefault(e => e.Id == id); + return _dbContext.Set().Find(id); } - public List List() + public async Task GetByIdAsync(int id) { - return _dbContext.Set().ToList(); + return await _dbContext.Set().FindAsync(id); } - public List List(ISpecification spec) + public IEnumerable ListAll() + { + return _dbContext.Set().AsEnumerable(); + } + + public async Task> ListAllAsync() + { + return await _dbContext.Set().ToListAsync(); + } + + public IEnumerable List(ISpecification spec) { var queryableResultWithIncludes = spec.Includes - .Aggregate(_dbContext.Set().AsQueryable(), + .Aggregate(_dbContext.Set().AsQueryable(), (current, include) => current.Include(include)); return queryableResultWithIncludes .Where(spec.Criteria) - .ToList(); + .AsEnumerable(); + } + public async Task> ListAsync(ISpecification spec) + { + var queryableResultWithIncludes = spec.Includes + .Aggregate(_dbContext.Set().AsQueryable(), + (current, include) => current.Include(include)); + return await queryableResultWithIncludes + .Where(spec.Criteria) + .ToListAsync(); } public T Add(T entity) @@ -43,10 +68,12 @@ namespace Infrastructure.Data return entity; } - public void Delete(T entity) + public async Task AddAsync(T entity) { - _dbContext.Set().Remove(entity); - _dbContext.SaveChanges(); + _dbContext.Set().Add(entity); + await _dbContext.SaveChangesAsync(); + + return entity; } public void Update(T entity) @@ -54,6 +81,21 @@ namespace Infrastructure.Data _dbContext.Entry(entity).State = EntityState.Modified; _dbContext.SaveChanges(); } + public async Task UpdateAsync(T entity) + { + _dbContext.Entry(entity).State = EntityState.Modified; + await _dbContext.SaveChangesAsync(); + } + public void Delete(T entity) + { + _dbContext.Set().Remove(entity); + _dbContext.SaveChanges(); + } + public async Task DeleteAsync(T entity) + { + _dbContext.Set().Remove(entity); + await _dbContext.SaveChangesAsync(); + } } } diff --git a/src/Infrastructure/FileSystem/LocalFileImageService.cs b/src/Infrastructure/FileSystem/LocalFileImageService.cs deleted file mode 100644 index 82db8b2..0000000 --- a/src/Infrastructure/FileSystem/LocalFileImageService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ApplicationCore.Exceptions; -using ApplicationCore.Interfaces; -using Microsoft.AspNetCore.Hosting; -using System.IO; - -namespace Infrastructure.FileSystem -{ - public class LocalFileImageService : IImageService - { - private readonly IHostingEnvironment _env; - - public LocalFileImageService(IHostingEnvironment env) - { - _env = env; - } - public byte[] GetImageBytesById(int id) - { - try - { - var contentRoot = _env.ContentRootPath + "//Pics"; - var path = Path.Combine(contentRoot, id + ".png"); - return File.ReadAllBytes(path); - } - catch (FileNotFoundException ex) - { - throw new CatalogImageMissingException(ex); - } - } - } -} diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index a8099f3..99ee879 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; namespace Infrastructure.Identity diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 06ff46f..ad865bf 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -1,26 +1,19 @@  - netstandard1.4 + netcoreapp2.0 - - - - - - - - - + + - - - + + + diff --git a/src/Web/Controllers/AccountController.cs b/src/Web/Controllers/AccountController.cs index 1623971..1c7b974 100644 --- a/src/Web/Controllers/AccountController.cs +++ b/src/Web/Controllers/AccountController.cs @@ -3,12 +3,11 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; using Infrastructure.Identity; using System; -using Microsoft.eShopWeb.ApplicationCore.Entities; using ApplicationCore.Interfaces; using Web; +using Microsoft.AspNetCore.Authentication; namespace Microsoft.eShopWeb.Controllers { @@ -17,18 +16,15 @@ namespace Microsoft.eShopWeb.Controllers { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly string _externalCookieScheme; private readonly IBasketService _basketService; public AccountController( UserManager userManager, SignInManager signInManager, - IOptions identityCookieOptions, IBasketService basketService) { _userManager = userManager; _signInManager = signInManager; - _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme; _basketService = basketService; } @@ -37,7 +33,7 @@ namespace Microsoft.eShopWeb.Controllers [AllowAnonymous] public async Task SignIn(string returnUrl = null) { - await HttpContext.Authentication.SignOutAsync(_externalCookieScheme); + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ViewData["ReturnUrl"] = returnUrl; return View(); @@ -61,7 +57,7 @@ namespace Microsoft.eShopWeb.Controllers string anonymousBasketId = Request.Cookies[Constants.BASKET_COOKIENAME]; if (!String.IsNullOrEmpty(anonymousBasketId)) { - _basketService.TransferBasket(anonymousBasketId, model.Email); + await _basketService.TransferBasketAsync(anonymousBasketId, model.Email); Response.Cookies.Delete(Constants.BASKET_COOKIENAME); } return RedirectToLocal(returnUrl); @@ -74,7 +70,6 @@ namespace Microsoft.eShopWeb.Controllers [ValidateAntiForgeryToken] public async Task SignOut() { - HttpContext.Session.Clear(); await _signInManager.SignOutAsync(); return RedirectToAction(nameof(CatalogController.Index), "Catalog"); diff --git a/src/Web/Interfaces/IBasketService.cs b/src/Web/Interfaces/IBasketService.cs index 7794e7c..1cdbd2c 100644 --- a/src/Web/Interfaces/IBasketService.cs +++ b/src/Web/Interfaces/IBasketService.cs @@ -6,7 +6,7 @@ namespace ApplicationCore.Interfaces public interface IBasketService { Task GetOrCreateBasketForUser(string userName); - Task TransferBasket(string anonymousId, string userName); + Task TransferBasketAsync(string anonymousId, string userName); Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity); Task Checkout(int basketId); } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index b8c01c5..20289be 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,28 +1,20 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; namespace Microsoft.eShopWeb { public class Program { + public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseKestrel() - .UseUrls("http://0.0.0.0:5106") - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureLogging(factory => - { - factory.AddConsole(LogLevel.Warning); - factory.AddDebug(); - }) - .UseIISIntegration() - .UseStartup() - .UseApplicationInsights() - .Build(); - - host.Run(); + BuildWebHost(args).Run(); } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://0.0.0.0:5106") + .UseStartup() + .Build(); } } diff --git a/src/Web/Services/BasketService.cs b/src/Web/Services/BasketService.cs index 5e74e22..3e0e859 100644 --- a/src/Web/Services/BasketService.cs +++ b/src/Web/Services/BasketService.cs @@ -1,7 +1,6 @@ using ApplicationCore.Interfaces; using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Entities; -using System; using System.Linq; using Microsoft.eShopWeb.ViewModels; using System.Collections.Generic; @@ -11,11 +10,11 @@ namespace Web.Services { public class BasketService : IBasketService { - private readonly IRepository _basketRepository; + private readonly IAsyncRepository _basketRepository; private readonly IUriComposer _uriComposer; private readonly IRepository _itemRepository; - public BasketService(IRepository basketRepository, + public BasketService(IAsyncRepository basketRepository, IRepository itemRepository, IUriComposer uriComposer) { @@ -27,7 +26,7 @@ namespace Web.Services public async Task GetOrCreateBasketForUser(string userName) { var basketSpec = new BasketWithItemsSpecification(userName); - var basket = _basketRepository.List(basketSpec).FirstOrDefault(); + var basket = (await _basketRepository.ListAsync(basketSpec)).FirstOrDefault(); if(basket == null) { @@ -63,7 +62,7 @@ namespace Web.Services public async Task CreateBasketForUser(string userId) { var basket = new Basket() { BuyerId = userId }; - _basketRepository.Add(basket); + await _basketRepository.AddAsync(basket); return new BasketViewModel() { @@ -75,30 +74,29 @@ namespace Web.Services public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity) { - var basket = _basketRepository.GetById(basketId); + var basket = await _basketRepository.GetByIdAsync(basketId); basket.AddItem(catalogItemId, price, quantity); - _basketRepository.Update(basket); + await _basketRepository.UpdateAsync(basket); } public async Task Checkout(int basketId) { - var basket = _basketRepository.GetById(basketId); + var basket = await _basketRepository.GetByIdAsync(basketId); // TODO: Actually Process the order - _basketRepository.Delete(basket); + await _basketRepository.DeleteAsync(basket); } - public Task TransferBasket(string anonymousId, string userName) + public async Task TransferBasketAsync(string anonymousId, string userName) { var basketSpec = new BasketWithItemsSpecification(anonymousId); - var basket = _basketRepository.List(basketSpec).FirstOrDefault(); - if (basket == null) return Task.CompletedTask; + var basket = (await _basketRepository.ListAsync(basketSpec)).FirstOrDefault(); + if (basket == null) return; basket.BuyerId = userName; - _basketRepository.Update(basket); - return Task.CompletedTask; + await _basketRepository.UpdateAsync(basket); } } } diff --git a/src/Web/Services/CatalogService.cs b/src/Web/Services/CatalogService.cs index 1c1446f..8049b1f 100644 --- a/src/Web/Services/CatalogService.cs +++ b/src/Web/Services/CatalogService.cs @@ -15,15 +15,15 @@ namespace Microsoft.eShopWeb.Services { private readonly ILogger _logger; private readonly IRepository _itemRepository; - private readonly IRepository _brandRepository; - private readonly IRepository _typeRepository; + private readonly IAsyncRepository _brandRepository; + private readonly IAsyncRepository _typeRepository; private readonly IUriComposer _uriComposer; public CatalogService( ILoggerFactory loggerFactory, IRepository itemRepository, - IRepository brandRepository, - IRepository typeRepository, + IAsyncRepository brandRepository, + IAsyncRepository typeRepository, IUriComposer uriComposer) { _logger = loggerFactory.CreateLogger(); @@ -83,7 +83,7 @@ namespace Microsoft.eShopWeb.Services public async Task> GetBrands() { _logger.LogInformation("GetBrands called."); - var brands = _brandRepository.List(); + var brands = await _brandRepository.ListAllAsync(); var items = new List { @@ -100,7 +100,7 @@ namespace Microsoft.eShopWeb.Services public async Task> GetTypes() { _logger.LogInformation("GetTypes called."); - var types = _typeRepository.List(); + var types = await _typeRepository.ListAllAsync(); var items = new List { new SelectListItem() { Value = null, Text = "All", Selected = true } diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index 31de781..ec3d628 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -1,41 +1,32 @@ -using Microsoft.eShopWeb.Services; +using ApplicationCore.Interfaces; +using ApplicationCore.Services; +using Infrastructure.Data; +using Infrastructure.Identity; +using Infrastructure.Logging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.eShopWeb.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Infrastructure.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using System.Text; -using Microsoft.AspNetCore.Http; -using ApplicationCore.Interfaces; -using Infrastructure.FileSystem; -using Infrastructure.Logging; -using Microsoft.AspNetCore.Identity; using Web.Services; -using ApplicationCore.Services; -using Infrastructure.Data; namespace Microsoft.eShopWeb { public class Startup { private IServiceCollection _services; - public Startup(IHostingEnvironment env) + public Startup(IConfiguration configuration) { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); + Configuration = configuration; } - public IConfigurationRoot Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Requires LocalDB which can be installed with SQL Server Express 2016 @@ -46,11 +37,6 @@ namespace Microsoft.eShopWeb { c.UseInMemoryDatabase("Catalog"); //c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")); - c.ConfigureWarnings(wb => - { - //By default, in this application, we don't want to have client evaluations - wb.Log(RelationalEventId.QueryClientEvaluationWarning); - }); } catch (System.Exception ex ) { @@ -68,6 +54,7 @@ namespace Microsoft.eShopWeb .AddDefaultTokenProviders(); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); + services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); services.AddMemoryCache(); services.AddScoped(); @@ -76,12 +63,8 @@ namespace Microsoft.eShopWeb services.Configure(Configuration); services.AddSingleton(new UriComposer(Configuration.Get())); - // TODO: Remove - services.AddSingleton(); - services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); - // Add memory cache services services.AddMemoryCache(); @@ -98,25 +81,8 @@ namespace Microsoft.eShopWeb { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); - - app.Map("/allservices", builder => builder.Run(async context => - { - var sb = new StringBuilder(); - sb.Append("

All Services

"); - sb.Append(""); - sb.Append(""); - sb.Append(""); - foreach (var svc in _services) - { - sb.Append(""); - sb.Append($""); - sb.Append($""); - sb.Append($""); - sb.Append(""); - } - sb.Append("
TypeLifetimeInstance
{svc.ServiceType.FullName}{svc.Lifetime}{svc.ImplementationType?.FullName}
"); - await context.Response.WriteAsync(sb.ToString()); - })); + ListAllRegisteredServices(app); + app.UseDatabaseErrorPage(); } else { @@ -124,20 +90,43 @@ namespace Microsoft.eShopWeb } app.UseStaticFiles(); - app.UseIdentity(); + app.UseAuthentication(); app.UseMvc(); } + private void ListAllRegisteredServices(IApplicationBuilder app) + { + app.Map("/allservices", builder => builder.Run(async context => + { + var sb = new StringBuilder(); + sb.Append("

All Services

"); + sb.Append(""); + sb.Append(""); + sb.Append(""); + foreach (var svc in _services) + { + sb.Append(""); + sb.Append($""); + sb.Append($""); + sb.Append($""); + sb.Append(""); + } + sb.Append("
TypeLifetimeInstance
{svc.ServiceType.FullName}{svc.Lifetime}{svc.ImplementationType?.FullName}
"); + await context.Response.WriteAsync(sb.ToString()); + })); + } + public void ConfigureDevelopment(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, - UserManager userManager) + UserManager userManager, + CatalogContext catalogContext) { Configure(app, env); //Seed Data - CatalogContextSeed.SeedAsync(app, loggerFactory) + CatalogContextSeed.SeedAsync(app, catalogContext, loggerFactory) .Wait(); var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; @@ -155,17 +144,17 @@ namespace Microsoft.eShopWeb public void ConfigureProduction(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, - UserManager userManager) + UserManager userManager, + CatalogContext catalogContext) { Configure(app, env); //Seed Data - CatalogContextSeed.SeedAsync(app, loggerFactory) + CatalogContextSeed.SeedAsync(app, catalogContext, loggerFactory) .Wait(); var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; userManager.CreateAsync(defaultUser, "Pass@word1").Wait(); - } } } diff --git a/src/Web/ViewComponents/Basket.cs b/src/Web/ViewComponents/Basket.cs new file mode 100644 index 0000000..990e917 --- /dev/null +++ b/src/Web/ViewComponents/Basket.cs @@ -0,0 +1,27 @@ +using ApplicationCore.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopWeb.ViewModels; +using System.Threading.Tasks; + +namespace Web.ViewComponents +{ + public class Basket : ViewComponent + { + private readonly IBasketService _cartSvc; + + public Basket(IBasketService cartSvc) => _cartSvc = cartSvc; + + public async Task InvokeAsync(string userName) + { + var vm = new BasketComponentViewModel(); + var itemsInCart = await ItemsInBasketAsync(userName); + vm.ItemsCount = itemsInCart; + return View(vm); + } + private async Task ItemsInBasketAsync(string userName) + { + var basket = await _cartSvc.GetOrCreateBasketForUser(userName); + return basket.Items.Count; + } + } +} diff --git a/src/Web/ViewModels/BasketComponentViewModel.cs b/src/Web/ViewModels/BasketComponentViewModel.cs new file mode 100644 index 0000000..56725a5 --- /dev/null +++ b/src/Web/ViewModels/BasketComponentViewModel.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopWeb.ViewModels +{ + public class BasketComponentViewModel + { + public int ItemsCount { get; set; } + } +} diff --git a/src/Web/Views/Shared/Components/Basket/Default.cshtml b/src/Web/Views/Shared/Components/Basket/Default.cshtml new file mode 100644 index 0000000..c919cf5 --- /dev/null +++ b/src/Web/Views/Shared/Components/Basket/Default.cshtml @@ -0,0 +1,17 @@ +@model BasketComponentViewModel + +@{ + ViewData["Title"] = "My Basket"; +} + + +
+ +
+
+ @Model.ItemsCount +
+
diff --git a/src/Web/Views/Shared/_Layout.cshtml b/src/Web/Views/Shared/_Layout.cshtml index 85d989e..cb0b16b 100644 --- a/src/Web/Views/Shared/_Layout.cshtml +++ b/src/Web/Views/Shared/_Layout.cshtml @@ -7,7 +7,19 @@ - + + + @* + + + *@ + + + + @* + + + *@ - - + +