diff --git a/README.md b/README.md index deeb006..27329da 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ You can also run the samples in Docker (see below). } ``` -1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance. +2. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance. -2. Open a command prompt in the Web folder and execute the following commands: +3. Open a command prompt in the Web folder and execute the following commands: ``` dotnet restore @@ -68,7 +68,7 @@ dotnet ef database update -c appidentitydbcontext -p ../Infrastructure/Infrastru These commands will create two separate databases, one for the store's catalog data and shopping cart information, and one for the app's user credentials and identity data. -3. Run the application. +4. Run the application. The first time you run the application, it will seed both databases with data such that you should see products in the store, and you should be able to log in using the demouser@microsoft.com account. Note: If you need to create migrations, you can use these commands: diff --git a/docker-compose.yml b/docker-compose.yml index c62033b..5499146 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development ports: - - "5107:5107" + - "5107:80" eshopwebmvc: image: eshopwebmvc @@ -19,7 +19,7 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development ports: - - "5106:5106" + - "5106:80" networks: default: diff --git a/docs/Architecting Modern Web Applications with ASP.NET Core and Azure (v2.0).epub b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure (v2.0).epub new file mode 100644 index 0000000..9a37c05 Binary files /dev/null and b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure (v2.0).epub differ diff --git a/docs/Architecting Modern Web Applications with ASP.NET Core and Azure (v2.0).mobi b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure (v2.0).mobi new file mode 100644 index 0000000..3000552 Binary files /dev/null and b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure (v2.0).mobi differ diff --git a/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.epub b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.epub index 9a37c05..13d350d 100644 Binary files a/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.epub and b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.epub differ diff --git a/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.mobi b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.mobi index 3000552..21ed9c1 100644 Binary files a/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.mobi and b/docs/Architecting Modern Web Applications with ASP.NET Core and Azure.mobi differ diff --git a/eShopOnWeb.sln b/eShopOnWeb.sln index ab44fde..609ebbc 100644 --- a/eShopOnWeb.sln +++ b/eShopOnWeb.sln @@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{419A6ACE-041 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{7C461394-ABDC-43CD-A798-71249C58BA67}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{9CB6566E-E86A-4C07-BB8D-E0B95BCD4BD2}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationCore", "src\ApplicationCore\ApplicationCore.csproj", "{7FED7440-2311-4D1E-958B-3E887C585CD2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{15EA4737-125B-4E6E-A806-E13B7EBCDCCF}" @@ -19,13 +17,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "tests\I EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionalTests", "tests\FunctionalTests\FunctionalTests.csproj", "{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebRazorPages", "src\WebRazorPages\WebRazorPages.csproj", "{3CA62E98-218E-4A74-BF79-1098900BD421}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0BD72BEA-EF42-4B72-8B69-12A39EC76FBA}" ProjectSection(SolutionItems) = preProject docker-compose.yml = docker-compose.yml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{227CF035-29B0-448D-97E4-944F9EA850E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,10 +34,6 @@ Global {7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.Build.0 = Release|Any CPU - {9CB6566E-E86A-4C07-BB8D-E0B95BCD4BD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CB6566E-E86A-4C07-BB8D-E0B95BCD4BD2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CB6566E-E86A-4C07-BB8D-E0B95BCD4BD2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CB6566E-E86A-4C07-BB8D-E0B95BCD4BD2}.Release|Any CPU.Build.0 = Release|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -56,22 +50,21 @@ Global {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.Build.0 = Release|Any CPU - {3CA62E98-218E-4A74-BF79-1098900BD421}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3CA62E98-218E-4A74-BF79-1098900BD421}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3CA62E98-218E-4A74-BF79-1098900BD421}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3CA62E98-218E-4A74-BF79-1098900BD421}.Release|Any CPU.Build.0 = Release|Any CPU + {227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {7C461394-ABDC-43CD-A798-71249C58BA67} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} - {9CB6566E-E86A-4C07-BB8D-E0B95BCD4BD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {7FED7440-2311-4D1E-958B-3E887C585CD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} - {3CA62E98-218E-4A74-BF79-1098900BD421} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} + {227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B} diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index c24f5e0..da1721e 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -1,13 +1,13 @@  - - netstandard2.0 - Microsoft.eShopWeb.ApplicationCore - + + netstandard2.0 + Microsoft.eShopWeb.ApplicationCore + - - - - + + + + \ No newline at end of file diff --git a/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs index 15f336c..5705eb1 100644 --- a/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs +++ b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs @@ -1,6 +1,5 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Ardalis.GuardClauses; -using Microsoft.eShopWeb.ApplicationCore.Entities; using System.Collections.Generic; namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate diff --git a/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs index 0bb9df4..631ed4e 100644 --- a/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs +++ b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs @@ -1,6 +1,4 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; - -namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate +namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate { public class PaymentMethod : BaseEntity { diff --git a/src/ApplicationCore/Entities/CatalogBrand.cs b/src/ApplicationCore/Entities/CatalogBrand.cs index fc8be2f..1c90dfa 100644 --- a/src/ApplicationCore/Entities/CatalogBrand.cs +++ b/src/ApplicationCore/Entities/CatalogBrand.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Microsoft.eShopWeb.ApplicationCore.Entities +namespace Microsoft.eShopWeb.ApplicationCore.Entities { public class CatalogBrand : BaseEntity { diff --git a/src/ApplicationCore/Entities/OrderAggregate/Order.cs b/src/ApplicationCore/Entities/OrderAggregate/Order.cs index c8be0f1..4c2f92e 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/Order.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/Order.cs @@ -1,6 +1,5 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Ardalis.GuardClauses; -using Microsoft.eShopWeb.ApplicationCore.Entities; using System; using System.Collections.Generic; diff --git a/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs index 5557791..4c60432 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs @@ -1,6 +1,4 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; - -namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate +namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate { public class OrderItem : BaseEntity diff --git a/src/ApplicationCore/Interfaces/IAsyncRepository.cs b/src/ApplicationCore/Interfaces/IAsyncRepository.cs index dac6b14..459f607 100644 --- a/src/ApplicationCore/Interfaces/IAsyncRepository.cs +++ b/src/ApplicationCore/Interfaces/IAsyncRepository.cs @@ -7,10 +7,11 @@ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces public interface IAsyncRepository where T : BaseEntity { Task GetByIdAsync(int id); - Task> ListAllAsync(); - Task> ListAsync(ISpecification spec); + Task> ListAllAsync(); + Task> ListAsync(ISpecification spec); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); + Task CountAsync(ISpecification spec); } } diff --git a/src/ApplicationCore/Interfaces/IOrderRepository.cs b/src/ApplicationCore/Interfaces/IOrderRepository.cs index 6469cf0..f6081e1 100644 --- a/src/ApplicationCore/Interfaces/IOrderRepository.cs +++ b/src/ApplicationCore/Interfaces/IOrderRepository.cs @@ -4,9 +4,8 @@ using System.Threading.Tasks; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces { - public interface IOrderRepository : IRepository, IAsyncRepository + public interface IOrderRepository : IAsyncRepository { - Order GetByIdWithItems(int id); Task GetByIdWithItemsAsync(int id); } } diff --git a/src/ApplicationCore/Interfaces/IRepository.cs b/src/ApplicationCore/Interfaces/IRepository.cs index 6d56ec1..f7bf513 100644 --- a/src/ApplicationCore/Interfaces/IRepository.cs +++ b/src/ApplicationCore/Interfaces/IRepository.cs @@ -12,5 +12,6 @@ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces T Add(T entity); void Update(T entity); void Delete(T entity); + int Count(ISpecification spec); } } diff --git a/src/ApplicationCore/Interfaces/ISpecification.cs b/src/ApplicationCore/Interfaces/ISpecification.cs index 27e388f..a61420f 100644 --- a/src/ApplicationCore/Interfaces/ISpecification.cs +++ b/src/ApplicationCore/Interfaces/ISpecification.cs @@ -9,5 +9,11 @@ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces Expression> Criteria { get; } List>> Includes { get; } List IncludeStrings { get; } + Expression> OrderBy { get; } + Expression> OrderByDescending { get; } + + int Take { get; } + int Skip { get; } + bool isPagingEnabled { get;} } } diff --git a/src/ApplicationCore/Services/BasketService.cs b/src/ApplicationCore/Services/BasketService.cs index e31bbf9..7d3011a 100644 --- a/src/ApplicationCore/Services/BasketService.cs +++ b/src/ApplicationCore/Services/BasketService.cs @@ -12,6 +12,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services public class BasketService : IBasketService { private readonly IAsyncRepository _basketRepository; + private readonly IAsyncRepository _basketItemRepository; private readonly IUriComposer _uriComposer; private readonly IAppLogger _logger; private readonly IRepository _itemRepository; @@ -19,12 +20,14 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services public BasketService(IAsyncRepository basketRepository, IRepository itemRepository, IUriComposer uriComposer, - IAppLogger logger) + IAppLogger logger, + IAsyncRepository basketItemRepository) { _basketRepository = basketRepository; _uriComposer = uriComposer; - this._logger = logger; + _logger = logger; _itemRepository = itemRepository; + _basketItemRepository = basketItemRepository; } public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity) @@ -40,6 +43,11 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services { var basket = await _basketRepository.GetByIdAsync(basketId); + foreach (var item in basket.Items.ToList()) + { + await _basketItemRepository.DeleteAsync(item); + } + await _basketRepository.DeleteAsync(basket); } diff --git a/src/ApplicationCore/Services/UriComposer.cs b/src/ApplicationCore/Services/UriComposer.cs index cf312b3..a0f7609 100644 --- a/src/ApplicationCore/Services/UriComposer.cs +++ b/src/ApplicationCore/Services/UriComposer.cs @@ -1,5 +1,4 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb; namespace Microsoft.eShopWeb.ApplicationCore.Services { diff --git a/src/ApplicationCore/Specifications/BaseSpecification.cs b/src/ApplicationCore/Specifications/BaseSpecification.cs index beaa21c..1017f3d 100644 --- a/src/ApplicationCore/Specifications/BaseSpecification.cs +++ b/src/ApplicationCore/Specifications/BaseSpecification.cs @@ -14,6 +14,12 @@ namespace Microsoft.eShopWeb.ApplicationCore.Specifications public Expression> Criteria { get; } public List>> Includes { get; } = new List>>(); public List IncludeStrings { get; } = new List(); + public Expression> OrderBy { get; private set; } + public Expression> OrderByDescending { get; private set; } + + public int Take { get; private set; } + public int Skip { get; private set; } + public bool isPagingEnabled { get; private set; } = false; protected virtual void AddInclude(Expression> includeExpression) { @@ -23,5 +29,19 @@ namespace Microsoft.eShopWeb.ApplicationCore.Specifications { IncludeStrings.Add(includeString); } + protected virtual void ApplyPaging(int skip, int take) + { + Skip = skip; + Take = take; + isPagingEnabled = true; + } + protected virtual void ApplyOrderBy(Expression> orderByExpression) + { + OrderBy = orderByExpression; + } + protected virtual void ApplyOrderByDescending(Expression> orderByDescendingExpression) + { + OrderByDescending = orderByDescendingExpression; + } } } diff --git a/src/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs b/src/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs new file mode 100644 index 0000000..f24b8c6 --- /dev/null +++ b/src/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs @@ -0,0 +1,14 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; + +namespace Microsoft.eShopWeb.ApplicationCore.Specifications +{ + public class CatalogFilterPaginatedSpecification : BaseSpecification + { + public CatalogFilterPaginatedSpecification(int skip, int take, int? brandId, int? typeId) + : base(i => (!brandId.HasValue || i.CatalogBrandId == brandId) && + (!typeId.HasValue || i.CatalogTypeId == typeId)) + { + ApplyPaging(skip, take); + } + } +} diff --git a/src/Infrastructure/Data/CatalogContextSeed.cs b/src/Infrastructure/Data/CatalogContextSeed.cs index 24c4b8b..afa6349 100644 --- a/src/Infrastructure/Data/CatalogContextSeed.cs +++ b/src/Infrastructure/Data/CatalogContextSeed.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; diff --git a/src/Infrastructure/Data/EfRepository.cs b/src/Infrastructure/Data/EfRepository.cs index 968d4c1..4fe8c60 100644 --- a/src/Infrastructure/Data/EfRepository.cs +++ b/src/Infrastructure/Data/EfRepository.cs @@ -31,7 +31,6 @@ namespace Microsoft.eShopWeb.Infrastructure.Data return List(spec).FirstOrDefault(); } - public virtual async Task GetByIdAsync(int id) { return await _dbContext.Set().FindAsync(id); @@ -42,44 +41,28 @@ namespace Microsoft.eShopWeb.Infrastructure.Data return _dbContext.Set().AsEnumerable(); } - public async Task> ListAllAsync() + public async Task> ListAllAsync() { return await _dbContext.Set().ToListAsync(); } public IEnumerable List(ISpecification spec) { - // fetch a Queryable that includes all expression-based includes - var queryableResultWithIncludes = spec.Includes - .Aggregate(_dbContext.Set().AsQueryable(), - (current, include) => current.Include(include)); - - // modify the IQueryable to include any string-based include statements - var secondaryResult = spec.IncludeStrings - .Aggregate(queryableResultWithIncludes, - (current, include) => current.Include(include)); - - // return the result of the query using the specification's criteria expression - return secondaryResult - .Where(spec.Criteria) - .AsEnumerable(); + return ApplySpecification(spec).AsEnumerable(); } - public async Task> ListAsync(ISpecification spec) + public async Task> ListAsync(ISpecification spec) { - // fetch a Queryable that includes all expression-based includes - var queryableResultWithIncludes = spec.Includes - .Aggregate(_dbContext.Set().AsQueryable(), - (current, include) => current.Include(include)); - - // modify the IQueryable to include any string-based include statements - var secondaryResult = spec.IncludeStrings - .Aggregate(queryableResultWithIncludes, - (current, include) => current.Include(include)); - - // return the result of the query using the specification's criteria expression - return await secondaryResult - .Where(spec.Criteria) - .ToListAsync(); + return await ApplySpecification(spec).ToListAsync(); + } + + public int Count(ISpecification spec) + { + return ApplySpecification(spec).Count(); + } + + public async Task CountAsync(ISpecification spec) + { + return await ApplySpecification(spec).CountAsync(); } public T Add(T entity) @@ -103,6 +86,7 @@ namespace Microsoft.eShopWeb.Infrastructure.Data _dbContext.Entry(entity).State = EntityState.Modified; _dbContext.SaveChanges(); } + public async Task UpdateAsync(T entity) { _dbContext.Entry(entity).State = EntityState.Modified; @@ -114,10 +98,16 @@ namespace Microsoft.eShopWeb.Infrastructure.Data _dbContext.Set().Remove(entity); _dbContext.SaveChanges(); } + public async Task DeleteAsync(T entity) { _dbContext.Set().Remove(entity); await _dbContext.SaveChangesAsync(); } + + private IQueryable ApplySpecification(ISpecification spec) + { + return SpecificationEvaluator.GetQuery(_dbContext.Set().AsQueryable(), spec); + } } } diff --git a/src/Infrastructure/Data/Migrations/20171018175735_Initial.cs b/src/Infrastructure/Data/Migrations/20171018175735_Initial.cs index e971ac8..e0f1f76 100644 --- a/src/Infrastructure/Data/Migrations/20171018175735_Initial.cs +++ b/src/Infrastructure/Data/Migrations/20171018175735_Initial.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using System; -using System.Collections.Generic; namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations { diff --git a/src/Infrastructure/Data/Migrations/CatalogContextModelSnapshot.cs b/src/Infrastructure/Data/Migrations/CatalogContextModelSnapshot.cs index 03cd76e..c186289 100644 --- a/src/Infrastructure/Data/Migrations/CatalogContextModelSnapshot.cs +++ b/src/Infrastructure/Data/Migrations/CatalogContextModelSnapshot.cs @@ -1,5 +1,4 @@ -// -using System; +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/src/Infrastructure/Data/OrderRepository.cs b/src/Infrastructure/Data/OrderRepository.cs index 23f92d7..8e873bb 100644 --- a/src/Infrastructure/Data/OrderRepository.cs +++ b/src/Infrastructure/Data/OrderRepository.cs @@ -1,7 +1,6 @@ using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.EntityFrameworkCore; -using System.Linq; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Infrastructure.Data @@ -12,20 +11,12 @@ namespace Microsoft.eShopWeb.Infrastructure.Data { } - public Order GetByIdWithItems(int id) - { - return _dbContext.Orders - .Include(o => o.OrderItems) - .Include($"{nameof(Order.OrderItems)}.{nameof(OrderItem.ItemOrdered)}") - .FirstOrDefault(); - } - public Task GetByIdWithItemsAsync(int id) { return _dbContext.Orders .Include(o => o.OrderItems) .Include($"{nameof(Order.OrderItems)}.{nameof(OrderItem.ItemOrdered)}") - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(x => x.Id == id); } } } diff --git a/src/Infrastructure/Data/SpecificationEvaluator.cs b/src/Infrastructure/Data/SpecificationEvaluator.cs new file mode 100644 index 0000000..88910d2 --- /dev/null +++ b/src/Infrastructure/Data/SpecificationEvaluator.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.eShopWeb.Infrastructure.Data +{ + public class SpecificationEvaluator where T : BaseEntity + { + public static IQueryable GetQuery(IQueryable inputQuery, ISpecification specification) + { + var query = inputQuery; + + // modify the IQueryable using the specification's criteria expression + if (specification.Criteria != null) + { + query = query.Where(specification.Criteria); + } + + // Includes all expression-based includes + query = specification.Includes.Aggregate(query, + (current, include) => current.Include(include)); + + // Include any string-based include statements + query = specification.IncludeStrings.Aggregate(query, + (current, include) => current.Include(include)); + + // Apply ordering if expressions are set + if (specification.OrderBy != null) + { + query = query.OrderBy(specification.OrderBy); + } + else if (specification.OrderByDescending != null) + { + query = query.OrderByDescending(specification.OrderByDescending); + } + + // Apply paging if enabled + if (specification.isPagingEnabled) + { + query = query.Skip(specification.Skip) + .Take(specification.Take); + } + return query; + } + } +} diff --git a/src/Infrastructure/Identity/Migrations/20170822214310_InitialIdentityModel.cs b/src/Infrastructure/Identity/Migrations/20170822214310_InitialIdentityModel.cs index 6c146e7..4272bc5 100644 --- a/src/Infrastructure/Identity/Migrations/20170822214310_InitialIdentityModel.cs +++ b/src/Infrastructure/Identity/Migrations/20170822214310_InitialIdentityModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs b/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs index fffdaac..e672c18 100644 --- a/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.eShopWeb.Infrastructure.Identity; namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations { diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 0259dc6..d4b15fc 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -1,20 +1,20 @@  - - netstandard2.0 - Microsoft.eShopWeb.Infrastructure - + + netstandard2.0 + Microsoft.eShopWeb.Infrastructure + - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Web/.bowerrc b/src/Web/.bowerrc deleted file mode 100644 index 6406626..0000000 --- a/src/Web/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "wwwroot/lib" -} diff --git a/src/Web/Areas/Identity/Pages/_ViewStart.cshtml b/src/Web/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..c4284f6 --- /dev/null +++ b/src/Web/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} diff --git a/src/Web/Constants.cs b/src/Web/Constants.cs index fbc5855..3117820 100644 --- a/src/Web/Constants.cs +++ b/src/Web/Constants.cs @@ -3,5 +3,7 @@ public static class Constants { public const string BASKET_COOKIENAME = "eShop"; + public const int ITEMS_PER_PAGE = 10; + public const string DEFAULT_USERNAME = "Guest"; } } diff --git a/src/Web/Controllers/AccountController.cs b/src/Web/Controllers/AccountController.cs index f3dfbf1..281497b 100644 --- a/src/Web/Controllers/AccountController.cs +++ b/src/Web/Controllers/AccountController.cs @@ -1,17 +1,18 @@ -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb.Infrastructure.Identity; -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.ViewModels.Account; using System; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Controllers { + [ApiExplorerSettings(IgnoreApi = true)] [Route("[controller]/[action]")] - [Authorize] + [Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages public class AccountController : Controller { private readonly UserManager _userManager; @@ -149,10 +150,11 @@ namespace Microsoft.eShopWeb.Web.Controllers { await _signInManager.SignOutAsync(); - return RedirectToAction(nameof(CatalogController.Index), "Catalog"); + return RedirectToPage("/Index"); } [AllowAnonymous] + [HttpGet] public IActionResult Register() { return View(); @@ -184,7 +186,7 @@ namespace Microsoft.eShopWeb.Web.Controllers { if (userId == null || code == null) { - return RedirectToAction(nameof(CatalogController.Index), "Catalog"); + return RedirectToPage("/Index"); } var user = await _userManager.FindByIdAsync(userId); if (user == null) @@ -215,7 +217,7 @@ namespace Microsoft.eShopWeb.Web.Controllers } else { - return RedirectToAction(nameof(CatalogController.Index), "Catalog"); + return RedirectToPage("/Index"); } } diff --git a/src/Web/Controllers/Api/BaseApiController.cs b/src/Web/Controllers/Api/BaseApiController.cs index 3d0bb2d..dbf2837 100644 --- a/src/Web/Controllers/Api/BaseApiController.cs +++ b/src/Web/Controllers/Api/BaseApiController.cs @@ -1,10 +1,9 @@ -using Microsoft.eShopWeb.Web.Services; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; namespace Microsoft.eShopWeb.Web.Controllers.Api { [Route("api/[controller]/[action]")] + [ApiController] public class BaseApiController : Controller { } } diff --git a/src/Web/Controllers/BasketController.cs b/src/Web/Controllers/BasketController.cs deleted file mode 100644 index 97fdf43..0000000 --- a/src/Web/Controllers/BasketController.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.AspNetCore.Http; -using Microsoft.eShopWeb.Web.ViewModels; -using Microsoft.AspNetCore.Identity; -using Microsoft.eShopWeb.Infrastructure.Identity; -using System; -using System.Collections.Generic; -using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; -using Microsoft.AspNetCore.Authorization; -using Microsoft.eShopWeb.Web.Interfaces; - -namespace Microsoft.eShopWeb.Web.Controllers -{ - [Route("[controller]/[action]")] - public class BasketController : Controller - { - private readonly IBasketService _basketService; - private readonly IUriComposer _uriComposer; - private readonly SignInManager _signInManager; - private readonly IAppLogger _logger; - private readonly IOrderService _orderService; - private readonly IBasketViewModelService _basketViewModelService; - - public BasketController(IBasketService basketService, - IBasketViewModelService basketViewModelService, - IOrderService orderService, - IUriComposer uriComposer, - SignInManager signInManager, - IAppLogger logger) - { - _basketService = basketService; - _uriComposer = uriComposer; - _signInManager = signInManager; - _logger = logger; - _orderService = orderService; - _basketViewModelService = basketViewModelService; - } - - [HttpGet] - public async Task Index() - { - var basketModel = await GetBasketViewModelAsync(); - - return View(basketModel); - } - - [HttpPost] - public async Task Index(Dictionary items) - { - var basketViewModel = await GetBasketViewModelAsync(); - await _basketService.SetQuantities(basketViewModel.Id, items); - - return View(await GetBasketViewModelAsync()); - } - - - // POST: /Basket/AddToBasket - [HttpPost] - public async Task 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] - [Authorize] - public async Task Checkout(Dictionary items) - { - var basketViewModel = await GetBasketViewModelAsync(); - await _basketService.SetQuantities(basketViewModel.Id, items); - - await _orderService.CreateOrderAsync(basketViewModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); - - await _basketService.DeleteBasketAsync(basketViewModel.Id); - - return View("Checkout"); - } - - private async Task GetBasketViewModelAsync() - { - if (_signInManager.IsSignedIn(HttpContext.User)) - { - return await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name); - } - string anonymousId = GetOrSetBasketCookie(); - return await _basketViewModelService.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; - } - } -} diff --git a/src/Web/Controllers/CatalogController.cs b/src/Web/Controllers/CatalogController.cs deleted file mode 100644 index d7fa4b5..0000000 --- a/src/Web/Controllers/CatalogController.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.eShopWeb.Web.Services; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; - -namespace Microsoft.eShopWeb.Web.Controllers -{ - [Route("")] - public class CatalogController : Controller - { - private readonly ICatalogService _catalogService; - - public CatalogController(ICatalogService catalogService) => _catalogService = catalogService; - - [HttpGet] - [HttpPost] - public async Task Index(int? brandFilterApplied, int? typesFilterApplied, int? page) - { - var itemsPage = 10; - var catalogModel = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied); - return View(catalogModel); - } - - [HttpGet("Error")] - public IActionResult Error() - { - return View(); - } - } -} diff --git a/src/Web/Controllers/ManageController.cs b/src/Web/Controllers/ManageController.cs index ff21d58..4f6886e 100644 --- a/src/Web/Controllers/ManageController.cs +++ b/src/Web/Controllers/ManageController.cs @@ -1,11 +1,11 @@ -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb.Infrastructure.Identity; -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.Web.ViewModels.Manage; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Services; +using Microsoft.eShopWeb.Web.ViewModels.Manage; using System; using System.Linq; using System.Text; @@ -14,7 +14,8 @@ using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Controllers { - [Authorize] + [ApiExplorerSettings(IgnoreApi = true)] + [Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages [Route("[controller]/[action]")] public class ManageController : Controller { @@ -44,7 +45,7 @@ namespace Microsoft.eShopWeb.Web.Controllers public string StatusMessage { get; set; } [HttpGet] - public async Task Index() + public async Task MyAccount() { var user = await _userManager.GetUserAsync(User); if (user == null) diff --git a/src/Web/Controllers/OrderController.cs b/src/Web/Controllers/OrderController.cs index acff1f7..b60cd32 100644 --- a/src/Web/Controllers/OrderController.cs +++ b/src/Web/Controllers/OrderController.cs @@ -1,26 +1,27 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.eShopWeb.Web.ViewModels; -using System; -using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Specifications; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Linq; +using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Controllers { - [Authorize] + [ApiExplorerSettings(IgnoreApi = true)] + [Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages [Route("[controller]/[action]")] public class OrderController : Controller { private readonly IOrderRepository _orderRepository; - public OrderController(IOrderRepository orderRepository) { + public OrderController(IOrderRepository orderRepository) + { _orderRepository = orderRepository; } - - public async Task Index() + + [HttpGet()] + public async Task MyOrders() { var orders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name)); diff --git a/src/Web/Data/ApplicationDbContext.cs b/src/Web/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..ae1405f --- /dev/null +++ b/src/Web/Data/ApplicationDbContext.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Web2.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/src/Web/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/Web/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..7b549de --- /dev/null +++ b/src/Web/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,236 @@ +// +using System; +using Web2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Web2.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.0-preview1") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..f75aae2 --- /dev/null +++ b/src/Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,220 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Web2.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(maxLength: 128, nullable: false), + ProviderKey = table.Column(maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(maxLength: 128, nullable: false), + Name = table.Column(maxLength: 128, nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Web/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Web/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..e468eb0 --- /dev/null +++ b/src/Web/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,234 @@ +// +using System; +using Web2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Web2.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.0-preview1") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Dockerfile b/src/Web/Dockerfile index 835b401..11b81f3 100644 --- a/src/Web/Dockerfile +++ b/src/Web/Dockerfile @@ -7,7 +7,7 @@ # # RUN COMMAND # docker run --name eshopweb --rm -it -p 8000:5106 web -FROM microsoft/dotnet:2.1-sdk AS build +FROM microsoft/dotnet:2.2-sdk AS build WORKDIR /app COPY *.sln . @@ -17,7 +17,7 @@ RUN dotnet restore RUN dotnet publish -c Release -o out -FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime +FROM microsoft/dotnet:2.2-aspnetcore-runtime AS runtime WORKDIR /app COPY --from=build /app/src/Web/out ./ diff --git a/src/Web/HealthChecks/ApiHealthCheck.cs b/src/Web/HealthChecks/ApiHealthCheck.cs new file mode 100644 index 0000000..989fe99 --- /dev/null +++ b/src/Web/HealthChecks/ApiHealthCheck.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.HealthChecks +{ + public class ApiHealthCheck : IHealthCheck + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly LinkGenerator _linkGenerator; + + public ApiHealthCheck(IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator) + { + _httpContextAccessor = httpContextAccessor; + _linkGenerator = linkGenerator; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default(CancellationToken)) + { + var request = _httpContextAccessor.HttpContext.Request; + + string apiLink = _linkGenerator.GetPathByAction("List", "Catalog"); + string myUrl = request.Scheme + "://" + request.Host.ToString() + apiLink; + var client = new HttpClient(); + var response = await client.GetAsync(myUrl); + var pageContents = await response.Content.ReadAsStringAsync(); + if (pageContents.Contains(".NET Bot Black Sweatshirt")) + { + return HealthCheckResult.Healthy("The check indicates a healthy result."); + } + + return HealthCheckResult.Unhealthy("The check indicates an unhealthy result."); + } + } +} diff --git a/src/Web/HealthChecks/HomePageHealthCheck.cs b/src/Web/HealthChecks/HomePageHealthCheck.cs new file mode 100644 index 0000000..f858a55 --- /dev/null +++ b/src/Web/HealthChecks/HomePageHealthCheck.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.HealthChecks +{ + public class HomePageHealthCheck : IHealthCheck + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public HomePageHealthCheck(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default(CancellationToken)) + { + var request = _httpContextAccessor.HttpContext.Request; + string myUrl = request.Scheme + "://" + request.Host.ToString(); + + var client = new HttpClient(); + var response = await client.GetAsync(myUrl); + var pageContents = await response.Content.ReadAsStringAsync(); + if (pageContents.Contains(".NET Bot Black Sweatshirt")) + { + return HealthCheckResult.Healthy("The check indicates a healthy result."); + } + + return HealthCheckResult.Unhealthy("The check indicates an unhealthy result."); + } + } +} diff --git a/src/Web/Interfaces/IBasketService.cs b/src/Web/Interfaces/IBasketViewModelService.cs similarity index 81% rename from src/Web/Interfaces/IBasketService.cs rename to src/Web/Interfaces/IBasketViewModelService.cs index 7a5c845..3be5c84 100644 --- a/src/Web/Interfaces/IBasketService.cs +++ b/src/Web/Interfaces/IBasketViewModelService.cs @@ -1,4 +1,4 @@ -using Microsoft.eShopWeb.Web.ViewModels; +using Microsoft.eShopWeb.Web.Pages.Basket; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Interfaces diff --git a/src/Web/Models/ErrorViewModel.cs b/src/Web/Models/ErrorViewModel.cs new file mode 100644 index 0000000..e8187a1 --- /dev/null +++ b/src/Web/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace Web2.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} \ No newline at end of file diff --git a/src/Web/ViewModels/BasketItemViewModel.cs b/src/Web/Pages/Basket/BasketItemViewModel.cs similarity index 88% rename from src/Web/ViewModels/BasketItemViewModel.cs rename to src/Web/Pages/Basket/BasketItemViewModel.cs index 5f74436..129f0e2 100644 --- a/src/Web/ViewModels/BasketItemViewModel.cs +++ b/src/Web/Pages/Basket/BasketItemViewModel.cs @@ -1,4 +1,4 @@ -namespace Microsoft.eShopWeb.Web.ViewModels +namespace Microsoft.eShopWeb.Web.Pages.Basket { public class BasketItemViewModel { diff --git a/src/Web/ViewModels/BasketViewModel.cs b/src/Web/Pages/Basket/BasketViewModel.cs similarity index 90% rename from src/Web/ViewModels/BasketViewModel.cs rename to src/Web/Pages/Basket/BasketViewModel.cs index 901bef9..264df2b 100644 --- a/src/Web/ViewModels/BasketViewModel.cs +++ b/src/Web/Pages/Basket/BasketViewModel.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.eShopWeb.Web.ViewModels +namespace Microsoft.eShopWeb.Web.Pages.Basket { public class BasketViewModel { diff --git a/src/Web/Pages/Basket/Checkout.cshtml b/src/Web/Pages/Basket/Checkout.cshtml new file mode 100644 index 0000000..01e0a78 --- /dev/null +++ b/src/Web/Pages/Basket/Checkout.cshtml @@ -0,0 +1,16 @@ +@page + @model CheckoutModel +@{ + ViewData["Title"] = "Checkout Complete"; +} +
+
+ +
+
+ +
+

Thanks for your Order!

+ + Continue Shopping... +
diff --git a/src/Web/Pages/Basket/Checkout.cshtml.cs b/src/Web/Pages/Basket/Checkout.cshtml.cs new file mode 100644 index 0000000..5646f11 --- /dev/null +++ b/src/Web/Pages/Basket/Checkout.cshtml.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.eShopWeb.Web.Interfaces; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Pages.Basket +{ + public class CheckoutModel : PageModel + { + private readonly IBasketService _basketService; + private readonly IUriComposer _uriComposer; + private readonly SignInManager _signInManager; + private readonly IOrderService _orderService; + private string _username = null; + private readonly IBasketViewModelService _basketViewModelService; + + public CheckoutModel(IBasketService basketService, + IBasketViewModelService basketViewModelService, + IUriComposer uriComposer, + SignInManager signInManager, + IOrderService orderService) + { + _basketService = basketService; + _uriComposer = uriComposer; + _signInManager = signInManager; + _orderService = orderService; + _basketViewModelService = basketViewModelService; + } + + public BasketViewModel BasketModel { get; set; } = new BasketViewModel(); + + public void OnGet() + { + } + + public async Task OnPost(Dictionary items) + { + await SetBasketModelAsync(); + + await _basketService.SetQuantities(BasketModel.Id, items); + + await _orderService.CreateOrderAsync(BasketModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); + + await _basketService.DeleteBasketAsync(BasketModel.Id); + + return RedirectToPage(); + } + + private async Task SetBasketModelAsync() + { + if (_signInManager.IsSignedIn(HttpContext.User)) + { + BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name); + } + else + { + GetOrSetBasketCookieAndUserName(); + BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username); + } + } + + private void GetOrSetBasketCookieAndUserName() + { + if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) + { + _username = Request.Cookies[Constants.BASKET_COOKIENAME]; + } + if (_username != null) return; + + _username = Guid.NewGuid().ToString(); + var cookieOptions = new CookieOptions(); + cookieOptions.Expires = DateTime.Today.AddYears(10); + Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions); + } + } +} diff --git a/src/Web/Views/Basket/Index.cshtml b/src/Web/Pages/Basket/Index.cshtml similarity index 62% rename from src/Web/Views/Basket/Index.cshtml rename to src/Web/Pages/Basket/Index.cshtml index 17e0ecf..c1dc5f4 100644 --- a/src/Web/Views/Basket/Index.cshtml +++ b/src/Web/Pages/Basket/Index.cshtml @@ -1,17 +1,17 @@ -@using Microsoft.eShopWeb.Web.ViewModels -@model BasketViewModel +@page "{handler?}" +@model IndexModel @{ ViewData["Title"] = "Basket"; }
- +
- @if (Model.Items.Any()) + @if (Model.BasketModel.Items.Any()) {
@@ -23,9 +23,9 @@
Cost
- @for (int i=0; i< Model.Items.Count; i++) + @for (int i = 0; i < Model.BasketModel.Items.Count; i++) { - var item = Model.Items[i]; + var item = Model.BasketModel.Items[i];
@@ -44,32 +44,36 @@
@*
- @item.ProductId -
*@ + @item.ProductId +
*@ -
-
-
-
Total
-
- -
-
-
$ @Model.Total()
-
- -
-
-
- @**@ -
-
-
} + +
+
+
+
Total
+
+ +
+
+
$ @Model.BasketModel.Total().ToString("N2")
+
+ +
+
+
+ @**@ +
+
+
+
- + [ Update ] + +
diff --git a/src/Web/Pages/Basket/Index.cshtml.cs b/src/Web/Pages/Basket/Index.cshtml.cs new file mode 100644 index 0000000..a31fcde --- /dev/null +++ b/src/Web/Pages/Basket/Index.cshtml.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.eShopWeb.Web.Interfaces; +using Microsoft.eShopWeb.Web.ViewModels; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Pages.Basket +{ + public class IndexModel : PageModel + { + private readonly IBasketService _basketService; + private const string _basketSessionKey = "basketId"; + private readonly IUriComposer _uriComposer; + private readonly SignInManager _signInManager; + private string _username = null; + private readonly IBasketViewModelService _basketViewModelService; + + public IndexModel(IBasketService basketService, + IBasketViewModelService basketViewModelService, + IUriComposer uriComposer, + SignInManager signInManager) + { + _basketService = basketService; + _uriComposer = uriComposer; + _signInManager = signInManager; + _basketViewModelService = basketViewModelService; + } + + public BasketViewModel BasketModel { get; set; } = new BasketViewModel(); + + public async Task OnGet() + { + await SetBasketModelAsync(); + } + + public async Task OnPost(CatalogItemViewModel productDetails) + { + if (productDetails?.Id == null) + { + return RedirectToPage("/Index"); + } + await SetBasketModelAsync(); + + await _basketService.AddItemToBasket(BasketModel.Id, productDetails.Id, productDetails.Price, 1); + + await SetBasketModelAsync(); + + return RedirectToPage(); + } + + public async Task OnPostUpdate(Dictionary items) + { + await SetBasketModelAsync(); + await _basketService.SetQuantities(BasketModel.Id, items); + + await SetBasketModelAsync(); + } + + private async Task SetBasketModelAsync() + { + if (_signInManager.IsSignedIn(HttpContext.User)) + { + BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name); + } + else + { + GetOrSetBasketCookieAndUserName(); + BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username); + } + } + + private void GetOrSetBasketCookieAndUserName() + { + if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) + { + _username = Request.Cookies[Constants.BASKET_COOKIENAME]; + } + if (_username != null) return; + + _username = Guid.NewGuid().ToString(); + var cookieOptions = new CookieOptions { IsEssential = true }; + cookieOptions.Expires = DateTime.Today.AddYears(10); + Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions); + } + } +} diff --git a/src/Web/Pages/Error.cshtml b/src/Web/Pages/Error.cshtml new file mode 100644 index 0000000..6f92b95 --- /dev/null +++ b/src/Web/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/src/Web/Pages/Error.cshtml.cs b/src/Web/Pages/Error.cshtml.cs new file mode 100644 index 0000000..5ec6f2e --- /dev/null +++ b/src/Web/Pages/Error.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Diagnostics; + +namespace Microsoft.eShopWeb.Web.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/src/Web/Views/Catalog/Index.cshtml b/src/Web/Pages/Index.cshtml similarity index 51% rename from src/Web/Views/Catalog/Index.cshtml rename to src/Web/Pages/Index.cshtml index 76db5ce..2c3e9fb 100644 --- a/src/Web/Views/Catalog/Index.cshtml +++ b/src/Web/Pages/Index.cshtml @@ -1,43 +1,40 @@ -@{ +@page +@{ ViewData["Title"] = "Catalog"; - @model CatalogIndexViewModel + @model IndexModel }
- +
-
- +
-
- - @if (Model.CatalogItems.Any()) + @if (Model.CatalogModel.CatalogItems.Any()) { - +
- @foreach (var catalogItem in Model.CatalogItems) + @foreach (var catalogItem in Model.CatalogModel.CatalogItems) {
- +
}
- - + } else { diff --git a/src/Web/Pages/Index.cshtml.cs b/src/Web/Pages/Index.cshtml.cs new file mode 100644 index 0000000..7e32c30 --- /dev/null +++ b/src/Web/Pages/Index.cshtml.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.Web.Services; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Pages +{ + public class IndexModel : PageModel + { + private readonly ICatalogService _catalogService; + + public IndexModel(ICatalogService catalogService) + { + _catalogService = catalogService; + } + + public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); + + public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) + { + CatalogModel = await _catalogService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); + } + + + } +} diff --git a/src/Web/Pages/Privacy.cshtml b/src/Web/Pages/Privacy.cshtml new file mode 100644 index 0000000..46ba966 --- /dev/null +++ b/src/Web/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/src/Web/Pages/Privacy.cshtml.cs b/src/Web/Pages/Privacy.cshtml.cs new file mode 100644 index 0000000..b3f4e58 --- /dev/null +++ b/src/Web/Pages/Privacy.cshtml.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Microsoft.eShopWeb.Web.Pages +{ + public class PrivacyModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/src/Web/ViewComponents/Basket.cs b/src/Web/Pages/Shared/Components/BasketComponent/Basket.cs similarity index 88% rename from src/Web/ViewComponents/Basket.cs rename to src/Web/Pages/Shared/Components/BasketComponent/Basket.cs index 9cfa0e1..f1155dd 100644 --- a/src/Web/ViewComponents/Basket.cs +++ b/src/Web/Pages/Shared/Components/BasketComponent/Basket.cs @@ -1,14 +1,13 @@ -using Microsoft.eShopWeb.Infrastructure.Identity; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb; +using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Interfaces; +using Microsoft.eShopWeb.Web.Pages.Basket; using Microsoft.eShopWeb.Web.ViewModels; using System.Linq; using System.Threading.Tasks; -namespace Microsoft.eShopWeb.Web.ViewComponents +namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent { public class Basket : ViewComponent { diff --git a/src/Web/Pages/Shared/Components/BasketComponent/Default.cshtml b/src/Web/Pages/Shared/Components/BasketComponent/Default.cshtml new file mode 100644 index 0000000..b2a53f6 --- /dev/null +++ b/src/Web/Pages/Shared/Components/BasketComponent/Default.cshtml @@ -0,0 +1,13 @@ +@model BasketComponentViewModel +@{ + ViewData["Title"] = "My Basket"; +} + +
+ +
+
+ @Model.ItemsCount +
+
diff --git a/src/Web/Views/Catalog/_pagination.cshtml b/src/Web/Pages/Shared/_pagination.cshtml similarity index 78% rename from src/Web/Views/Catalog/_pagination.cshtml rename to src/Web/Pages/Shared/_pagination.cshtml index 6ba9f4f..ab5ecee 100644 --- a/src/Web/Views/Catalog/_pagination.cshtml +++ b/src/Web/Pages/Shared/_pagination.cshtml @@ -7,9 +7,7 @@
@@ -24,9 +22,7 @@
diff --git a/src/Web/Views/Catalog/_product.cshtml b/src/Web/Pages/Shared/_product.cshtml similarity index 59% rename from src/Web/Views/Catalog/_product.cshtml rename to src/Web/Pages/Shared/_product.cshtml index 2c1fcdb..d27d2e4 100644 --- a/src/Web/Views/Catalog/_product.cshtml +++ b/src/Web/Pages/Shared/_product.cshtml @@ -1,22 +1,14 @@ @model CatalogItemViewModel - -
- + -
@Model.Name
@Model.Price.ToString("N2")
- @* - - - - *@ diff --git a/src/Web/Pages/_ViewImports.cshtml b/src/Web/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..2bf31ee --- /dev/null +++ b/src/Web/Pages/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@using Microsoft.eShopWeb.Web +@using Microsoft.eShopWeb.Web.ViewModels +@using Microsoft.eShopWeb.Web.ViewModels.Account +@using Microsoft.eShopWeb.Web.ViewModels.Manage +@using Microsoft.eShopWeb.Web.Pages +@using Microsoft.AspNetCore.Identity +@using Microsoft.eShopWeb.Infrastructure.Identity +@namespace Microsoft.eShopWeb.Web.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Web/Pages/_ViewStart.cshtml b/src/Web/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/src/Web/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 9629b8f..1abbbcd 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,11 +1,11 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.eShopWeb.Infrastructure.Data; -using System; -using Microsoft.Extensions.Logging; -using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; namespace Microsoft.eShopWeb.Web { @@ -41,7 +41,6 @@ namespace Microsoft.eShopWeb.Web public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseUrls("http://0.0.0.0:5106") .UseStartup(); } } diff --git a/src/Web/Properties/launchSettings.json b/src/Web/Properties/launchSettings.json index ebf2925..5a578c5 100644 --- a/src/Web/Properties/launchSettings.json +++ b/src/Web/Properties/launchSettings.json @@ -1,10 +1,10 @@ -{ +{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:5106/", - "sslPort": 0 + "applicationUrl": "http://localhost:17469", + "sslPort": 44315 } }, "profiles": { @@ -15,13 +15,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "eShopWeb": { + "Web - PROD": { "commandName": "Project", "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5106" + "ASPNETCORE_ENVIRONMENT": "Production" + } } } -} \ No newline at end of file +} diff --git a/src/Web/Services/BasketViewModelService.cs b/src/Web/Services/BasketViewModelService.cs index 7aa9b9a..11e9612 100644 --- a/src/Web/Services/BasketViewModelService.cs +++ b/src/Web/Services/BasketViewModelService.cs @@ -1,9 +1,9 @@ -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.Interfaces; -using Microsoft.eShopWeb.Web.ViewModels; +using Microsoft.eShopWeb.Web.Pages.Basket; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -30,7 +30,7 @@ namespace Microsoft.eShopWeb.Web.Services var basketSpec = new BasketWithItemsSpecification(userName); var basket = (await _basketRepository.ListAsync(basketSpec)).FirstOrDefault(); - if(basket == null) + if (basket == null) { return await CreateBasketForUser(userName); } diff --git a/src/Web/Services/CatalogService.cs b/src/Web/Services/CatalogService.cs index e24794b..ad7fbeb 100644 --- a/src/Web/Services/CatalogService.cs +++ b/src/Web/Services/CatalogService.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Specifications; +using Microsoft.eShopWeb.Web.ViewModels; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.eShopWeb.Web.ViewModels; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.Extensions.Logging; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using System; -using Microsoft.eShopWeb.ApplicationCore.Specifications; namespace Microsoft.eShopWeb.Web.Services { @@ -42,14 +42,12 @@ namespace Microsoft.eShopWeb.Web.Services _logger.LogInformation("GetCatalogItems called."); var filterSpecification = new CatalogFilterSpecification(brandId, typeId); - var root = _itemRepository.List(filterSpecification); + var filterPaginatedSpecification = + new CatalogFilterPaginatedSpecification(itemsPage * pageIndex, itemsPage, brandId, typeId); - var totalItems = root.Count(); - - var itemsOnPage = root - .Skip(itemsPage * pageIndex) - .Take(itemsPage) - .ToList(); + // the implementation below using ForEach and Count. We need a List. + var itemsOnPage = _itemRepository.List(filterPaginatedSpecification).ToList(); + var totalItems = _itemRepository.Count(filterSpecification); itemsOnPage.ForEach(x => { diff --git a/src/Web/SlugifyParameterTransformer.cs b/src/Web/SlugifyParameterTransformer.cs new file mode 100644 index 0000000..3987c4f --- /dev/null +++ b/src/Web/SlugifyParameterTransformer.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Routing; +using System.Text.RegularExpressions; + +namespace Microsoft.eShopWeb.Web +{ + + public class SlugifyParameterTransformer : IOutboundParameterTransformer + { + public string TransformOutbound(object value) + { + if (value == null) { return null; } + + // Slugify value + return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower(); + } + } +} diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index c3bd22c..cea6f26 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -1,19 +1,30 @@ -using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.Infrastructure.Services; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.Web.HealthChecks; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.Swagger; using System; +using System.Linq; +using System.Net.Mime; using System.Text; namespace Microsoft.eShopWeb.Web @@ -65,23 +76,17 @@ namespace Microsoft.eShopWeb.Web ConfigureServices(services); } + + + // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + ConfigureCookieSettings(services); - services.ConfigureApplicationCookie(options => - { - options.Cookie.HttpOnly = true; - options.ExpireTimeSpan = TimeSpan.FromHours(1); - options.LoginPath = "/Account/Signin"; - options.LogoutPath = "/Account/Signout"; - options.Cookie = new CookieBuilder - { - IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy - }; - }); + services.AddIdentity() + .AddDefaultUI(UIFramework.Bootstrap4) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); @@ -101,40 +106,132 @@ namespace Microsoft.eShopWeb.Web // Add memory cache services services.AddMemoryCache(); - services.AddMvc() - .SetCompatibilityVersion(AspNetCore.Mvc.CompatibilityVersion.Version_2_1); + services.AddRouting(options => + { + // Replace the type and the name used to refer to it with your own + // IOutboundParameterTransformer implementation + options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); - _services = services; + services.AddMvc(options => + { + options.Conventions.Add(new RouteTokenTransformerConvention( + new SlugifyParameterTransformer())); + } + ) + .AddRazorPagesOptions(options => + { + options.Conventions.AuthorizePage("/Basket/Checkout"); + }) + .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + + services.AddHttpContextAccessor(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" }); + }); + + services.AddHealthChecks() + .AddCheck("home_page_health_check") + .AddCheck("api_health_check"); + + _services = services; // used to debug registered services + } + + private static void ConfigureCookieSettings(IServiceCollection services) + { + services.Configure(options => + { + // This lambda determines whether user consent for non-essential cookies is needed for a given request. + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); + services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromHours(1); + options.LoginPath = "/Account/Signin"; + options.LogoutPath = "/Account/Signout"; + options.Cookie = new CookieBuilder + { + IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy + }; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, - IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IHostingEnvironment env, LinkGenerator linkGenerator) { + //app.UseDeveloperExceptionPage(); + app.UseHealthChecks("/health", + new HealthCheckOptions + { + ResponseWriter = async (context, report) => + { + var result = JsonConvert.SerializeObject( + new + { + status = report.Status.ToString(), + errors = report.Entries.Select(e => new + { + key = e.Key, + value = Enum.GetName(typeof(HealthStatus), e.Value.Status) + }) + }); + context.Response.ContentType = MediaTypeNames.Application.Json; + await context.Response.WriteAsync(result); + } + }); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - ListAllRegisteredServices(app); + ListAllRegisteredServices(app, linkGenerator); app.UseDatabaseErrorPage(); } else { - app.UseExceptionHandler("/Catalog/Error"); + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); + app.UseCookiePolicy(); + app.UseAuthentication(); - app.UseMvc(); + // Enable middleware to serve generated Swagger as a JSON endpoint. + app.UseSwagger(); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), + // specifying the Swagger JSON endpoint. + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}"); + }); } - private void ListAllRegisteredServices(IApplicationBuilder app) + private void ListAllRegisteredServices(IApplicationBuilder app, LinkGenerator linkGenerator) { + var homePageLink = linkGenerator.GetPathByAction("Index", "Catalog"); + var loginLink = linkGenerator.GetPathByAction("SignIn", "Account"); app.Map("/allservices", builder => builder.Run(async context => { var sb = new StringBuilder(); + sb.Append("Return to site | "); + sb.Append("Login to site"); sb.Append("

All Services

"); sb.Append(""); sb.Append(""); @@ -151,5 +248,6 @@ namespace Microsoft.eShopWeb.Web await context.Response.WriteAsync(sb.ToString()); })); } + } } diff --git a/src/Web/ViewModels/Account/RegisterViewModel.cs b/src/Web/ViewModels/Account/RegisterViewModel.cs index 0d89911..b1bd4f9 100644 --- a/src/Web/ViewModels/Account/RegisterViewModel.cs +++ b/src/Web/ViewModels/Account/RegisterViewModel.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Account { diff --git a/src/Web/ViewModels/Manage/TwoFactorAuthenticationViewModel.cs b/src/Web/ViewModels/Manage/TwoFactorAuthenticationViewModel.cs index c281a39..00a5795 100644 --- a/src/Web/ViewModels/Manage/TwoFactorAuthenticationViewModel.cs +++ b/src/Web/ViewModels/Manage/TwoFactorAuthenticationViewModel.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.eShopWeb.Web.ViewModels.Manage +namespace Microsoft.eShopWeb.Web.ViewModels.Manage { public class TwoFactorAuthenticationViewModel { diff --git a/src/Web/ViewModels/OrderItemViewModel.cs b/src/Web/ViewModels/OrderItemViewModel.cs index 13022d8..ae15fe2 100644 --- a/src/Web/ViewModels/OrderItemViewModel.cs +++ b/src/Web/ViewModels/OrderItemViewModel.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.eShopWeb.Web.ViewModels +namespace Microsoft.eShopWeb.Web.ViewModels { public class OrderItemViewModel { diff --git a/src/Web/Views/Account/LoginWith2fa.cshtml b/src/Web/Views/Account/LoginWith2fa.cshtml index 2e52d37..39f4443 100644 --- a/src/Web/Views/Account/LoginWith2fa.cshtml +++ b/src/Web/Views/Account/LoginWith2fa.cshtml @@ -1,4 +1,3 @@ -@using Microsoft.eShopWeb.Web.ViewModels.Account @model LoginWith2faViewModel @{ ViewData["Title"] = "Two-factor authentication"; diff --git a/src/Web/Views/Account/Register.cshtml b/src/Web/Views/Account/Register.cshtml index 188ca5f..576111e 100644 --- a/src/Web/Views/Account/Register.cshtml +++ b/src/Web/Views/Account/Register.cshtml @@ -1,7 +1,3 @@ -@using System.Collections.Generic -@using Microsoft.AspNetCore.Http -@using Microsoft.AspNetCore.Http.Authentication -@using Microsoft.eShopWeb.Web.ViewModels.Account @model RegisterViewModel @{ ViewData["Title"] = "Register"; diff --git a/src/Web/Views/Account/Signin.cshtml b/src/Web/Views/Account/Signin.cshtml index 7c88be0..9d1742d 100644 --- a/src/Web/Views/Account/Signin.cshtml +++ b/src/Web/Views/Account/Signin.cshtml @@ -1,4 +1,3 @@ -@using Microsoft.eShopWeb.Web.ViewModels.Account @model LoginViewModel @{ ViewData["Title"] = "Log in"; diff --git a/src/Web/Views/Basket/Checkout.cshtml b/src/Web/Views/Basket/Checkout.cshtml deleted file mode 100644 index 3cc3d86..0000000 --- a/src/Web/Views/Basket/Checkout.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@using Microsoft.eShopWeb.Web.ViewModels -@{ - ViewData["Title"] = "Checkout Complete"; - @model BasketViewModel -} -
-
- -
-
- -
-

Thanks for your Order!

- - Continue Shopping... -
diff --git a/src/Web/Views/Manage/Index.cshtml b/src/Web/Views/Manage/MyAccount.cshtml similarity index 77% rename from src/Web/Views/Manage/Index.cshtml rename to src/Web/Views/Manage/MyAccount.cshtml index fff740a..c8ebf1f 100644 --- a/src/Web/Views/Manage/Index.cshtml +++ b/src/Web/Views/Manage/MyAccount.cshtml @@ -18,15 +18,15 @@ @if (Model.IsEmailConfirmed) { -
- - -
+
+ + +
} else { - - + + } diff --git a/src/Web/Views/Order/Detail.cshtml b/src/Web/Views/Order/Detail.cshtml index 1eacdef..3977ecd 100644 --- a/src/Web/Views/Order/Detail.cshtml +++ b/src/Web/Views/Order/Detail.cshtml @@ -1,5 +1,4 @@ -@using Microsoft.eShopWeb.Web.ViewModels -@model OrderViewModel +@model OrderViewModel @{ ViewData["Title"] = "My Order History"; } @@ -26,14 +25,14 @@ @*
-
-
Description
-
+
+
Description
+
-
-
@Model.Description
-
-
*@ +
+
@Model.Description
+
+ *@
@@ -81,7 +80,7 @@
-
$ @Model.Total
+
$ @Model.Total.ToString("N2")
diff --git a/src/Web/Views/Order/Index.cshtml b/src/Web/Views/Order/MyOrders.cshtml similarity index 95% rename from src/Web/Views/Order/Index.cshtml rename to src/Web/Views/Order/MyOrders.cshtml index b35e00a..7478ba6 100644 --- a/src/Web/Views/Order/Index.cshtml +++ b/src/Web/Views/Order/MyOrders.cshtml @@ -1,5 +1,4 @@ -@using Microsoft.eShopWeb.Web.ViewModels -@model IEnumerable +@model IEnumerable @{ ViewData["Title"] = "My Order History"; } diff --git a/src/Web/Views/Shared/_CookieConsentPartial.cshtml b/src/Web/Views/Shared/_CookieConsentPartial.cshtml new file mode 100644 index 0000000..a535ea4 --- /dev/null +++ b/src/Web/Views/Shared/_CookieConsentPartial.cshtml @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Http.Features + +@{ + var consentFeature = Context.Features.Get(); + var showBanner = !consentFeature?.CanTrack ?? false; + var cookieString = consentFeature?.CreateConsentCookie(); +} + +@if (showBanner) +{ + + +} diff --git a/src/Web/Views/Shared/_Layout.cshtml b/src/Web/Views/Shared/_Layout.cshtml index 1b71248..c6408ee 100644 --- a/src/Web/Views/Shared/_Layout.cshtml +++ b/src/Web/Views/Shared/_Layout.cshtml @@ -1,77 +1,66 @@  - - - @ViewData["Title"] - Microsoft.eShopOnWeb - - - - - - - - - + + + @ViewData["Title"] - Microsoft.eShopOnWeb + + + + + + + + - + - + @RenderBody() +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + @RenderSection("scripts", required: false) diff --git a/src/Web/Views/Shared/_LoginPartial.cshtml b/src/Web/Views/Shared/_LoginPartial.cshtml index 6f03b12..5385d36 100644 --- a/src/Web/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/Views/Shared/_LoginPartial.cshtml @@ -1,66 +1,55 @@ -@using Microsoft.AspNetCore.Identity - - @if (Context.User.Identity.IsAuthenticated) +@if (Context.User.Identity.IsAuthenticated) { -
-
- -
+
+
+ +
+ @*
@User.FindFirst(x => x.Type == "preferred_username").Value
*@ + +
+
+ +
My orders
+ +
+ +
My account
+ +
+ +
Log Out
+ +
+
+ +
+
- @*
@User.FindFirst(x => x.Type == "preferred_username").Value
*@ - -
- -
- - - -
My orders
- -
- - - -
My account
- -
- - - -
Log Out
- -
-
- -
-
- -
- @await Component.InvokeAsync("Basket", User.Identity.Name) -
+
+ @await Component.InvokeAsync("Basket", User.Identity.Name) +
} else { -
-
-
-
+
+
+
+ +
+
+
- - Login - -
-
-
-
- -
- @await Component.InvokeAsync("Basket") -
+
+ @await Component.InvokeAsync("Basket") +
} diff --git a/src/Web/Views/_ViewImports.cshtml b/src/Web/Views/_ViewImports.cshtml index fa5c648..2bf31ee 100644 --- a/src/Web/Views/_ViewImports.cshtml +++ b/src/Web/Views/_ViewImports.cshtml @@ -2,6 +2,8 @@ @using Microsoft.eShopWeb.Web.ViewModels @using Microsoft.eShopWeb.Web.ViewModels.Account @using Microsoft.eShopWeb.Web.ViewModels.Manage +@using Microsoft.eShopWeb.Web.Pages @using Microsoft.AspNetCore.Identity @using Microsoft.eShopWeb.Infrastructure.Identity -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file +@namespace Microsoft.eShopWeb.Web.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 58ac383..9e2848c 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -1,18 +1,19 @@  - netcoreapp2.1 + netcoreapp2.2 Microsoft.eShopWeb.Web + aspnet-Web2-1FA3F72E-E7E3-4360-9E49-1CCCD7FE85F7 + InProcess + - - - - - + + + + - @@ -37,6 +38,98 @@ Always + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + <_ContentIncludedByDefault Remove="Views\Account\Lockout.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Account\LoginWith2fa.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Account\Register.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Account\Signin.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\ChangePassword.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\Disable2fa.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\EnableAuthenticator.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\ExternalLogins.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\GenerateRecoveryCodes.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\ResetAuthenticator.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\SetPassword.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\TwoFactorAuthentication.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\_Layout.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\_ManageNav.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\_StatusMessage.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Manage\_ViewImports.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Order\Detail.cshtml" /> + <_ContentIncludedByDefault Remove="Views\Shared\Components\Basket\Default.cshtml" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index fa8ce71..e203e94 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -1,6 +1,5 @@ -{ +{ "Logging": { - "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index 20d2185..dd8d5ff 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -10,6 +10,7 @@ "Default": "Warning", "Microsoft": "Warning", "System": "Warning" - } + }, + "AllowedHosts": "*" } } diff --git a/src/Web/bower.json b/src/Web/bower.json deleted file mode 100644 index b07e3cc..0000000 --- a/src/Web/bower.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "asp.net", - "private": true, - "dependencies": { - "bootstrap": "3.3.7", - "jquery": "2.2.0", - "jquery-validation": "1.14.0", - "jquery-validation-unobtrusive": "3.2.6" - } -} diff --git a/src/Web/bundleconfig.json b/src/Web/bundleconfig.json deleted file mode 100644 index 0914b21..0000000 --- a/src/Web/bundleconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -// Configure bundling and minification for the project. -// More info at https://go.microsoft.com/fwlink/?LinkId=808241 -[ - { - "outputFileName": "wwwroot/css/app.min.css", - // An array of relative input file paths. Globbing patterns supported - "inputFiles": [ - "wwwroot/css/app.css" - ] - }, - { - "outputFileName": "wwwroot/js/site.min.js", - "inputFiles": [ - "wwwroot/js/site.js" - ], - // Optionally specify minification options - "minify": { - "enabled": true, - "renameLocals": true - }, - // Optionally generate .map file - "sourceMap": false - } -] diff --git a/src/Web/compilerconfig.json b/src/Web/compilerconfig.json deleted file mode 100644 index 73e8abe..0000000 --- a/src/Web/compilerconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "outputFile": "wwwroot/css/orders/orders.component.css", - "inputFile": "wwwroot/css/orders/orders.component.scss" - }, - //{ - // "outputFile": "wwwroot/css/orders/orders-new/orders-new.component.css", - // "inputFile": "wwwroot/css/orders/orders-new/orders-new.component.scss" - //}, - //{ - // "outputFile": "wwwroot/css/orders/orders-detail/orders-detail.component.css", - // "inputFile": "wwwroot/css/orders/orders-detail/orders-detail.component.scss" - //}, - { - "outputFile": "wwwroot/css/catalog/catalog.component.css", - "inputFile": "wwwroot/css/catalog/catalog.component.scss" - }, - { - "outputFile": "wwwroot/css/basket/basket.component.css", - "inputFile": "wwwroot/css/basket/basket.component.scss" - }, - { - "outputFile": "wwwroot/css/basket/basket-status/basket-status.component.css", - "inputFile": "wwwroot/css/basket/basket-status/basket-status.component.scss" - }, - //{ - // "outputFile": "wwwroot/css/shared/components/header/header.css", - // "inputFile": "wwwroot/css/shared/components/header/header.scss" - //}, - //{ - // "outputFile": "wwwroot/css/shared/components/identity/identity.css", - // "inputFile": "wwwroot/css/shared/components/identity/identity.scss" - //}, - //{ - // "outputFile": "wwwroot/css/shared/components/pager/pager.css", - // "inputFile": "wwwroot/css/shared/components/pager/pager.scss" - //}, - { - "outputFile": "wwwroot/css/app.component.css", - "inputFile": "wwwroot/css/app.component.scss" - } -] \ No newline at end of file diff --git a/src/Web/wwwroot/lib/jquery-validation/.bower.json b/src/Web/wwwroot/lib/jquery-validation/.bower.json deleted file mode 100644 index cab34a4..0000000 --- a/src/Web/wwwroot/lib/jquery-validation/.bower.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "jquery-validation", - "homepage": "http://jqueryvalidation.org/", - "repository": { - "type": "git", - "url": "git://github.com/jzaefferer/jquery-validation.git" - }, - "authors": [ - "Jörn Zaefferer " - ], - "description": "Form validation made easy", - "main": "dist/jquery.validate.js", - "keywords": [ - "forms", - "validation", - "validate" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "demo", - "lib" - ], - "dependencies": { - "jquery": ">= 1.7.2" - }, - "version": "1.14.0", - "_release": "1.14.0", - "_resolution": { - "type": "version", - "tag": "1.14.0", - "commit": "c1343fb9823392aa9acbe1c3ffd337b8c92fed48" - }, - "_source": "git://github.com/jzaefferer/jquery-validation.git", - "_target": ">=1.8", - "_originalSource": "jquery-validation" -} \ No newline at end of file diff --git a/src/WebRazorPages/.bowerrc b/src/WebRazorPages/.bowerrc deleted file mode 100644 index 6406626..0000000 --- a/src/WebRazorPages/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "wwwroot/lib" -} diff --git a/src/WebRazorPages/Areas/Identity/Pages/_ViewStart.cshtml b/src/WebRazorPages/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..7bd9b6b --- /dev/null +++ b/src/WebRazorPages/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Pages/Shared/_Layout.cshtml"; +} diff --git a/src/WebRazorPages/Data/ApplicationDbContext.cs b/src/WebRazorPages/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..2390eb2 --- /dev/null +++ b/src/WebRazorPages/Data/ApplicationDbContext.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace WebRazorPages.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/src/WebRazorPages/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/WebRazorPages/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..2e20bbd --- /dev/null +++ b/src/WebRazorPages/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,236 @@ +// +using System; +using WebRazorPages.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace WebRazorPages.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.0-preview1") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/WebRazorPages/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/WebRazorPages/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..ab4ad76 --- /dev/null +++ b/src/WebRazorPages/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,220 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WebRazorPages.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(maxLength: 128, nullable: false), + ProviderKey = table.Column(maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(maxLength: 128, nullable: false), + Name = table.Column(maxLength: 128, nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/WebRazorPages/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/WebRazorPages/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..1c2ffc9 --- /dev/null +++ b/src/WebRazorPages/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,234 @@ +// +using System; +using WebRazorPages.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace WebRazorPages.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.0-preview1") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/WebRazorPages/Dockerfile b/src/WebRazorPages/Dockerfile index ca49eb1..97e6078 100644 --- a/src/WebRazorPages/Dockerfile +++ b/src/WebRazorPages/Dockerfile @@ -7,7 +7,7 @@ # # RUN COMMAND # docker run --name eshopweb --rm -it -p 5107:5107 webrazor -FROM microsoft/dotnet:2.1-sdk AS build +FROM microsoft/dotnet:2.2-sdk AS build WORKDIR /app COPY *.sln . @@ -17,7 +17,7 @@ RUN dotnet restore RUN dotnet publish -c Release -o out -FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime +FROM microsoft/dotnet:2.2-aspnetcore-runtime AS runtime WORKDIR /app COPY --from=build /app/src/WebRazorPages/out ./ diff --git a/src/WebRazorPages/Interfaces/IBasketViewModelService.cs b/src/WebRazorPages/Interfaces/IBasketViewModelService.cs index 975478a..c1dbbe1 100644 --- a/src/WebRazorPages/Interfaces/IBasketViewModelService.cs +++ b/src/WebRazorPages/Interfaces/IBasketViewModelService.cs @@ -1,5 +1,4 @@ using Microsoft.eShopWeb.RazorPages.ViewModels; -using System.Collections.Generic; using System.Threading.Tasks; namespace Microsoft.eShopWeb.RazorPages.Interfaces diff --git a/src/WebRazorPages/Pages/Account/ConfirmEmail.cshtml b/src/WebRazorPages/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..5236616 --- /dev/null +++ b/src/WebRazorPages/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,13 @@ + +@page +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

+
+

+ Thank you for confirming your email. +

+
\ No newline at end of file diff --git a/src/WebRazorPages/Pages/Account/ConfirmEmail.cshtml.cs b/src/WebRazorPages/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 0000000..9164e35 --- /dev/null +++ b/src/WebRazorPages/Pages/Account/ConfirmEmail.cshtml.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.Infrastructure.Identity; + +namespace Microsoft.eShopWeb.RazorPages.Pages.Account +{ + public class ConfirmEmailModel : PageModel + { + private readonly UserManager _userManager; + + public ConfirmEmailModel(UserManager userManager) + { + _userManager = userManager; + } + + public async Task OnGetAsync(string userId, string code) + { + if (userId == null || code == null) + { + return RedirectToPage("/Index"); + } + + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{userId}'."); + } + + var result = await _userManager.ConfirmEmailAsync(user, code); + if (!result.Succeeded) + { + throw new ApplicationException($"Error confirming email for user with ID '{userId}':"); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/src/WebRazorPages/Pages/Account/Signin.cshtml b/src/WebRazorPages/Pages/Account/Signin.cshtml index e5f4407..e594ae5 100644 --- a/src/WebRazorPages/Pages/Account/Signin.cshtml +++ b/src/WebRazorPages/Pages/Account/Signin.cshtml @@ -1,7 +1,4 @@ @page -@using System.Collections.Generic -@using Microsoft.AspNetCore.Http -@using Microsoft.AspNetCore.Http.Authentication @model SigninModel @{ ViewData["Title"] = "Log in"; diff --git a/src/WebRazorPages/Pages/Basket/Checkout.cshtml b/src/WebRazorPages/Pages/Basket/Checkout.cshtml index 9b23d55..01e0a78 100644 --- a/src/WebRazorPages/Pages/Basket/Checkout.cshtml +++ b/src/WebRazorPages/Pages/Basket/Checkout.cshtml @@ -5,7 +5,7 @@ }
- +
diff --git a/src/WebRazorPages/Pages/Basket/Index.cshtml b/src/WebRazorPages/Pages/Basket/Index.cshtml index 0dc9670..c1dc5f4 100644 --- a/src/WebRazorPages/Pages/Basket/Index.cshtml +++ b/src/WebRazorPages/Pages/Basket/Index.cshtml @@ -5,7 +5,7 @@ }
- +
@@ -23,7 +23,7 @@
Cost
- @for (int i=0; i< Model.BasketModel.Items.Count; i++) + @for (int i = 0; i < Model.BasketModel.Items.Count; i++) { var item = Model.BasketModel.Items[i];
@@ -44,31 +44,35 @@
@*
- @item.ProductId -
*@ + @item.ProductId + *@ -
-
-
-
Total
-
- -
-
-
$ @Model.BasketModel.Total()
-
- -
-
-
- @**@ -
-
-
} + +
+
+
+
Total
+
+ +
+
+
$ @Model.BasketModel.Total().ToString("N2")
+
+ +
+
+
+ @**@ +
+
+
+
+ asp-page-handler="Update"> + [ Update ] + diff --git a/src/WebRazorPages/Pages/Basket/Index.cshtml.cs b/src/WebRazorPages/Pages/Basket/Index.cshtml.cs index d7ccf30..fd7e42a 100644 --- a/src/WebRazorPages/Pages/Basket/Index.cshtml.cs +++ b/src/WebRazorPages/Pages/Basket/Index.cshtml.cs @@ -1,14 +1,14 @@ -using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.eShopWeb.RazorPages.ViewModels; -using Microsoft.eShopWeb.RazorPages.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.AspNetCore.Identity; using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.eShopWeb.RazorPages.Interfaces; +using Microsoft.eShopWeb.RazorPages.ViewModels; using System; -using Microsoft.AspNetCore.Http; using System.Collections.Generic; +using System.Threading.Tasks; namespace Microsoft.eShopWeb.RazorPages.Pages.Basket { @@ -54,7 +54,7 @@ namespace Microsoft.eShopWeb.RazorPages.Pages.Basket return RedirectToPage(); } - public async Task OnPostUpdate(Dictionary items) + public async Task OnPostUpdate(Dictionary items) { await SetBasketModelAsync(); await _basketService.SetQuantities(BasketModel.Id, items); @@ -84,7 +84,7 @@ namespace Microsoft.eShopWeb.RazorPages.Pages.Basket if (_username != null) return; _username = Guid.NewGuid().ToString(); - var cookieOptions = new CookieOptions(); + var cookieOptions = new CookieOptions { IsEssential = true }; cookieOptions.Expires = DateTime.Today.AddYears(10); Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions); } diff --git a/src/WebRazorPages/Pages/Error.cshtml b/src/WebRazorPages/Pages/Error.cshtml index b1f3143..6f92b95 100644 --- a/src/WebRazorPages/Pages/Error.cshtml +++ b/src/WebRazorPages/Pages/Error.cshtml @@ -16,8 +16,11 @@

Development Mode

- Swapping to Development environment will display more detailed information about the error that occurred. + Swapping to the Development environment displays detailed information about the error that occurred.

- Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. + The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app.

diff --git a/src/WebRazorPages/Pages/Error.cshtml.cs b/src/WebRazorPages/Pages/Error.cshtml.cs index 6fc7fb5..284ffb2 100644 --- a/src/WebRazorPages/Pages/Error.cshtml.cs +++ b/src/WebRazorPages/Pages/Error.cshtml.cs @@ -1,8 +1,10 @@ -using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Diagnostics; namespace Microsoft.eShopWeb.RazorPages.Pages { + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class ErrorModel : PageModel { public string RequestId { get; set; } diff --git a/src/WebRazorPages/Pages/Index.cshtml b/src/WebRazorPages/Pages/Index.cshtml index 3d4b745..3fe3020 100644 --- a/src/WebRazorPages/Pages/Index.cshtml +++ b/src/WebRazorPages/Pages/Index.cshtml @@ -5,7 +5,7 @@ }
- +
diff --git a/src/WebRazorPages/Pages/Index.cshtml.cs b/src/WebRazorPages/Pages/Index.cshtml.cs index 74fce01..68af9ab 100644 --- a/src/WebRazorPages/Pages/Index.cshtml.cs +++ b/src/WebRazorPages/Pages/Index.cshtml.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.RazorPages.ViewModels; using Microsoft.eShopWeb.RazorPages.Interfaces; diff --git a/src/WebRazorPages/Pages/Order/Index.cshtml b/src/WebRazorPages/Pages/Order/Index.cshtml index 312bcfb..130e9c6 100644 --- a/src/WebRazorPages/Pages/Order/Index.cshtml +++ b/src/WebRazorPages/Pages/Order/Index.cshtml @@ -1,5 +1,4 @@ @page -@using System.Linq; @model IndexModel @{ ViewData["Title"] = "My Order History"; diff --git a/src/WebRazorPages/Pages/Privacy.cshtml b/src/WebRazorPages/Pages/Privacy.cshtml new file mode 100644 index 0000000..46ba966 --- /dev/null +++ b/src/WebRazorPages/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/src/WebRazorPages/Pages/Privacy.cshtml.cs b/src/WebRazorPages/Pages/Privacy.cshtml.cs new file mode 100644 index 0000000..3201f6a --- /dev/null +++ b/src/WebRazorPages/Pages/Privacy.cshtml.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Microsoft.eShopWeb.RazorPages.Pages +{ + public class PrivacyModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/src/WebRazorPages/Pages/Components/Basket/Default.cshtml b/src/WebRazorPages/Pages/Shared/Components/Basket/Default.cshtml similarity index 100% rename from src/WebRazorPages/Pages/Components/Basket/Default.cshtml rename to src/WebRazorPages/Pages/Shared/Components/Basket/Default.cshtml diff --git a/src/WebRazorPages/Pages/Shared/_CookieConsentPartial.cshtml b/src/WebRazorPages/Pages/Shared/_CookieConsentPartial.cshtml new file mode 100644 index 0000000..7df65c4 --- /dev/null +++ b/src/WebRazorPages/Pages/Shared/_CookieConsentPartial.cshtml @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Http.Features + +@{ + var consentFeature = Context.Features.Get(); + var showBanner = !consentFeature?.CanTrack ?? false; + var cookieString = consentFeature?.CreateConsentCookie(); +} + +@if (showBanner) +{ + + +} diff --git a/src/WebRazorPages/Pages/Shared/_Layout.cshtml b/src/WebRazorPages/Pages/Shared/_Layout.cshtml index 970be6f..b0d253a 100644 --- a/src/WebRazorPages/Pages/Shared/_Layout.cshtml +++ b/src/WebRazorPages/Pages/Shared/_Layout.cshtml @@ -3,16 +3,17 @@ - @ViewData["Title"] - Microsoft.eShopOnWeb - - + @ViewData["Title"] - WebRazorPages + - - + + asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" + crossorigin="anonymous" + integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" /> @@ -26,51 +27,53 @@ - + @RenderBody() -
-
-
-
- - + - - + @**@ + - - - - - - @RenderSection("scripts", required: false) + + @RenderSection("Scripts", required: false) diff --git a/src/WebRazorPages/Pages/Shared/_LoginPartial.cshtml b/src/WebRazorPages/Pages/Shared/_LoginPartial.cshtml index 708fb14..35e7f60 100644 --- a/src/WebRazorPages/Pages/Shared/_LoginPartial.cshtml +++ b/src/WebRazorPages/Pages/Shared/_LoginPartial.cshtml @@ -1,63 +1,56 @@ -@using Microsoft.AspNetCore.Identity - - @if (Context.User.Identity.IsAuthenticated) +@inject SignInManager SignInManager +@inject UserManager UserManager +@if (Context.User.Identity.IsAuthenticated) { -
-
- -
- @*
@User.FindFirst(x => x.Type == "preferred_username").Value
*@ - -
+
+
+ +
+ @*
@User.FindFirst(x => x.Type == "preferred_username").Value
*@ + +
+
+ +
My orders
+ +
+ +
My account
+ +
+ +
Log Out
+ +
+
+ +
+
-
- - -
My orders
- -
- - - -
My account
- -
- - - -
Log Out
- -
-
- -
-
- -
- @await Component.InvokeAsync("Basket") -
+
+ @await Component.InvokeAsync("Basket") +
} else { -
-
-
-
+
+
+
+ +
+
+
- - Login - -
-
-
-
- -
- @await Component.InvokeAsync("Basket") -
+
+ @await Component.InvokeAsync("Basket") +
} diff --git a/src/WebRazorPages/Pages/Shared/_ValidationScriptsPartial.cshtml b/src/WebRazorPages/Pages/Shared/_ValidationScriptsPartial.cshtml index a2b13b3..c442042 100644 --- a/src/WebRazorPages/Pages/Shared/_ValidationScriptsPartial.cshtml +++ b/src/WebRazorPages/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -3,16 +3,16 @@
- - diff --git a/src/WebRazorPages/Pages/_ViewImports.cshtml b/src/WebRazorPages/Pages/_ViewImports.cshtml index 1d6b867..3b07c91 100644 --- a/src/WebRazorPages/Pages/_ViewImports.cshtml +++ b/src/WebRazorPages/Pages/_ViewImports.cshtml @@ -1,6 +1,6 @@ -@using Microsoft.eShopWeb.RazorPages +@using Microsoft.eShopWeb.RazorPages @using Microsoft.eShopWeb.RazorPages.ViewModels @using Microsoft.AspNetCore.Identity @using Microsoft.eShopWeb.Infrastructure.Identity @namespace Microsoft.eShopWeb.RazorPages.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/WebRazorPages/Program.cs b/src/WebRazorPages/Program.cs index e5030f5..4f38455 100644 --- a/src/WebRazorPages/Program.cs +++ b/src/WebRazorPages/Program.cs @@ -1,11 +1,11 @@ -using System; -using Microsoft.AspNetCore; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Identity; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using System; namespace Microsoft.eShopWeb.RazorPages { @@ -41,7 +41,6 @@ namespace Microsoft.eShopWeb.RazorPages public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseUrls("http://0.0.0.0:5107") .UseStartup(); } } diff --git a/src/WebRazorPages/Properties/launchSettings.json b/src/WebRazorPages/Properties/launchSettings.json index fb21aef..fb9ac93 100644 --- a/src/WebRazorPages/Properties/launchSettings.json +++ b/src/WebRazorPages/Properties/launchSettings.json @@ -1,10 +1,10 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:5107/", - "sslPort": 44305 + "applicationUrl": "http://localhost:17930", + "sslPort": 44394 } }, "profiles": { @@ -18,15 +18,10 @@ "WebRazorPages": { "commandName": "Project", "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5107/" - }, - "Docker": { - "commandName": "Docker", - "launchBrowser": true, - "launchUrl": "{Scheme}://localhost:{ServicePort}" + } } } } \ No newline at end of file diff --git a/src/WebRazorPages/Services/CatalogService.cs b/src/WebRazorPages/Services/CatalogService.cs index d41a218..613d5c3 100644 --- a/src/WebRazorPages/Services/CatalogService.cs +++ b/src/WebRazorPages/Services/CatalogService.cs @@ -1,7 +1,7 @@ -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.RazorPages.Interfaces; using Microsoft.eShopWeb.RazorPages.ViewModels; using Microsoft.Extensions.Logging; @@ -43,14 +43,12 @@ namespace Microsoft.eShopWeb.RazorPages.Services _logger.LogInformation("GetCatalogItems called."); var filterSpecification = new CatalogFilterSpecification(brandId, typeId); - var root = _itemRepository.List(filterSpecification); + var filterPaginatedSpecification = + new CatalogFilterPaginatedSpecification(itemsPage * pageIndex, itemsPage, brandId, typeId); - var totalItems = root.Count(); - - var itemsOnPage = root - .Skip(itemsPage * pageIndex) - .Take(itemsPage) - .ToList(); + // the implementation below using ForEach and Count. We need a List. + var itemsOnPage = _itemRepository.List(filterPaginatedSpecification).ToList(); + var totalItems = _itemRepository.Count(filterSpecification); itemsOnPage.ForEach(x => { diff --git a/src/WebRazorPages/Startup.cs b/src/WebRazorPages/Startup.cs index fc3ac8b..6f0c160 100644 --- a/src/WebRazorPages/Startup.cs +++ b/src/WebRazorPages/Startup.cs @@ -1,14 +1,16 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.Infrastructure.Services; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.RazorPages.Interfaces; using Microsoft.eShopWeb.RazorPages.Services; using Microsoft.Extensions.Configuration; @@ -74,20 +76,17 @@ namespace Microsoft.eShopWeb.RazorPages ConfigureServices(services); } + + // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + ConfigureCookieOptions(services); + services.AddIdentity() + .AddDefaultUI(UIFramework.Bootstrap4) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - services.ConfigureApplicationCookie(options => - { - options.Cookie.HttpOnly = true; - options.ExpireTimeSpan = TimeSpan.FromHours(1); - options.LoginPath = "/Account/Signin"; - options.LogoutPath = "/Account/Signout"; - }); - services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); @@ -106,8 +105,9 @@ namespace Microsoft.eShopWeb.RazorPages // Add memory cache services services.AddMemoryCache(); + services.AddMvc() - .SetCompatibilityVersion(AspNetCore.Mvc.CompatibilityVersion.Version_2_1) + .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddRazorPagesOptions(options => { options.Conventions.AuthorizeFolder("/Order"); @@ -117,9 +117,25 @@ namespace Microsoft.eShopWeb.RazorPages _services = services; } + private static void ConfigureCookieOptions(IServiceCollection services) + { + services.Configure(options => + { + // This lambda determines whether user consent for non-essential cookies is needed for a given request. + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); + services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromHours(1); + options.LoginPath = "/Account/Signin"; + options.LogoutPath = "/Account/Signout"; + }); + } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, - IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { @@ -129,12 +145,15 @@ namespace Microsoft.eShopWeb.RazorPages } else { - app.UseExceptionHandler("/Catalog/Error"); + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); + app.UseCookiePolicy(); + app.UseAuthentication(); app.UseMvc(); @@ -161,5 +180,6 @@ namespace Microsoft.eShopWeb.RazorPages await context.Response.WriteAsync(sb.ToString()); })); } + } } diff --git a/src/WebRazorPages/ViewComponents/Basket.cs b/src/WebRazorPages/ViewComponents/Basket.cs index a9b8ee7..11fd674 100644 --- a/src/WebRazorPages/ViewComponents/Basket.cs +++ b/src/WebRazorPages/ViewComponents/Basket.cs @@ -1,6 +1,5 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; diff --git a/src/WebRazorPages/WebRazorPages.csproj b/src/WebRazorPages/WebRazorPages.csproj index 8219d7b..7594006 100644 --- a/src/WebRazorPages/WebRazorPages.csproj +++ b/src/WebRazorPages/WebRazorPages.csproj @@ -1,16 +1,31 @@  + - netcoreapp2.1 + netcoreapp2.2 Microsoft.eShopWeb.RazorPages Microsoft.eShopWeb.RazorPages Linux - 231ddc1b-6787-4704-a0c0-18df6a022660 + aspnet-WebRazorPages-6B1EFE4D-B682-4543-93F5-69EE95000B1D + InProcess + - + + + + + + + + + + PreserveNewest + + + diff --git a/src/WebRazorPages/appsettings.Development.json b/src/WebRazorPages/appsettings.Development.json index fa8ce71..e203e94 100644 --- a/src/WebRazorPages/appsettings.Development.json +++ b/src/WebRazorPages/appsettings.Development.json @@ -1,6 +1,5 @@ -{ +{ "Logging": { - "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", diff --git a/src/WebRazorPages/appsettings.json b/src/WebRazorPages/appsettings.json index 5fff67b..d4d3578 100644 --- a/src/WebRazorPages/appsettings.json +++ b/src/WebRazorPages/appsettings.json @@ -1,8 +1,11 @@ -{ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-WebRazorPages-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" + }, "Logging": { - "IncludeScopes": false, "LogLevel": { "Default": "Warning" } - } + }, + "AllowedHosts": "*" } diff --git a/src/WebRazorPages/bower.json b/src/WebRazorPages/bower.json deleted file mode 100644 index b07e3cc..0000000 --- a/src/WebRazorPages/bower.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "asp.net", - "private": true, - "dependencies": { - "bootstrap": "3.3.7", - "jquery": "2.2.0", - "jquery-validation": "1.14.0", - "jquery-validation-unobtrusive": "3.2.6" - } -} diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index 3073c95..21b873e 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -1,21 +1,26 @@  - netcoreapp2.1 + netcoreapp2.2 Microsoft.eShopWeb.FunctionalTests + false - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers + + - @@ -23,4 +28,8 @@ + + + + diff --git a/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs b/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs index d9a5f69..e3e675b 100644 --- a/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs +++ b/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.eShopWeb.Web; +using Microsoft.eShopWeb.Web; using Microsoft.eShopWeb.Web.ViewModels; using Newtonsoft.Json; using System.Linq; diff --git a/tests/FunctionalTests/Web/Controllers/CatalogControllerIndex.cs b/tests/FunctionalTests/Web/Controllers/CatalogControllerIndex.cs index 3f75870..8fec0cc 100644 --- a/tests/FunctionalTests/Web/Controllers/CatalogControllerIndex.cs +++ b/tests/FunctionalTests/Web/Controllers/CatalogControllerIndex.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.eShopWeb.Web; +using Microsoft.eShopWeb.Web; using System.Net.Http; using System.Threading.Tasks; using Xunit; diff --git a/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs b/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs index 2b20b58..5d0da19 100644 --- a/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs +++ b/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs @@ -22,11 +22,11 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers [Fact] public async Task ReturnsRedirectGivenAnonymousUser() { - var response = await Client.GetAsync("/Order/Index"); + var response = await Client.GetAsync("/order/my-orders"); var redirectLocation = response.Headers.Location.OriginalString; Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); - Assert.Contains("Account/Signin", redirectLocation); + Assert.Contains("Account/Login", redirectLocation); } } } diff --git a/tests/FunctionalTests/Web/Controllers/CustomWebApplicationFactory.cs b/tests/FunctionalTests/Web/CustomWebApplicationFactory.cs similarity index 96% rename from tests/FunctionalTests/Web/Controllers/CustomWebApplicationFactory.cs rename to tests/FunctionalTests/Web/CustomWebApplicationFactory.cs index aeb116c..ea52d80 100644 --- a/tests/FunctionalTests/Web/Controllers/CustomWebApplicationFactory.cs +++ b/tests/FunctionalTests/Web/CustomWebApplicationFactory.cs @@ -1,12 +1,12 @@ -using Microsoft.eShopWeb.Infrastructure.Data; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.eShopWeb.Infrastructure.Identity; namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers { diff --git a/tests/FunctionalTests/WebRazorPages/HomePageOnGet.cs b/tests/FunctionalTests/Web/Pages/HomePageOnGet.cs similarity index 72% rename from tests/FunctionalTests/WebRazorPages/HomePageOnGet.cs rename to tests/FunctionalTests/Web/Pages/HomePageOnGet.cs index 323ce90..1a795e1 100644 --- a/tests/FunctionalTests/WebRazorPages/HomePageOnGet.cs +++ b/tests/FunctionalTests/Web/Pages/HomePageOnGet.cs @@ -1,13 +1,14 @@ -using Microsoft.eShopWeb.RazorPages; +using Microsoft.eShopWeb.FunctionalTests.Web.Controllers; +using Microsoft.eShopWeb.Web; using System.Net.Http; using System.Threading.Tasks; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages { - public class HomePageOnGet : IClassFixture> + public class HomePageOnGet : IClassFixture> { - public HomePageOnGet(CustomWebRazorPagesApplicationFactory factory) + public HomePageOnGet(CustomWebApplicationFactory factory) { Client = factory.CreateClient(); } diff --git a/tests/FunctionalTests/WebRazorPages/CustomWebRazorPagesApplicationFactory.cs b/tests/FunctionalTests/WebRazorPages/CustomWebRazorPagesApplicationFactory.cs deleted file mode 100644 index b9bce7d..0000000 --- a/tests/FunctionalTests/WebRazorPages/CustomWebRazorPagesApplicationFactory.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.eShopWeb.Infrastructure.Data; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.eShopWeb.Web; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.eShopWeb.Infrastructure.Identity; - -namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages -{ - public class CustomWebRazorPagesApplicationFactory - : WebApplicationFactory - { - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureServices(services => - { - // Create a new service provider. - var serviceProvider = new ServiceCollection() - .AddEntityFrameworkInMemoryDatabase() - .BuildServiceProvider(); - - // Add a database context (ApplicationDbContext) using an in-memory - // database for testing. - services.AddDbContext(options => - { - options.UseInMemoryDatabase("InMemoryDbForTesting"); - options.UseInternalServiceProvider(serviceProvider); - }); - - services.AddDbContext(options => - { - options.UseInMemoryDatabase("Identity"); - options.UseInternalServiceProvider(serviceProvider); - }); - - // Build the service provider. - var sp = services.BuildServiceProvider(); - - // Create a scope to obtain a reference to the database - // context (ApplicationDbContext). - using (var scope = sp.CreateScope()) - { - var scopedServices = scope.ServiceProvider; - var db = scopedServices.GetRequiredService(); - var loggerFactory = scopedServices.GetRequiredService(); - - var logger = scopedServices - .GetRequiredService>>(); - - // Ensure the database is created. - db.Database.EnsureCreated(); - - try - { - // Seed the database with test data. - CatalogContextSeed.SeedAsync(db, loggerFactory).Wait(); - } - catch (Exception ex) - { - logger.LogError(ex, $"An error occurred seeding the " + - "database with test messages. Error: {ex.Message}"); - } - } - }); - } - } -} diff --git a/tests/FunctionalTests/WebRazorPages/OrderIndexOnGet.cs b/tests/FunctionalTests/WebRazorPages/OrderIndexOnGet.cs deleted file mode 100644 index e5a1959..0000000 --- a/tests/FunctionalTests/WebRazorPages/OrderIndexOnGet.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.eShopWeb.Web; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages -{ - public class OrderIndexOnGet : IClassFixture> - { - public OrderIndexOnGet(CustomWebRazorPagesApplicationFactory factory) - { - Client = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false - }); - } - - public HttpClient Client { get; } - - [Fact] - public async Task ReturnsRedirectGivenAnonymousUser() - { - var response = await Client.GetAsync("/Order/Index"); - var redirectLocation = response.Headers.Location.OriginalString; - - Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); - Assert.Contains("Account/Signin", redirectLocation); - } - } -} diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index 51ff2b4..2cbbf59 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -1,20 +1,24 @@  - netcoreapp2.1 + netcoreapp2.2 Microsoft.eShopWeb.IntegrationTests + false - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers + - + diff --git a/tests/IntegrationTests/Repositories/BasketItemRepositoryTests/DeleteAsync_Should.cs b/tests/IntegrationTests/Repositories/BasketItemRepositoryTests/DeleteAsync_Should.cs new file mode 100644 index 0000000..c9014f1 --- /dev/null +++ b/tests/IntegrationTests/Repositories/BasketItemRepositoryTests/DeleteAsync_Should.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.UnitTests.Builders; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.eShopWeb.IntegrationTests.Repositories.BasketItemRepositoryTests +{ + public class DeleteAsync_Should + { + private readonly CatalogContext _catalogContext; + private readonly EfRepository _basketRepository; + private readonly EfRepository _basketItemRepository; + private BasketBuilder BasketBuilder { get; } = new BasketBuilder(); + private readonly ITestOutputHelper _output; + + public DeleteAsync_Should(ITestOutputHelper output) + { + _output = output; + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TestCatalog") + .Options; + _catalogContext = new CatalogContext(dbOptions); + _basketRepository = new EfRepository(_catalogContext); + _basketItemRepository = new EfRepository(_catalogContext); + } + + [Fact] + public async Task DeleteItemFromBasket() + { + var existingBasket = BasketBuilder.WithOneBasketItem(); + _catalogContext.Add(existingBasket); + _catalogContext.SaveChanges(); + + await _basketItemRepository.DeleteAsync(existingBasket.Items.FirstOrDefault()); + _catalogContext.SaveChanges(); + + var basketFromDB = _basketRepository.GetById(BasketBuilder.BasketId); + + Assert.Equal(0, basketFromDB.Items.Count); + } + } +} diff --git a/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync_Should.cs b/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync_Should.cs new file mode 100644 index 0000000..1f8c75f --- /dev/null +++ b/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync_Should.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.UnitTests.Builders; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.eShopWeb.IntegrationTests.Repositories.OrderRepositoryTests +{ + public class GetByIdWithItemsAsync_Should + { + private readonly CatalogContext _catalogContext; + private readonly OrderRepository _orderRepository; + private OrderBuilder OrderBuilder { get; } = new OrderBuilder(); + private readonly ITestOutputHelper _output; + public GetByIdWithItemsAsync_Should(ITestOutputHelper output) + { + _output = output; + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TestCatalog") + .Options; + _catalogContext = new CatalogContext(dbOptions); + _orderRepository = new OrderRepository(_catalogContext); + } + + [Fact] + public async Task GetOrderAndItemsByOrderId_When_MultipleOrdersPresent() + { + //Arrange + var itemOneUnitPrice = 5.50m; + var itemOneUnits = 2; + var itemTwoUnitPrice = 7.50m; + var itemTwoUnits = 5; + + var firstOrder = OrderBuilder.WithDefaultValues(); + _catalogContext.Orders.Add(firstOrder); + int firstOrderId = firstOrder.Id; + + var secondOrderItems = new List(); + secondOrderItems.Add(new OrderItem(OrderBuilder.TestCatalogItemOrdered, itemOneUnitPrice, itemOneUnits)); + secondOrderItems.Add(new OrderItem(OrderBuilder.TestCatalogItemOrdered, itemTwoUnitPrice, itemTwoUnits)); + var secondOrder = OrderBuilder.WithItems(secondOrderItems); + _catalogContext.Orders.Add(secondOrder); + int secondOrderId = secondOrder.Id; + + _catalogContext.SaveChanges(); + + //Act + var orderFromRepo = await _orderRepository.GetByIdWithItemsAsync(secondOrderId); + + //Assert + Assert.Equal(secondOrderId, orderFromRepo.Id); + Assert.Equal(secondOrder.OrderItems.Count, orderFromRepo.OrderItems.Count); + Assert.Equal(1, orderFromRepo.OrderItems.Count(x => x.UnitPrice == itemOneUnitPrice)); + Assert.Equal(1, orderFromRepo.OrderItems.Count(x => x.UnitPrice == itemTwoUnitPrice)); + Assert.Equal(itemOneUnits, orderFromRepo.OrderItems.SingleOrDefault(x => x.UnitPrice == itemOneUnitPrice).Units); + Assert.Equal(itemTwoUnits, orderFromRepo.OrderItems.SingleOrDefault(x => x.UnitPrice == itemTwoUnitPrice).Units); + } + } +} diff --git a/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs b/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs index ad8d0ab..3849ec1 100644 --- a/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs +++ b/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs @@ -1,5 +1,4 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using System.Linq; using Xunit; diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs new file mode 100644 index 0000000..dae2c64 --- /dev/null +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs @@ -0,0 +1,37 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Services; +using Moq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests +{ + public class DeleteBasket + { + private Mock> _mockBasketRepo; + private Mock> _mockBasketItemRepo; + + public DeleteBasket() + { + _mockBasketRepo = new Mock>(); + _mockBasketItemRepo = new Mock>(); + } + + [Fact] + public async Task Should_InvokeBasketRepoOnceAndBasketItemRepoTwice_Given_TwoItemsInBasket() + { + var basket = new Basket(); + basket.AddItem(1, It.IsAny(), It.IsAny()); + basket.AddItem(2, It.IsAny(), It.IsAny()); + _mockBasketRepo.Setup(x => x.GetByIdAsync(It.IsAny())) + .ReturnsAsync(basket); + var basketService = new BasketService(_mockBasketRepo.Object, null, null, null, _mockBasketItemRepo.Object); + + await basketService.DeleteBasketAsync(It.IsAny()); + + _mockBasketRepo.Verify(x => x.DeleteAsync(It.IsAny()), Times.Once); + _mockBasketItemRepo.Verify(x => x.DeleteAsync(It.IsAny()), Times.Exactly(2)); + } + } +} diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/SetQuantities.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/SetQuantities.cs index 184b3ff..7de18e3 100644 --- a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/SetQuantities.cs +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/SetQuantities.cs @@ -1,7 +1,6 @@ using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; -using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Moq; using System; @@ -22,7 +21,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes [Fact] public async void ThrowsGivenInvalidBasketId() { - var basketService = new BasketService(_mockBasketRepo.Object, null, null, null); + var basketService = new BasketService(_mockBasketRepo.Object, null, null, null, null); await Assert.ThrowsAsync(async () => await basketService.SetQuantities(_invalidId, new System.Collections.Generic.Dictionary())); @@ -31,7 +30,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes [Fact] public async void ThrowsGivenNullQuantities() { - var basketService = new BasketService(null, null, null, null); + var basketService = new BasketService(null, null, null, null, null); await Assert.ThrowsAsync(async () => await basketService.SetQuantities(123, null)); diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs index 38c0a30..41e1069 100644 --- a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs @@ -9,7 +9,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes [Fact] public async void ThrowsGivenNullAnonymousId() { - var basketService = new BasketService(null, null, null, null); + var basketService = new BasketService(null, null, null, null, null); await Assert.ThrowsAsync(async () => await basketService.TransferBasketAsync(null, "steve")); } @@ -17,7 +17,7 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes [Fact] public async void ThrowsGivenNullUserId() { - var basketService = new BasketService(null, null, null, null); + var basketService = new BasketService(null, null, null, null, null); await Assert.ThrowsAsync(async () => await basketService.TransferBasketAsync("abcdefg", null)); } diff --git a/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs index b42e75e..ee55a9d 100644 --- a/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs +++ b/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -1,5 +1,4 @@ using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using System.Collections.Generic; using System.Linq; diff --git a/tests/UnitTests/Builders/BasketBuilder.cs b/tests/UnitTests/Builders/BasketBuilder.cs new file mode 100644 index 0000000..10d0632 --- /dev/null +++ b/tests/UnitTests/Builders/BasketBuilder.cs @@ -0,0 +1,34 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; + +namespace Microsoft.eShopWeb.UnitTests.Builders +{ + public class BasketBuilder + { + private Basket _basket; + public string BasketBuyerId => "testbuyerId@test.com"; + public int BasketId => 1; + + public BasketBuilder() + { + _basket = WithNoItems(); + } + + public Basket Build() + { + return _basket; + } + + public Basket WithNoItems() + { + _basket = new Basket { BuyerId = BasketBuyerId, Id = BasketId }; + return _basket; + } + + public Basket WithOneBasketItem() + { + _basket = new Basket { BuyerId = BasketBuyerId, Id = BasketId }; + _basket.AddItem(2, 3.40m, 4); + return _basket; + } + } +} diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index f351677..9917fe0 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -1,22 +1,28 @@  - netcoreapp2.1 + netcoreapp2.2 Microsoft.eShopWeb.UnitTests + false - + - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + - diff --git a/tests/XUnitTestProject1/UnitTest1.cs b/tests/XUnitTestProject1/UnitTest1.cs new file mode 100644 index 0000000..ded6b0c --- /dev/null +++ b/tests/XUnitTestProject1/UnitTest1.cs @@ -0,0 +1,14 @@ +using System; +using Xunit; + +namespace XUnitTestProject1 +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} diff --git a/tests/XUnitTestProject1/XUnitTestProject1.csproj b/tests/XUnitTestProject1/XUnitTestProject1.csproj new file mode 100644 index 0000000..3e02510 --- /dev/null +++ b/tests/XUnitTestProject1/XUnitTestProject1.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.2 + + false + + + + + + + + +
TypeLifetimeInstance