├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── dotnetcore.yml │ └── richnav.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CodeCoverage.runsettings ├── Directory.Packages.props ├── LICENSE ├── README.md ├── cgmanifest.json ├── eShopOnWeb.sln ├── global.json ├── src ├── ApplicationCore │ ├── ApplicationCore.csproj │ ├── CatalogSettings.cs │ ├── Constants │ │ └── AuthorizationConstants.cs │ ├── Entities │ │ ├── BaseEntity.cs │ │ ├── BasketAggregate │ │ │ ├── Basket.cs │ │ │ └── BasketItem.cs │ │ ├── BuyerAggregate │ │ │ ├── Buyer.cs │ │ │ └── PaymentMethod.cs │ │ ├── CatalogBrand.cs │ │ ├── CatalogItem.cs │ │ ├── CatalogType.cs │ │ ├── EshopDiagram.cd │ │ └── OrderAggregate │ │ │ ├── Address.cs │ │ │ ├── CatalogItemOrdered.cs │ │ │ ├── Order.cs │ │ │ └── OrderItem.cs │ ├── Exceptions │ │ ├── BasketNotFoundException.cs │ │ ├── DuplicateException.cs │ │ └── EmptyBasketOnCheckoutException.cs │ ├── Extensions │ │ ├── GuardExtensions.cs │ │ └── JsonExtensions.cs │ ├── Interfaces │ │ ├── IAggregateRoot.cs │ │ ├── IAppLogger.cs │ │ ├── IBasketQueryService.cs │ │ ├── IBasketService.cs │ │ ├── IEmailSender.cs │ │ ├── IOrderService.cs │ │ ├── IReadRepository.cs │ │ ├── IRepository.cs │ │ ├── ITokenClaimsService.cs │ │ └── IUriComposer.cs │ ├── Services │ │ ├── BasketService.cs │ │ ├── OrderService.cs │ │ └── UriComposer.cs │ └── Specifications │ │ ├── BasketWithItemsSpecification.cs │ │ ├── CatalogFilterPaginatedSpecification.cs │ │ ├── CatalogFilterSpecification.cs │ │ ├── CatalogItemNameSpecification.cs │ │ ├── CatalogItemsSpecification.cs │ │ ├── CustomerOrdersWithItemsSpecification.cs │ │ └── OrderWithItemsByIdSpec.cs ├── BlazorAdmin │ ├── App.razor │ ├── BlazorAdmin.csproj │ ├── CustomAuthStateProvider.cs │ ├── Helpers │ │ ├── BlazorComponent.cs │ │ ├── BlazorLayoutComponent.cs │ │ ├── RefreshBroadcast.cs │ │ └── ToastComponent.cs │ ├── JavaScript │ │ ├── Cookies.cs │ │ ├── Css.cs │ │ ├── JSInteropConstants.cs │ │ └── Route.cs │ ├── Pages │ │ ├── CatalogItemPage │ │ │ ├── Create.razor │ │ │ ├── Delete.razor │ │ │ ├── Details.razor │ │ │ ├── Edit.razor │ │ │ ├── List.razor │ │ │ └── List.razor.cs │ │ └── Logout.razor │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Services │ │ ├── CacheEntry.cs │ │ ├── CachedCatalogItemServiceDecorator.cs │ │ ├── CachedCatalogLookupDataServiceDecorator .cs │ │ ├── CatalogItemService.cs │ │ ├── CatalogLookupDataService.cs │ │ ├── HttpService.cs │ │ └── ToastService.cs │ ├── ServicesConfiguration.cs │ ├── Shared │ │ ├── CustomInputSelect.cs │ │ ├── MainLayout.razor │ │ ├── NavMenu.razor │ │ ├── RedirectToLogin.razor │ │ ├── Spinner.razor │ │ └── Toast.razor │ ├── _Imports.razor │ └── wwwroot │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ └── css │ │ ├── admin.css │ │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ └── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ ├── css │ │ └── open-iconic-bootstrap.min.css │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff ├── BlazorShared │ ├── Attributes │ │ └── EndpointAttribute.cs │ ├── Authorization │ │ ├── ClaimValue.cs │ │ ├── Constants.cs │ │ └── UserInfo.cs │ ├── BaseUrlConfiguration.cs │ ├── BlazorShared.csproj │ ├── Interfaces │ │ ├── ICatalogItemService.cs │ │ ├── ICatalogLookupDataService.cs │ │ └── ILookupDataResponse.cs │ └── Models │ │ ├── CatalogBrand.cs │ │ ├── CatalogBrandResponse.cs │ │ ├── CatalogItem.cs │ │ ├── CatalogType.cs │ │ ├── CatalogTypeResponse.cs │ │ ├── CreateCatalogItemRequest.cs │ │ ├── CreateCatalogItemResponse.cs │ │ ├── DeleteCatalogItemResponse.cs │ │ ├── EditCatalogItemResponse.cs │ │ ├── ErrorDetails.cs │ │ ├── LookupData.cs │ │ └── PagedCatalogItemResponse.cs ├── Infrastructure │ ├── Data │ │ ├── CatalogContext.cs │ │ ├── CatalogContextSeed.cs │ │ ├── Config │ │ │ ├── BasketConfiguration.cs │ │ │ ├── BasketItemConfiguration.cs │ │ │ ├── CatalogBrandConfiguration.cs │ │ │ ├── CatalogItemConfiguration.cs │ │ │ ├── CatalogTypeConfiguration.cs │ │ │ ├── OrderConfiguration.cs │ │ │ └── OrderItemConfiguration.cs │ │ ├── EfRepository.cs │ │ ├── FileItem.cs │ │ ├── Migrations │ │ │ ├── 20201202111507_InitialModel.Designer.cs │ │ │ ├── 20201202111507_InitialModel.cs │ │ │ ├── 20211026175614_FixBuyerId.Designer.cs │ │ │ ├── 20211026175614_FixBuyerId.cs │ │ │ ├── 20211231093753_FixShipToAddress.Designer.cs │ │ │ ├── 20211231093753_FixShipToAddress.cs │ │ │ └── CatalogContextModelSnapshot.cs │ │ └── Queries │ │ │ └── BasketQueryService.cs │ ├── Dependencies.cs │ ├── Identity │ │ ├── AppIdentityDbContext.cs │ │ ├── AppIdentityDbContextSeed.cs │ │ ├── ApplicationUser.cs │ │ ├── IdentityTokenClaimService.cs │ │ └── Migrations │ │ │ ├── 20201202111612_InitialIdentityModel.Designer.cs │ │ │ ├── 20201202111612_InitialIdentityModel.cs │ │ │ └── AppIdentityDbContextModelSnapshot.cs │ ├── Infrastructure.csproj │ ├── Logging │ │ └── LoggerAdapter.cs │ └── Services │ │ └── EmailSender.cs ├── PublicApi │ ├── AuthEndpoints │ │ ├── AuthenticateEndpoint.AuthenticateRequest.cs │ │ ├── AuthenticateEndpoint.AuthenticateResponse.cs │ │ ├── AuthenticateEndpoint.ClaimValue.cs │ │ ├── AuthenticateEndpoint.UserInfo.cs │ │ └── AuthenticateEndpoint.cs │ ├── BaseMessage.cs │ ├── BaseRequest.cs │ ├── BaseResponse.cs │ ├── CatalogBrandEndpoints │ │ ├── CatalogBrandDto.cs │ │ ├── CatalogBrandListEndpoint.ListCatalogBrandsResponse.cs │ │ └── CatalogBrandListEndpoint.cs │ ├── CatalogItemEndpoints │ │ ├── CatalogItemDto.cs │ │ ├── CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs │ │ ├── CatalogItemGetByIdEndpoint.GetByIdCatalogItemResponse.cs │ │ ├── CatalogItemGetByIdEndpoint.cs │ │ ├── CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs │ │ ├── CatalogItemListPagedEndpoint.ListPagedCatalogItemResponse.cs │ │ ├── CatalogItemListPagedEndpoint.cs │ │ ├── CreateCatalogItemEndpoint.CreateCatalogItemRequest.cs │ │ ├── CreateCatalogItemEndpoint.CreateCatalogItemResponse.cs │ │ ├── CreateCatalogItemEndpoint.cs │ │ ├── DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs │ │ ├── DeleteCatalogItemEndpoint.DeleteCatalogItemResponse.cs │ │ ├── DeleteCatalogItemEndpoint.cs │ │ ├── UpdateCatalogItemEndpoint.UpdateCatalogItemRequest.cs │ │ ├── UpdateCatalogItemEndpoint.UpdateCatalogItemResponse.cs │ │ └── UpdateCatalogItemEndpoint.cs │ ├── CatalogTypeEndpoints │ │ ├── CatalogTypeDto.cs │ │ ├── CatalogTypeListEndpoint.ListCatalogTypesResponse.cs │ │ └── CatalogTypeListEndpoint.cs │ ├── CustomSchemaFilters.cs │ ├── ImageValidators.cs │ ├── MappingProfile.cs │ ├── Middleware │ │ └── ExceptionMiddleware.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── PublicApi.csproj │ ├── README.md │ ├── appsettings.Development.json │ └── appsettings.json └── Web │ ├── .config │ └── dotnet-tools.json │ ├── Areas │ └── Identity │ │ ├── IdentityHostingStartup.cs │ │ └── Pages │ │ ├── Account │ │ ├── ConfirmEmail.cshtml │ │ ├── ConfirmEmail.cshtml.cs │ │ ├── Login.cshtml │ │ ├── Login.cshtml.cs │ │ ├── Logout.cshtml │ │ ├── Logout.cshtml.cs │ │ ├── Register.cshtml │ │ ├── Register.cshtml.cs │ │ └── _ViewImports.cshtml │ │ ├── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Configuration │ ├── ConfigureCookieSettings.cs │ ├── ConfigureCoreServices.cs │ ├── ConfigureWebServices.cs │ └── RevokeAuthenticationEvents.cs │ ├── Constants.cs │ ├── Controllers │ ├── Api │ │ └── BaseApiController.cs │ ├── ManageController.cs │ ├── OrderController.cs │ └── UserController.cs │ ├── Extensions │ ├── CacheHelpers.cs │ ├── EmailSenderExtensions.cs │ └── UrlHelperExtensions.cs │ ├── Features │ ├── MyOrders │ │ ├── GetMyOrders.cs │ │ └── GetMyOrdersHandler.cs │ └── OrderDetails │ │ ├── GetOrderDetails.cs │ │ └── GetOrderDetailsHandler.cs │ ├── HealthChecks │ ├── ApiHealthCheck.cs │ └── HomePageHealthCheck.cs │ ├── Interfaces │ ├── IBasketViewModelService.cs │ ├── ICatalogItemViewModelService.cs │ └── ICatalogViewModelService.cs │ ├── Pages │ ├── Admin │ │ ├── EditCatalogItem.cshtml │ │ ├── EditCatalogItem.cshtml.cs │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ ├── Basket │ │ ├── BasketItemViewModel.cs │ │ ├── BasketViewModel.cs │ │ ├── Checkout.cshtml │ │ ├── Checkout.cshtml.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── Success.cshtml │ │ └── Success.cshtml.cs │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Index.cshtml │ ├── Index.cshtml.cs │ ├── Privacy.cshtml │ ├── Privacy.cshtml.cs │ ├── Shared │ │ ├── Components │ │ │ └── BasketComponent │ │ │ │ ├── Basket.cs │ │ │ │ └── Default.cshtml │ │ ├── _editCatalog.cshtml │ │ ├── _pagination.cshtml │ │ └── _product.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Services │ ├── BasketViewModelService.cs │ ├── CachedCatalogViewModelService.cs │ ├── CatalogItemViewModelService.cs │ └── CatalogViewModelService.cs │ ├── SlugifyParameterTransformer.cs │ ├── ViewModels │ ├── Account │ │ ├── LoginViewModel.cs │ │ ├── LoginWith2faViewModel.cs │ │ ├── RegisterViewModel.cs │ │ └── ResetPasswordViewModel.cs │ ├── BasketComponentViewModel.cs │ ├── CatalogIndexViewModel.cs │ ├── CatalogItemViewModel.cs │ ├── File │ │ └── FileViewModel.cs │ ├── Manage │ │ ├── ChangePasswordViewModel.cs │ │ ├── EnableAuthenticatorViewModel.cs │ │ ├── ExternalLoginsViewModel.cs │ │ ├── IndexViewModel.cs │ │ ├── RemoveLoginViewModel.cs │ │ ├── SetPasswordViewModel.cs │ │ ├── ShowRecoveryCodesViewModel.cs │ │ └── TwoFactorAuthenticationViewModel.cs │ ├── OrderItemViewModel.cs │ ├── OrderViewModel.cs │ └── PaginationInfoViewModel.cs │ ├── Views │ ├── Account │ │ ├── Lockout.cshtml │ │ └── LoginWith2fa.cshtml │ ├── Manage │ │ ├── ChangePassword.cshtml │ │ ├── Disable2fa.cshtml │ │ ├── EnableAuthenticator.cshtml │ │ ├── ExternalLogins.cshtml │ │ ├── GenerateRecoveryCodes.cshtml │ │ ├── ManageNavPages.cs │ │ ├── MyAccount.cshtml │ │ ├── ResetAuthenticator.cshtml │ │ ├── SetPassword.cshtml │ │ ├── ShowRecoverCodes.cshtml │ │ ├── TwoFactorAuthentication.cshtml │ │ ├── _Layout.cshtml │ │ ├── _ManageNav.cshtml │ │ ├── _StatusMessage.cshtml │ │ └── _ViewImports.cshtml │ ├── Order │ │ ├── Detail.cshtml │ │ └── MyOrders.cshtml │ ├── Shared │ │ ├── Components │ │ │ └── Basket │ │ │ │ └── Default.cshtml │ │ ├── Error.cshtml │ │ ├── _CookieConsentPartial.cshtml │ │ ├── _Layout.cshtml │ │ ├── _LoginPartial.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── Web.csproj │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── bundleconfig.json │ ├── compilerconfig.json │ ├── compilerconfig.json.defaults │ ├── key-768c1632-cf7b-41a9-bb7a-bff228ae8fba.xml │ ├── libman.json │ └── wwwroot │ ├── css │ ├── _variables.css │ ├── _variables.min.css │ ├── _variables.scss │ ├── app.component.css │ ├── app.component.min.css │ ├── app.component.scss │ ├── app.css │ ├── app.min.css │ ├── basket │ │ ├── basket-status │ │ │ ├── basket-status.component.css │ │ │ ├── basket-status.component.min.css │ │ │ └── basket-status.component.scss │ │ ├── basket.component.css │ │ ├── basket.component.min.css │ │ └── basket.component.scss │ ├── catalog │ │ ├── catalog.component.css │ │ ├── catalog.component.min.css │ │ ├── catalog.component.scss │ │ └── pager.css │ ├── orders │ │ ├── orders.component.css │ │ ├── orders.component.min.css │ │ └── orders.component.scss │ ├── shared │ │ └── components │ │ │ ├── header │ │ │ ├── header.css │ │ │ ├── header.min.css │ │ │ └── header.scss │ │ │ ├── identity │ │ │ ├── identity.css │ │ │ ├── identity.min.css │ │ │ └── identity.scss │ │ │ └── pager │ │ │ ├── pager.css │ │ │ ├── pager.min.css │ │ │ └── pager.scss │ └── site.min.css │ ├── favicon.ico │ ├── fonts │ ├── Montserrat-Bold.eot │ ├── Montserrat-Bold.svg │ ├── Montserrat-Bold.ttf │ ├── Montserrat-Bold.woff │ ├── Montserrat-Bold.woff2 │ ├── Montserrat-Regular.eot │ ├── Montserrat-Regular.svg │ ├── Montserrat-Regular.ttf │ ├── Montserrat-Regular.woff │ └── Montserrat-Regular.woff2 │ ├── images │ ├── arrow-down.png │ ├── arrow-right.svg │ ├── brand.png │ ├── cart.png │ ├── logout.png │ ├── main_banner.png │ ├── main_banner_text.png │ ├── main_banner_text.svg │ ├── my_orders.png │ ├── products │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.jpg │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── 9.png │ │ └── eCatalog-item-default.png │ └── refresh.svg │ └── js │ ├── site.js │ └── site.min.js └── tests ├── FunctionalTests ├── FunctionalTests.csproj ├── PublicApi │ ├── ApiTestFixture.cs │ ├── ApiTokenHelper.cs │ └── AuthEndpoints │ │ └── AuthenticateEndpoint.cs └── Web │ ├── Controllers │ ├── AccountControllerSignIn.cs │ ├── CatalogControllerIndex.cs │ └── OrderControllerIndex.cs │ ├── Pages │ ├── Basket │ │ ├── BasketPageCheckout.cs │ │ ├── CheckoutTest.cs │ │ └── IndexTest.cs │ └── HomePageOnGet.cs │ ├── WebPageHelpers.cs │ └── WebTestFixture.cs ├── IntegrationTests ├── IntegrationTests.csproj └── Repositories │ ├── BasketRepositoryTests │ └── SetQuantities.cs │ └── OrderRepositoryTests │ ├── GetById.cs │ └── GetByIdWithItemsAsync.cs ├── PublicApiIntegrationTests ├── ApiTokenHelper.cs ├── AuthEndpoints │ └── AuthenticateEndpointTest.cs ├── CatalogItemEndpoints │ ├── CatalogItemGetByIdEndpointTest.cs │ ├── CatalogItemListPagedEndpoint.cs │ ├── CreateCatalogItemEndpointTest.cs │ └── DeleteCatalogItemEndpointTest.cs ├── ProgramTest.cs ├── PublicApiIntegrationTests.csproj └── appsettings.test.json └── UnitTests ├── ApplicationCore ├── Entities │ ├── BasketTests │ │ ├── BasketAddItem.cs │ │ ├── BasketRemoveEmptyItems.cs │ │ └── BasketTotalItems.cs │ └── OrderTests │ │ └── OrderTotal.cs ├── Extensions │ ├── JsonExtensions.cs │ ├── TestChild.cs │ └── TestParent.cs ├── Services │ └── BasketServiceTests │ │ ├── AddItemToBasket.cs │ │ ├── DeleteBasket.cs │ │ └── TransferBasket.cs └── Specifications │ ├── BasketWithItemsSpecification.cs │ ├── CatalogFilterPaginatedSpecification.cs │ ├── CatalogFilterSpecification.cs │ ├── CatalogItemsSpecification.cs │ └── CustomerOrdersWithItemsSpecification.cs ├── Builders ├── AddressBuilder.cs ├── BasketBuilder.cs └── OrderBuilder.cs ├── MediatorHandlers └── OrdersTests │ ├── GetMyOrders.cs │ └── GetOrderDetails.cs ├── UnitTests.csproj └── Web └── Extensions └── CacheHelpersTests ├── GenerateBrandsCacheKey.cs ├── GenerateCatalogItemCacheKey.cs └── GenerateTypesCacheKey.cs /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | */bin 8 | */obj 9 | **/.toolstarget -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: eShopOnWeb Build and Test 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: '7.0.x' 16 | include-prerelease: true 17 | 18 | - name: Build with dotnet 19 | run: dotnet build ./eShopOnWeb.sln --configuration Release 20 | 21 | - name: Test with dotnet 22 | run: dotnet test ./eShopOnWeb.sln --configuration Release 23 | -------------------------------------------------------------------------------- /.github/workflows/richnav.yml: -------------------------------------------------------------------------------- 1 | name: eShopOnWeb Rich Code Navigation 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-2019 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 7.0.x 16 | 17 | - name: Build with dotnet 18 | run: dotnet build ./eShopOnWeb.sln --configuration Release 19 | 20 | - uses: microsoft/RichCodeNavIndexer@v0.1 21 | with: 22 | repo-token: ${{ github.token }} -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp", 4 | "formulahendry.dotnet-test-explorer", 5 | "ms-vscode.vscode-node-azure-pack", 6 | "ms-kubernetes-tools.vscode-kubernetes-tools", 7 | "redhat.vscode-yaml" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Web/Web.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Web/Web.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/Web/Web.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 4 | 5 | Copyright (c) .NET Foundation and Contributors 6 | 7 | 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | 11 | of this software and associated documentation files (the "Software"), to deal 12 | 13 | in the Software without restriction, including without limitation the rights 14 | 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | 17 | copies of the Software, and to permit persons to whom the Software is 18 | 19 | furnished to do so, subject to the following conditions: 20 | 21 | 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | 25 | copies or substantial portions of the Software. 26 | 27 | 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | 37 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 38 | 39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 40 | 41 | SOFTWARE. 42 | -------------------------------------------------------------------------------- /cgmanifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/component-detection-manifest.json", 3 | "version": 1, 4 | "registrations": [ 5 | { 6 | "component": { 7 | "type": "git", 8 | "git": { 9 | "repositoryUrl": "https://github.com/dotnet-architecture/eShopOnWeb", 10 | "commitHash": "c5e7fd33488cb0cd136eba86b4c158a8abaa40b1" 11 | } 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "7.0.x", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/ApplicationCore/ApplicationCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.eShopWeb.ApplicationCore 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ApplicationCore/CatalogSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb; 2 | 3 | public class CatalogSettings 4 | { 5 | public string? CatalogBaseUrl { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/ApplicationCore/Constants/AuthorizationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Constants; 2 | 3 | public class AuthorizationConstants 4 | { 5 | public const string AUTH_KEY = "AuthKeyOfDoomThatMustBeAMinimumNumberOfBytes"; 6 | 7 | // TODO: Don't use this in production 8 | public const string DEFAULT_PASSWORD = "Pass@word1"; 9 | 10 | // TODO: Change this to an environment variable 11 | public const string JWT_SECRET_KEY = "SecretKeyOfDoomThatMustBeAMinimumNumberOfBytes"; 12 | } 13 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Entities; 2 | 3 | // This can easily be modified to be BaseEntity and public T Id to support different key types. 4 | // Using non-generic integer types for simplicity and to ease caching logic 5 | public abstract class BaseEntity 6 | { 7 | public virtual int Id { get; protected set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/BasketAggregate/Basket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Ardalis.GuardClauses; 4 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 5 | 6 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 7 | 8 | public class Basket : BaseEntity, IAggregateRoot 9 | { 10 | public string BuyerId { get; private set; } 11 | private readonly List _items = new List(); 12 | public IReadOnlyCollection Items => _items.AsReadOnly(); 13 | 14 | public int TotalItems => _items.Sum(i => i.Quantity); 15 | 16 | 17 | public Basket(string buyerId) 18 | { 19 | BuyerId = buyerId; 20 | } 21 | 22 | public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1) 23 | { 24 | if (!Items.Any(i => i.CatalogItemId == catalogItemId)) 25 | { 26 | _items.Add(new BasketItem(catalogItemId, quantity, unitPrice)); 27 | return; 28 | } 29 | var existingItem = Items.First(i => i.CatalogItemId == catalogItemId); 30 | existingItem.AddQuantity(quantity); 31 | } 32 | 33 | public void RemoveEmptyItems() 34 | { 35 | _items.RemoveAll(i => i.Quantity == 0); 36 | } 37 | 38 | public void SetNewBuyerId(string buyerId) 39 | { 40 | BuyerId = buyerId; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/BasketAggregate/BasketItem.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 4 | 5 | public class BasketItem : BaseEntity 6 | { 7 | 8 | public decimal UnitPrice { get; private set; } 9 | public int Quantity { get; private set; } 10 | public int CatalogItemId { get; private set; } 11 | public int BasketId { get; private set; } 12 | 13 | public BasketItem(int catalogItemId, int quantity, decimal unitPrice) 14 | { 15 | CatalogItemId = catalogItemId; 16 | UnitPrice = unitPrice; 17 | SetQuantity(quantity); 18 | } 19 | 20 | public void AddQuantity(int quantity) 21 | { 22 | Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue); 23 | 24 | Quantity += quantity; 25 | } 26 | 27 | public void SetQuantity(int quantity) 28 | { 29 | Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue); 30 | 31 | Quantity = quantity; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Ardalis.GuardClauses; 3 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate; 6 | 7 | public class Buyer : BaseEntity, IAggregateRoot 8 | { 9 | public string IdentityGuid { get; private set; } 10 | 11 | private List _paymentMethods = new List(); 12 | 13 | public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); 14 | 15 | #pragma warning disable CS8618 // Required by Entity Framework 16 | private Buyer() { } 17 | 18 | public Buyer(string identity) : this() 19 | { 20 | Guard.Against.NullOrEmpty(identity, nameof(identity)); 21 | IdentityGuid = identity; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate; 2 | 3 | public class PaymentMethod : BaseEntity 4 | { 5 | public string? Alias { get; private set; } 6 | public string? CardId { get; private set; } // actual card data must be stored in a PCI compliant system, like Stripe 7 | public string? Last4 { get; private set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/CatalogBrand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Entities; 4 | 5 | public class CatalogBrand : BaseEntity, IAggregateRoot 6 | { 7 | public string Brand { get; private set; } 8 | public CatalogBrand(string brand) 9 | { 10 | Brand = brand; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/CatalogType.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Entities; 4 | 5 | public class CatalogType : BaseEntity, IAggregateRoot 6 | { 7 | public string Type { get; private set; } 8 | public CatalogType(string type) 9 | { 10 | Type = type; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/EshopDiagram.cd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAA= 7 | Entities\CatalogBrand.cs 8 | 9 | 10 | 11 | 12 | 13 | 14 | AAgAAAAAA4AgAwAAAAAAAAQAAAEAAAAAAAAAAQAACQA= 15 | Entities\CatalogItem.cs 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA= 27 | Entities\CatalogType.cs 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/OrderAggregate/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 2 | 3 | public class Address // ValueObject 4 | { 5 | public string Street { get; private set; } 6 | 7 | public string City { get; private set; } 8 | 9 | public string State { get; private set; } 10 | 11 | public string Country { get; private set; } 12 | 13 | public string ZipCode { get; private set; } 14 | 15 | #pragma warning disable CS8618 // Required by Entity Framework 16 | private Address() { } 17 | 18 | public Address(string street, string city, string state, string country, string zipcode) 19 | { 20 | Street = street; 21 | City = city; 22 | State = state; 23 | Country = country; 24 | ZipCode = zipcode; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 4 | 5 | /// 6 | /// Represents a snapshot of the item that was ordered. If catalog item details change, details of 7 | /// the item that was part of a completed order should not change. 8 | /// 9 | public class CatalogItemOrdered // ValueObject 10 | { 11 | public CatalogItemOrdered(int catalogItemId, string productName, string pictureUri) 12 | { 13 | Guard.Against.OutOfRange(catalogItemId, nameof(catalogItemId), 1, int.MaxValue); 14 | Guard.Against.NullOrEmpty(productName, nameof(productName)); 15 | Guard.Against.NullOrEmpty(pictureUri, nameof(pictureUri)); 16 | 17 | CatalogItemId = catalogItemId; 18 | ProductName = productName; 19 | PictureUri = pictureUri; 20 | } 21 | 22 | #pragma warning disable CS8618 // Required by Entity Framework 23 | private CatalogItemOrdered() {} 24 | 25 | public int CatalogItemId { get; private set; } 26 | public string ProductName { get; private set; } 27 | public string PictureUri { get; private set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 2 | 3 | public class OrderItem : BaseEntity 4 | { 5 | public CatalogItemOrdered ItemOrdered { get; private set; } 6 | public decimal UnitPrice { get; private set; } 7 | public int Units { get; private set; } 8 | 9 | #pragma warning disable CS8618 // Required by Entity Framework 10 | private OrderItem() {} 11 | 12 | public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) 13 | { 14 | ItemOrdered = itemOrdered; 15 | UnitPrice = unitPrice; 16 | Units = units; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ApplicationCore/Exceptions/BasketNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Exceptions; 4 | 5 | public class BasketNotFoundException : Exception 6 | { 7 | public BasketNotFoundException(int basketId) : base($"No basket found with id {basketId}") 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ApplicationCore/Exceptions/DuplicateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Exceptions; 4 | 5 | public class DuplicateException : Exception 6 | { 7 | public DuplicateException(string message) : base(message) 8 | { 9 | 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/ApplicationCore/Exceptions/EmptyBasketOnCheckoutException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Exceptions; 4 | 5 | public class EmptyBasketOnCheckoutException : Exception 6 | { 7 | public EmptyBasketOnCheckoutException() 8 | : base($"Basket cannot have 0 items on checkout") 9 | { 10 | } 11 | 12 | protected EmptyBasketOnCheckoutException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) 13 | { 14 | } 15 | 16 | public EmptyBasketOnCheckoutException(string message) : base(message) 17 | { 18 | } 19 | 20 | public EmptyBasketOnCheckoutException(string message, Exception innerException) : base(message, innerException) 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ApplicationCore/Extensions/GuardExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 4 | using Microsoft.eShopWeb.ApplicationCore.Exceptions; 5 | 6 | namespace Ardalis.GuardClauses; 7 | 8 | public static class BasketGuards 9 | { 10 | public static void EmptyBasketOnCheckout(this IGuardClause guardClause, IReadOnlyCollection basketItems) 11 | { 12 | if (!basketItems.Any()) 13 | throw new EmptyBasketOnCheckoutException(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ApplicationCore/Extensions/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Microsoft.eShopWeb; 4 | 5 | public static class JsonExtensions 6 | { 7 | private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions 8 | { 9 | PropertyNameCaseInsensitive = true 10 | }; 11 | 12 | public static T? FromJson(this string json) => 13 | JsonSerializer.Deserialize(json, _jsonOptions); 14 | 15 | public static string ToJson(this T obj) => 16 | JsonSerializer.Serialize(obj, _jsonOptions); 17 | } 18 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | 3 | public interface IAggregateRoot 4 | { } 5 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IAppLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | 3 | /// 4 | /// This type eliminates the need to depend directly on the ASP.NET Core logging types. 5 | /// 6 | /// 7 | public interface IAppLogger 8 | { 9 | void LogInformation(string message, params object[] args); 10 | void LogWarning(string message, params object[] args); 11 | } 12 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IBasketQueryService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | /// 6 | /// Specific query used to fetch count without running in memory 7 | /// 8 | public interface IBasketQueryService 9 | { 10 | Task CountTotalBasketItems(string username); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IBasketService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Ardalis.Result; 4 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 5 | 6 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 7 | 8 | public interface IBasketService 9 | { 10 | Task TransferBasketAsync(string anonymousId, string userName); 11 | Task AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1); 12 | Task> SetQuantities(int basketId, Dictionary quantities); 13 | Task DeleteBasketAsync(int basketId); 14 | } 15 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | public interface IEmailSender 6 | { 7 | Task SendEmailAsync(string email, string subject, string message); 8 | } 9 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IOrderService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 5 | 6 | public interface IOrderService 7 | { 8 | Task CreateOrderAsync(int basketId, Address shippingAddress); 9 | } 10 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IReadRepository.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | public interface IReadRepository : IReadRepositoryBase where T : class, IAggregateRoot 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IRepository.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | public interface IRepository : IRepositoryBase where T : class, IAggregateRoot 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/ITokenClaimsService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | public interface ITokenClaimsService 6 | { 7 | Task GetTokenAsync(string userName); 8 | } 9 | -------------------------------------------------------------------------------- /src/ApplicationCore/Interfaces/IUriComposer.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | 3 | public interface IUriComposer 4 | { 5 | string ComposePicUri(string uriTemplate); 6 | } 7 | -------------------------------------------------------------------------------- /src/ApplicationCore/Services/UriComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | 3 | namespace Microsoft.eShopWeb.ApplicationCore.Services; 4 | 5 | public class UriComposer : IUriComposer 6 | { 7 | private readonly CatalogSettings _catalogSettings; 8 | 9 | public UriComposer(CatalogSettings catalogSettings) => _catalogSettings = catalogSettings; 10 | 11 | public string ComposePicUri(string uriTemplate) 12 | { 13 | return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public sealed class BasketWithItemsSpecification : Specification, ISingleResultSpecification 7 | { 8 | public BasketWithItemsSpecification(int basketId) 9 | { 10 | Query 11 | .Where(b => b.Id == basketId) 12 | .Include(b => b.Items); 13 | } 14 | 15 | public BasketWithItemsSpecification(string buyerId) 16 | { 17 | Query 18 | .Where(b => b.BuyerId == buyerId) 19 | .Include(b => b.Items); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public class CatalogFilterPaginatedSpecification : Specification 7 | { 8 | public CatalogFilterPaginatedSpecification(int skip, int take, int? brandId, int? typeId) 9 | : base() 10 | { 11 | if (take == 0) 12 | { 13 | take = int.MaxValue; 14 | } 15 | Query 16 | .Where(i => (!brandId.HasValue || i.CatalogBrandId == brandId) && 17 | (!typeId.HasValue || i.CatalogTypeId == typeId)) 18 | .Skip(skip).Take(take); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/CatalogFilterSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public class CatalogFilterSpecification : Specification 7 | { 8 | public CatalogFilterSpecification(int? brandId, int? typeId) 9 | { 10 | Query.Where(i => (!brandId.HasValue || i.CatalogBrandId == brandId) && 11 | (!typeId.HasValue || i.CatalogTypeId == typeId)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/CatalogItemNameSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public class CatalogItemNameSpecification : Specification 7 | { 8 | public CatalogItemNameSpecification(string catalogItemName) 9 | { 10 | Query.Where(item => catalogItemName == item.Name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/CatalogItemsSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Ardalis.Specification; 4 | using Microsoft.eShopWeb.ApplicationCore.Entities; 5 | 6 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 7 | 8 | public class CatalogItemsSpecification : Specification 9 | { 10 | public CatalogItemsSpecification(params int[] ids) 11 | { 12 | Query.Where(c => ids.Contains(c.Id)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public class CustomerOrdersWithItemsSpecification : Specification 7 | { 8 | public CustomerOrdersWithItemsSpecification(string buyerId) 9 | { 10 | Query.Where(o => o.BuyerId == buyerId) 11 | .Include(o => o.OrderItems) 12 | .ThenInclude(i => i.ItemOrdered); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ApplicationCore/Specifications/OrderWithItemsByIdSpec.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public class OrderWithItemsByIdSpec : Specification, ISingleResultSpecification 7 | { 8 | public OrderWithItemsByIdSpec(int orderId) 9 | { 10 | Query 11 | .Where(order => order.Id == orderId) 12 | .Include(o => o.OrderItems) 13 | .ThenInclude(i => i.ItemOrdered); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/BlazorAdmin/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | @if (!context.User.Identity.IsAuthenticated) 8 | { 9 | 10 | } 11 | else 12 | { 13 |

Not Authorized

14 |

15 | You are not authorized to access 16 | this resource. 17 | 18 | Return to eShop 19 |

20 | } 21 |
22 |
23 |
24 | 25 | 26 |

Sorry, there's nothing at this address.

27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /src/BlazorAdmin/BlazorAdmin.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Delete.cs 21 | 22 | 23 | GetById.cs 24 | 25 | 26 | Edit.cs 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Helpers/BlazorComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace BlazorAdmin.Helpers; 4 | 5 | public class BlazorComponent : ComponentBase 6 | { 7 | private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance; 8 | 9 | protected override void OnInitialized() 10 | { 11 | _refresh.RefreshRequested += DoRefresh; 12 | base.OnInitialized(); 13 | } 14 | 15 | public void CallRequestRefresh() 16 | { 17 | _refresh.CallRequestRefresh(); 18 | } 19 | 20 | private void DoRefresh() 21 | { 22 | StateHasChanged(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace BlazorAdmin.Helpers; 4 | 5 | public class BlazorLayoutComponent : LayoutComponentBase 6 | { 7 | private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance; 8 | 9 | protected override void OnInitialized() 10 | { 11 | _refresh.RefreshRequested += DoRefresh; 12 | base.OnInitialized(); 13 | } 14 | 15 | public void CallRequestRefresh() 16 | { 17 | _refresh.CallRequestRefresh(); 18 | } 19 | 20 | private void DoRefresh() 21 | { 22 | StateHasChanged(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Helpers/RefreshBroadcast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorAdmin.Helpers; 4 | 5 | internal sealed class RefreshBroadcast 6 | { 7 | private static readonly Lazy 8 | Lazy = 9 | new Lazy 10 | (() => new RefreshBroadcast()); 11 | 12 | public static RefreshBroadcast Instance => Lazy.Value; 13 | 14 | private RefreshBroadcast() 15 | { 16 | } 17 | 18 | public event Action RefreshRequested; 19 | public void CallRequestRefresh() 20 | { 21 | RefreshRequested?.Invoke(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/BlazorAdmin/JavaScript/Cookies.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.JSInterop; 3 | 4 | namespace BlazorAdmin.JavaScript; 5 | 6 | public class Cookies 7 | { 8 | private readonly IJSRuntime _jsRuntime; 9 | 10 | public Cookies(IJSRuntime jsRuntime) 11 | { 12 | _jsRuntime = jsRuntime; 13 | } 14 | 15 | public async Task DeleteCookie(string name) 16 | { 17 | await _jsRuntime.InvokeAsync(JSInteropConstants.DeleteCookie, name); 18 | } 19 | 20 | public async Task GetCookie(string name) 21 | { 22 | return await _jsRuntime.InvokeAsync(JSInteropConstants.GetCookie, name); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/BlazorAdmin/JavaScript/Css.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.JSInterop; 3 | 4 | namespace BlazorAdmin.JavaScript; 5 | 6 | public class Css 7 | { 8 | private readonly IJSRuntime _jsRuntime; 9 | 10 | public Css(IJSRuntime jsRuntime) 11 | { 12 | _jsRuntime = jsRuntime; 13 | } 14 | 15 | public async Task ShowBodyOverflow() 16 | { 17 | await _jsRuntime.InvokeAsync(JSInteropConstants.ShowBodyOverflow); 18 | } 19 | 20 | public async Task HideBodyOverflow() 21 | { 22 | return await _jsRuntime.InvokeAsync(JSInteropConstants.HideBodyOverflow); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/BlazorAdmin/JavaScript/JSInteropConstants.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAdmin.JavaScript; 2 | 3 | public static class JSInteropConstants 4 | { 5 | public static string DeleteCookie => "deleteCookie"; 6 | public static string GetCookie => "getCookie"; 7 | public static string RouteOutside => "routeOutside"; 8 | public static string HideBodyOverflow => "hideBodyOverflow"; 9 | public static string ShowBodyOverflow => "showBodyOverflow"; 10 | } 11 | -------------------------------------------------------------------------------- /src/BlazorAdmin/JavaScript/Route.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.JSInterop; 3 | 4 | namespace BlazorAdmin.JavaScript; 5 | 6 | public class Route 7 | { 8 | private readonly IJSRuntime _jsRuntime; 9 | 10 | public Route(IJSRuntime jsRuntime) 11 | { 12 | _jsRuntime = jsRuntime; 13 | } 14 | 15 | public async Task RouteOutside(string path) 16 | { 17 | await _jsRuntime.InvokeAsync(JSInteropConstants.RouteOutside, path); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Pages/Logout.razor: -------------------------------------------------------------------------------- 1 | @page "/logout" 2 | @inject IJSRuntime JSRuntime 3 | @inject HttpClient HttpClient 4 | @inherits BlazorAdmin.Helpers.BlazorComponent 5 | 6 | @code { 7 | 8 | protected override async Task OnInitializedAsync() 9 | { 10 | await HttpClient.PostAsync("User/Logout", null); 11 | await new Route(JSRuntime).RouteOutside("/Identity/Account/Login"); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58126", 7 | "sslPort": 44315 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "BlazorAdmin": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Services/CacheEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorAdmin.Services; 4 | 5 | public class CacheEntry 6 | { 7 | public CacheEntry(T item) 8 | { 9 | Value = item; 10 | } 11 | public CacheEntry() 12 | { 13 | 14 | } 15 | 16 | public T Value { get; set; } 17 | public DateTime DateCreated { get; set; } = DateTime.UtcNow; 18 | } 19 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Services/ToastService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Timers; 3 | 4 | namespace BlazorAdmin.Services; 5 | 6 | public enum ToastLevel 7 | { 8 | Info, 9 | Success, 10 | Warning, 11 | Error 12 | } 13 | 14 | public class ToastService : IDisposable 15 | { 16 | public event Action OnShow; 17 | public event Action OnHide; 18 | private Timer Countdown; 19 | public void ShowToast(string message, ToastLevel level) 20 | { 21 | OnShow?.Invoke(message, level); 22 | StartCountdown(); 23 | } 24 | private void StartCountdown() 25 | { 26 | SetCountdown(); 27 | if (Countdown.Enabled) 28 | { 29 | Countdown.Stop(); 30 | Countdown.Start(); 31 | } 32 | else 33 | { 34 | Countdown.Start(); 35 | } 36 | } 37 | private void SetCountdown() 38 | { 39 | if (Countdown == null) 40 | { 41 | Countdown = new Timer(3000); 42 | Countdown.Elapsed += HideToast; 43 | Countdown.AutoReset = false; 44 | } 45 | } 46 | private void HideToast(object source, ElapsedEventArgs args) 47 | { 48 | OnHide?.Invoke(); 49 | } 50 | public void Dispose() 51 | { 52 | Countdown?.Dispose(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/BlazorAdmin/ServicesConfiguration.cs: -------------------------------------------------------------------------------- 1 | using BlazorAdmin.Services; 2 | using BlazorShared.Interfaces; 3 | using BlazorShared.Models; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace BlazorAdmin; 7 | 8 | public static class ServicesConfiguration 9 | { 10 | public static IServiceCollection AddBlazorServices(this IServiceCollection services) 11 | { 12 | services.AddScoped, CachedCatalogLookupDataServiceDecorator>(); 13 | services.AddScoped>(); 14 | services.AddScoped, CachedCatalogLookupDataServiceDecorator>(); 15 | services.AddScoped>(); 16 | services.AddScoped(); 17 | services.AddScoped(); 18 | 19 | return services; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Shared/CustomInputSelect.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Forms; 2 | 3 | namespace BlazorAdmin.Shared; 4 | 5 | /// 6 | /// This is needed until 5.0 ships with native support 7 | /// https://www.pragimtech.com/blog/blazor/inputselect-does-not-support-system.int32/ 8 | /// 9 | /// 10 | public class CustomInputSelect : InputSelect 11 | { 12 | protected override bool TryParseValueFromString(string value, out TValue result, 13 | out string validationErrorMessage) 14 | { 15 | if (typeof(TValue) == typeof(int)) 16 | { 17 | if (int.TryParse(value, out var resultInt)) 18 | { 19 | result = (TValue)(object)resultInt; 20 | validationErrorMessage = null; 21 | return true; 22 | } 23 | else 24 | { 25 | result = default; 26 | validationErrorMessage = 27 | $"The selected value {value} is not a valid number."; 28 | return false; 29 | } 30 | } 31 | else 32 | { 33 | return base.TryParseValueFromString(value, out result, 34 | out validationErrorMessage); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inject AuthenticationStateProvider AuthStateProvider 2 | @inject IJSRuntime JSRuntime 3 | 4 | @inherits BlazorAdmin.Helpers.BlazorLayoutComponent 5 | 6 | 7 | 8 | 11 | 12 | 13 |
14 | 15 |
16 | About eShopOnWeb 17 |
18 | 19 |
20 | 21 | @Body 22 |
23 |
24 | @code 25 | { 26 | protected override async Task OnAfterRenderAsync(bool firstRender) 27 | { 28 | if (firstRender) 29 | { 30 | var authState = await AuthStateProvider.GetAuthenticationStateAsync(); 31 | 32 | if (authState.User == null) 33 | { 34 | await new Route(JSRuntime).RouteOutside("/Identity/Account/Login"); 35 | } 36 | CallRequestRefresh(); 37 | } 38 | 39 | await base.OnAfterRenderAsync(firstRender); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Shared/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @using System.Web; 2 | 3 | @inject NavigationManager Navigation 4 | @inject IJSRuntime JsRuntime 5 | 6 | @code { 7 | protected override void OnInitialized() 8 | { 9 | var returnUrl = HttpUtility.UrlEncode($"/{Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri))}"); 10 | JsRuntime.InvokeVoidAsync("location.replace", $"Identity/Account/Login?returnUrl={returnUrl}"); 11 | } 12 | } -------------------------------------------------------------------------------- /src/BlazorAdmin/Shared/Spinner.razor: -------------------------------------------------------------------------------- 1 |  2 | @inherits BlazorAdmin.Helpers.BlazorComponent 3 | 4 | @namespace BlazorAdmin.Shared 5 | 6 |
7 | Loading... 8 |
9 | 10 | -------------------------------------------------------------------------------- /src/BlazorAdmin/Shared/Toast.razor: -------------------------------------------------------------------------------- 1 | @inherits BlazorAdmin.Helpers.ToastComponent 2 | 3 | @namespace BlazorAdmin.Shared 4 | 5 |
6 |
7 | 8 |
9 |
10 |
@Heading
11 |

@Message

12 |
13 |
14 | -------------------------------------------------------------------------------- /src/BlazorAdmin/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Authorization; 4 | @using Microsoft.AspNetCore.Components.Authorization 5 | @using Microsoft.AspNetCore.Components.Forms 6 | @using Microsoft.AspNetCore.Components.Routing 7 | @using Microsoft.AspNetCore.Components.Web 8 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 9 | @using Microsoft.JSInterop 10 | @using Microsoft.Extensions.Logging 11 | @using BlazorAdmin 12 | @using BlazorAdmin.Shared 13 | @using BlazorAdmin.Services 14 | @using BlazorAdmin.JavaScript 15 | @using BlazorShared.Authorization 16 | @using BlazorShared.Interfaces 17 | @using BlazorInputFile 18 | @using BlazorShared.Models 19 | -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "https://localhost:5099/api/", 4 | "webBase": "https://localhost:44315/" 5 | }, 6 | "Logging": { 7 | "IncludeScopes": false, 8 | "LogLevel": { 9 | "Default": "Information", 10 | "Microsoft": "Warning", 11 | "System": "Warning" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "https://localhost:5099/api/", 4 | "webBase": "https://localhost:44315/" 5 | }, 6 | "Logging": { 7 | "IncludeScopes": false, 8 | "LogLevel": { 9 | "Default": "Information", 10 | "Microsoft": "Warning", 11 | "System": "Warning" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/BlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /src/BlazorShared/Attributes/EndpointAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorShared.Attributes; 4 | 5 | public class EndpointAttribute : Attribute 6 | { 7 | public string Name { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/BlazorShared/Authorization/ClaimValue.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared.Authorization; 2 | 3 | public class ClaimValue 4 | { 5 | public ClaimValue() 6 | { 7 | } 8 | 9 | public ClaimValue(string type, string value) 10 | { 11 | Type = type; 12 | Value = value; 13 | } 14 | 15 | public string Type { get; set; } 16 | public string Value { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/BlazorShared/Authorization/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared.Authorization; 2 | 3 | public static class Constants 4 | { 5 | public static class Roles 6 | { 7 | public const string ADMINISTRATORS = "Administrators"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/BlazorShared/Authorization/UserInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BlazorShared.Authorization; 4 | 5 | public class UserInfo 6 | { 7 | public static readonly UserInfo Anonymous = new UserInfo(); 8 | public bool IsAuthenticated { get; set; } 9 | public string NameClaimType { get; set; } 10 | public string RoleClaimType { get; set; } 11 | public string Token { get; set; } 12 | public IEnumerable Claims { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/BlazorShared/BaseUrlConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared; 2 | 3 | public class BaseUrlConfiguration 4 | { 5 | public const string CONFIG_NAME = "baseUrls"; 6 | 7 | public string ApiBase { get; set; } 8 | public string WebBase { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/BlazorShared/BlazorShared.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | BlazorShared 5 | BlazorShared 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/BlazorShared/Interfaces/ICatalogItemService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using BlazorShared.Models; 4 | 5 | namespace BlazorShared.Interfaces; 6 | 7 | public interface ICatalogItemService 8 | { 9 | Task Create(CreateCatalogItemRequest catalogItem); 10 | Task Edit(CatalogItem catalogItem); 11 | Task Delete(int id); 12 | Task GetById(int id); 13 | Task> ListPaged(int pageSize); 14 | Task> List(); 15 | } 16 | -------------------------------------------------------------------------------- /src/BlazorShared/Interfaces/ICatalogLookupDataService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using BlazorShared.Models; 4 | 5 | namespace BlazorShared.Interfaces; 6 | 7 | public interface ICatalogLookupDataService where TLookupData : LookupData 8 | { 9 | Task> List(); 10 | } 11 | -------------------------------------------------------------------------------- /src/BlazorShared/Interfaces/ILookupDataResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using BlazorShared.Models; 3 | 4 | namespace BlazorShared.Interfaces; 5 | 6 | public interface ILookupDataResponse where TLookupData : LookupData 7 | { 8 | List List { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/CatalogBrand.cs: -------------------------------------------------------------------------------- 1 | using BlazorShared.Attributes; 2 | 3 | namespace BlazorShared.Models; 4 | 5 | [Endpoint(Name = "catalog-brands")] 6 | public class CatalogBrand : LookupData 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/CatalogBrandResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | using BlazorShared.Interfaces; 4 | 5 | namespace BlazorShared.Models; 6 | 7 | public class CatalogBrandResponse : ILookupDataResponse 8 | { 9 | [JsonPropertyName("CatalogBrands")] 10 | public List List { get; set; } = new List(); 11 | } 12 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/CatalogType.cs: -------------------------------------------------------------------------------- 1 | using BlazorShared.Attributes; 2 | 3 | namespace BlazorShared.Models; 4 | 5 | [Endpoint(Name = "catalog-types")] 6 | public class CatalogType : LookupData 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/CatalogTypeResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | using BlazorShared.Interfaces; 4 | 5 | namespace BlazorShared.Models; 6 | 7 | public class CatalogTypeResponse : ILookupDataResponse 8 | { 9 | 10 | [JsonPropertyName("CatalogTypes")] 11 | public List List { get; set; } = new List(); 12 | } 13 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/CreateCatalogItemRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace BlazorShared.Models; 4 | 5 | public class CreateCatalogItemRequest 6 | { 7 | public int CatalogTypeId { get; set; } 8 | 9 | public int CatalogBrandId { get; set; } 10 | 11 | [Required(ErrorMessage = "The Name field is required")] 12 | public string Name { get; set; } = string.Empty; 13 | 14 | [Required(ErrorMessage = "The Description field is required")] 15 | public string Description { get; set; } = string.Empty; 16 | 17 | // decimal(18,2) 18 | [RegularExpression(@"^\d+(\.\d{0,2})*$", ErrorMessage = "The field Price must be a positive number with maximum two decimals.")] 19 | [Range(0.01, 1000)] 20 | [DataType(DataType.Currency)] 21 | public decimal Price { get; set; } = 0; 22 | 23 | public string PictureUri { get; set; } = string.Empty; 24 | public string PictureBase64 { get; set; } = string.Empty; 25 | public string PictureName { get; set; } = string.Empty; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/CreateCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared.Models; 2 | 3 | public class CreateCatalogItemResponse 4 | { 5 | public CatalogItem CatalogItem { get; set; } = new CatalogItem(); 6 | } 7 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/DeleteCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared.Models; 2 | 3 | public class DeleteCatalogItemResponse 4 | { 5 | public string Status { get; set; } = "Deleted"; 6 | } 7 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/EditCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared.Models; 2 | 3 | public class EditCatalogItemResult 4 | { 5 | public CatalogItem CatalogItem { get; set; } = new CatalogItem(); 6 | } 7 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/ErrorDetails.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace BlazorShared.Models; 4 | 5 | public class ErrorDetails 6 | { 7 | public int StatusCode { get; set; } 8 | public string Message { get; set; } 9 | public override string ToString() 10 | { 11 | return JsonSerializer.Serialize(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/LookupData.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorShared.Models; 2 | 3 | public abstract class LookupData 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/BlazorShared/Models/PagedCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BlazorShared.Models; 4 | 5 | public class PagedCatalogItemResponse 6 | { 7 | public List CatalogItems { get; set; } = new List(); 8 | public int PageCount { get; set; } = 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/CatalogContext.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities; 4 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 5 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 6 | 7 | namespace Microsoft.eShopWeb.Infrastructure.Data; 8 | 9 | public class CatalogContext : DbContext 10 | { 11 | #pragma warning disable CS8618 // Required by Entity Framework 12 | public CatalogContext(DbContextOptions options) : base(options) {} 13 | 14 | public DbSet Baskets { get; set; } 15 | public DbSet CatalogItems { get; set; } 16 | public DbSet CatalogBrands { get; set; } 17 | public DbSet CatalogTypes { get; set; } 18 | public DbSet Orders { get; set; } 19 | public DbSet OrderItems { get; set; } 20 | public DbSet BasketItems { get; set; } 21 | 22 | protected override void OnModelCreating(ModelBuilder builder) 23 | { 24 | base.OnModelCreating(builder); 25 | builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/BasketConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class BasketConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items)); 12 | navigation?.SetPropertyAccessMode(PropertyAccessMode.Field); 13 | 14 | builder.Property(b => b.BuyerId) 15 | .IsRequired() 16 | .HasMaxLength(256); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/BasketItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class BasketItemConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(bi => bi.UnitPrice) 12 | .IsRequired(true) 13 | .HasColumnType("decimal(18,2)"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/CatalogBrandConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class CatalogBrandConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(ci => ci.Id); 12 | 13 | builder.Property(ci => ci.Id) 14 | .UseHiLo("catalog_brand_hilo") 15 | .IsRequired(); 16 | 17 | builder.Property(cb => cb.Brand) 18 | .IsRequired() 19 | .HasMaxLength(100); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/CatalogItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class CatalogItemConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Catalog"); 12 | 13 | builder.Property(ci => ci.Id) 14 | .UseHiLo("catalog_hilo") 15 | .IsRequired(); 16 | 17 | builder.Property(ci => ci.Name) 18 | .IsRequired(true) 19 | .HasMaxLength(50); 20 | 21 | builder.Property(ci => ci.Price) 22 | .IsRequired(true) 23 | .HasColumnType("decimal(18,2)"); 24 | 25 | builder.Property(ci => ci.PictureUri) 26 | .IsRequired(false); 27 | 28 | builder.HasOne(ci => ci.CatalogBrand) 29 | .WithMany() 30 | .HasForeignKey(ci => ci.CatalogBrandId); 31 | 32 | builder.HasOne(ci => ci.CatalogType) 33 | .WithMany() 34 | .HasForeignKey(ci => ci.CatalogTypeId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/CatalogTypeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class CatalogTypeConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(ci => ci.Id); 12 | 13 | builder.Property(ci => ci.Id) 14 | .UseHiLo("catalog_type_hilo") 15 | .IsRequired(); 16 | 17 | builder.Property(cb => cb.Type) 18 | .IsRequired() 19 | .HasMaxLength(100); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/OrderConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class OrderConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems)); 12 | 13 | navigation?.SetPropertyAccessMode(PropertyAccessMode.Field); 14 | 15 | builder.Property(b => b.BuyerId) 16 | .IsRequired() 17 | .HasMaxLength(256); 18 | 19 | builder.OwnsOne(o => o.ShipToAddress, a => 20 | { 21 | a.WithOwner(); 22 | 23 | a.Property(a => a.ZipCode) 24 | .HasMaxLength(18) 25 | .IsRequired(); 26 | 27 | a.Property(a => a.Street) 28 | .HasMaxLength(180) 29 | .IsRequired(); 30 | 31 | a.Property(a => a.State) 32 | .HasMaxLength(60); 33 | 34 | a.Property(a => a.Country) 35 | .HasMaxLength(90) 36 | .IsRequired(); 37 | 38 | a.Property(a => a.City) 39 | .HasMaxLength(100) 40 | .IsRequired(); 41 | }); 42 | 43 | builder.Navigation(x => x.ShipToAddress).IsRequired(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Config/OrderItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Data.Config; 6 | 7 | public class OrderItemConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.OwnsOne(i => i.ItemOrdered, io => 12 | { 13 | io.WithOwner(); 14 | 15 | io.Property(cio => cio.ProductName) 16 | .HasMaxLength(50) 17 | .IsRequired(); 18 | }); 19 | 20 | builder.Property(oi => oi.UnitPrice) 21 | .IsRequired(true) 22 | .HasColumnType("decimal(18,2)"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/EfRepository.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification.EntityFrameworkCore; 2 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 3 | 4 | namespace Microsoft.eShopWeb.Infrastructure.Data; 5 | 6 | public class EfRepository : RepositoryBase, IReadRepository, IRepository where T : class, IAggregateRoot 7 | { 8 | public EfRepository(CatalogContext dbContext) : base(dbContext) 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/FileItem.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Infrastructure.Data; 2 | 3 | public class FileItem 4 | { 5 | public string? FileName { get; set; } 6 | public string? Url { get; set; } 7 | public long Size { get; set; } 8 | public string? Ext { get; set; } 9 | public string? Type { get; set; } 10 | public string? DataBase64 { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Infrastructure/Data/Queries/BasketQueryService.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 5 | 6 | namespace Microsoft.eShopWeb.Infrastructure.Data.Queries; 7 | 8 | public class BasketQueryService : IBasketQueryService 9 | { 10 | private readonly CatalogContext _dbContext; 11 | 12 | public BasketQueryService(CatalogContext dbContext) 13 | { 14 | _dbContext = dbContext; 15 | } 16 | 17 | /// 18 | /// This method performs the sum on the database rather than in memory 19 | /// 20 | /// 21 | /// 22 | public async Task CountTotalBasketItems(string username) 23 | { 24 | var totalItems = await _dbContext.Baskets 25 | .Where(basket => basket.BuyerId == username) 26 | .SelectMany(item => item.Items) 27 | .SumAsync(sum => sum.Quantity); 28 | 29 | return totalItems; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Infrastructure/Identity/AppIdentityDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | 5 | namespace Microsoft.eShopWeb.Infrastructure.Identity; 6 | 7 | public class AppIdentityDbContext : IdentityDbContext 8 | { 9 | public AppIdentityDbContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | protected override void OnModelCreating(ModelBuilder builder) 15 | { 16 | base.OnModelCreating(builder); 17 | // Customize the ASP.NET Identity model and override the defaults if needed. 18 | // For example, you can rename the ASP.NET Identity table names and more. 19 | // Add your customizations after calling base.OnModelCreating(builder); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Infrastructure/Identity/AppIdentityDbContextSeed.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.eShopWeb.ApplicationCore.Constants; 5 | 6 | namespace Microsoft.eShopWeb.Infrastructure.Identity; 7 | 8 | public class AppIdentityDbContextSeed 9 | { 10 | public static async Task SeedAsync(AppIdentityDbContext identityDbContext, UserManager userManager, RoleManager roleManager) 11 | { 12 | 13 | if (identityDbContext.Database.IsSqlServer()) 14 | { 15 | identityDbContext.Database.Migrate(); 16 | } 17 | 18 | await roleManager.CreateAsync(new IdentityRole(BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)); 19 | 20 | var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; 21 | await userManager.CreateAsync(defaultUser, AuthorizationConstants.DEFAULT_PASSWORD); 22 | 23 | string adminUserName = "admin@microsoft.com"; 24 | var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName }; 25 | await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD); 26 | adminUser = await userManager.FindByNameAsync(adminUserName); 27 | await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Identity/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace Microsoft.eShopWeb.Infrastructure.Identity; 4 | 5 | public class ApplicationUser : IdentityUser 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.eShopWeb.Infrastructure 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Infrastructure/Logging/LoggerAdapter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Microsoft.eShopWeb.Infrastructure.Logging; 5 | 6 | public class LoggerAdapter : IAppLogger 7 | { 8 | private readonly ILogger _logger; 9 | public LoggerAdapter(ILoggerFactory loggerFactory) 10 | { 11 | _logger = loggerFactory.CreateLogger(); 12 | } 13 | 14 | public void LogWarning(string message, params object[] args) 15 | { 16 | _logger.LogWarning(message, args); 17 | } 18 | 19 | public void LogInformation(string message, params object[] args) 20 | { 21 | _logger.LogInformation(message, args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Infrastructure/Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 3 | 4 | namespace Microsoft.eShopWeb.Infrastructure.Services; 5 | 6 | // This class is used by the application to send email for account confirmation and password reset. 7 | // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 8 | public class EmailSender : IEmailSender 9 | { 10 | public Task SendEmailAsync(string email, string subject, string message) 11 | { 12 | // TODO: Wire this up to actual email sending logic via SendGrid, local SMTP, etc. 13 | return Task.CompletedTask; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; 2 | 3 | public class AuthenticateRequest : BaseRequest 4 | { 5 | public string Username { get; set; } 6 | public string Password { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; 4 | 5 | public class AuthenticateResponse : BaseResponse 6 | { 7 | public AuthenticateResponse(Guid correlationId) : base(correlationId) 8 | { 9 | } 10 | 11 | public AuthenticateResponse() 12 | { 13 | } 14 | public bool Result { get; set; } = false; 15 | public string Token { get; set; } = string.Empty; 16 | public string Username { get; set; } = string.Empty; 17 | public bool IsLockedOut { get; set; } = false; 18 | public bool IsNotAllowed { get; set; } = false; 19 | public bool RequiresTwoFactor { get; set; } = false; 20 | } 21 | -------------------------------------------------------------------------------- /src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; 7 | 8 | public class ClaimValue 9 | { 10 | public ClaimValue() 11 | { 12 | } 13 | 14 | public ClaimValue(string type, string value) 15 | { 16 | Type = type; 17 | Value = value; 18 | } 19 | 20 | public string Type { get; set; } 21 | public string Value { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; 7 | 8 | public class UserInfo 9 | { 10 | public static readonly UserInfo Anonymous = new UserInfo(); 11 | public bool IsAuthenticated { get; set; } 12 | public string NameClaimType { get; set; } 13 | public string RoleClaimType { get; set; } 14 | public IEnumerable Claims { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/PublicApi/BaseMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi; 4 | 5 | /// 6 | /// Base class used by API requests 7 | /// 8 | public abstract class BaseMessage 9 | { 10 | /// 11 | /// Unique Identifier used by logging 12 | /// 13 | protected Guid _correlationId = Guid.NewGuid(); 14 | public Guid CorrelationId() => _correlationId; 15 | } 16 | -------------------------------------------------------------------------------- /src/PublicApi/BaseRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi; 2 | 3 | /// 4 | /// Base class used by API requests 5 | /// 6 | public abstract class BaseRequest : BaseMessage 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/PublicApi/BaseResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi; 4 | 5 | /// 6 | /// Base class used by API responses 7 | /// 8 | public abstract class BaseResponse : BaseMessage 9 | { 10 | public BaseResponse(Guid correlationId) : base() 11 | { 12 | base._correlationId = correlationId; 13 | } 14 | 15 | public BaseResponse() 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogBrandEndpoints/CatalogBrandDto.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; 2 | 3 | public class CatalogBrandDto 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.ListCatalogBrandsResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; 5 | 6 | public class ListCatalogBrandsResponse : BaseResponse 7 | { 8 | public ListCatalogBrandsResponse(Guid correlationId) : base(correlationId) 9 | { 10 | } 11 | 12 | public ListCatalogBrandsResponse() 13 | { 14 | } 15 | 16 | public List CatalogBrands { get; set; } = new List(); 17 | } 18 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CatalogItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 2 | 3 | public class CatalogItemDto 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public string Description { get; set; } 8 | public decimal Price { get; set; } 9 | public string PictureUri { get; set; } 10 | public int CatalogTypeId { get; set; } 11 | public int CatalogBrandId { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 2 | 3 | public class GetByIdCatalogItemRequest : BaseRequest 4 | { 5 | public int CatalogItemId { get; init; } 6 | 7 | public GetByIdCatalogItemRequest(int catalogItemId) 8 | { 9 | CatalogItemId = catalogItemId; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 4 | 5 | public class GetByIdCatalogItemResponse : BaseResponse 6 | { 7 | public GetByIdCatalogItemResponse(Guid correlationId) : base(correlationId) 8 | { 9 | } 10 | 11 | public GetByIdCatalogItemResponse() 12 | { 13 | } 14 | 15 | public CatalogItemDto CatalogItem { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 2 | 3 | public class ListPagedCatalogItemRequest : BaseRequest 4 | { 5 | public int? PageSize { get; init; } 6 | public int? PageIndex { get; init; } 7 | public int? CatalogBrandId { get; init; } 8 | public int? CatalogTypeId { get; init; } 9 | 10 | public ListPagedCatalogItemRequest(int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId) 11 | { 12 | PageSize = pageSize ?? 0; 13 | PageIndex = pageIndex ?? 0; 14 | CatalogBrandId = catalogBrandId; 15 | CatalogTypeId = catalogTypeId; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 5 | 6 | public class ListPagedCatalogItemResponse : BaseResponse 7 | { 8 | public ListPagedCatalogItemResponse(Guid correlationId) : base(correlationId) 9 | { 10 | } 11 | 12 | public ListPagedCatalogItemResponse() 13 | { 14 | } 15 | 16 | public List CatalogItems { get; set; } = new List(); 17 | public int PageCount { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 2 | 3 | public class CreateCatalogItemRequest : BaseRequest 4 | { 5 | public int CatalogBrandId { get; set; } 6 | public int CatalogTypeId { get; set; } 7 | public string Description { get; set; } 8 | public string Name { get; set; } 9 | public string PictureUri { get; set; } 10 | public string PictureBase64 { get; set; } 11 | public string PictureName { get; set; } 12 | public decimal Price { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 4 | 5 | public class CreateCatalogItemResponse : BaseResponse 6 | { 7 | public CreateCatalogItemResponse(Guid correlationId) : base(correlationId) 8 | { 9 | } 10 | 11 | public CreateCatalogItemResponse() 12 | { 13 | } 14 | 15 | public CatalogItemDto CatalogItem { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 2 | 3 | public class DeleteCatalogItemRequest : BaseRequest 4 | { 5 | public int CatalogItemId { get; init; } 6 | 7 | public DeleteCatalogItemRequest(int catalogItemId) 8 | { 9 | CatalogItemId = catalogItemId; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 4 | 5 | public class DeleteCatalogItemResponse : BaseResponse 6 | { 7 | public DeleteCatalogItemResponse(Guid correlationId) : base(correlationId) 8 | { 9 | } 10 | 11 | public DeleteCatalogItemResponse() 12 | { 13 | } 14 | 15 | public string Status { get; set; } = "Deleted"; 16 | } 17 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 4 | 5 | public class UpdateCatalogItemRequest : BaseRequest 6 | { 7 | [Range(1, 10000)] 8 | public int Id { get; set; } 9 | [Range(1, 10000)] 10 | public int CatalogBrandId { get; set; } 11 | [Range(1, 10000)] 12 | public int CatalogTypeId { get; set; } 13 | [Required] 14 | public string Description { get; set; } 15 | [Required] 16 | public string Name { get; set; } 17 | public string PictureBase64 { get; set; } 18 | public string PictureUri { get; set; } 19 | public string PictureName { get; set; } 20 | [Range(0.01, 10000)] 21 | public decimal Price { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 4 | 5 | public class UpdateCatalogItemResponse : BaseResponse 6 | { 7 | public UpdateCatalogItemResponse(Guid correlationId) : base(correlationId) 8 | { 9 | } 10 | 11 | public UpdateCatalogItemResponse() 12 | { 13 | } 14 | 15 | public CatalogItemDto CatalogItem { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogTypeEndpoints/CatalogTypeDto.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; 2 | 3 | public class CatalogTypeDto 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.ListCatalogTypesResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; 5 | 6 | public class ListCatalogTypesResponse : BaseResponse 7 | { 8 | public ListCatalogTypesResponse(Guid correlationId) : base(correlationId) 9 | { 10 | } 11 | 12 | public ListCatalogTypesResponse() 13 | { 14 | } 15 | 16 | public List CatalogTypes { get; set; } = new List(); 17 | } 18 | -------------------------------------------------------------------------------- /src/PublicApi/CustomSchemaFilters.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | using Swashbuckle.AspNetCore.SwaggerGen; 3 | 4 | namespace Microsoft.eShopWeb.PublicApi; 5 | 6 | public class CustomSchemaFilters : ISchemaFilter 7 | { 8 | public void Apply(OpenApiSchema schema, SchemaFilterContext context) 9 | { 10 | var excludeProperties = new[] { "CorrelationId" }; 11 | 12 | foreach (var prop in excludeProperties) 13 | if (schema.Properties.ContainsKey(prop)) 14 | schema.Properties.Remove(prop); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/PublicApi/ImageValidators.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Microsoft.eShopWeb.PublicApi; 5 | 6 | public static class ImageValidators 7 | { 8 | private const int ImageMaximumBytes = 512000; 9 | 10 | public static bool IsValidImage(this byte[] postedFile, string fileName) 11 | { 12 | return postedFile != null && postedFile.Length > 0 && postedFile.Length <= ImageMaximumBytes && IsExtensionValid(fileName); 13 | } 14 | 15 | private static bool IsExtensionValid(string fileName) 16 | { 17 | var extension = Path.GetExtension(fileName); 18 | 19 | return string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) || 20 | string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase) || 21 | string.Equals(extension, ".gif", StringComparison.OrdinalIgnoreCase) || 22 | string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PublicApi/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities; 3 | using Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; 4 | using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 5 | using Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; 6 | 7 | namespace Microsoft.eShopWeb.PublicApi; 8 | 9 | public class MappingProfile : Profile 10 | { 11 | public MappingProfile() 12 | { 13 | CreateMap(); 14 | CreateMap() 15 | .ForMember(dto => dto.Name, options => options.MapFrom(src => src.Type)); 16 | CreateMap() 17 | .ForMember(dto => dto.Name, options => options.MapFrom(src => src.Brand)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PublicApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "IIS Express": { 4 | "commandName": "IISExpress", 5 | "launchBrowser": true, 6 | "launchUrl": "swagger", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | }, 11 | "PublicApi": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "launchUrl": "swagger", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | }, 18 | "applicationUrl": "https://localhost:5099;http://localhost:5098" 19 | }, 20 | "WSL": { 21 | "commandName": "WSL2", 22 | "launchBrowser": true, 23 | "launchUrl": "https://localhost:5099/swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development", 26 | "ASPNETCORE_URLS": "https://localhost:5099;http://localhost:5098" 27 | }, 28 | "distributionName": "" 29 | }, 30 | "Docker": { 31 | "commandName": "Docker", 32 | "launchBrowser": true, 33 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 34 | "publishAllPorts": true, 35 | "useSSL": true 36 | } 37 | }, 38 | "$schema": "http://json.schemastore.org/launchsettings.json", 39 | "iisSettings": { 40 | "windowsAuthentication": false, 41 | "anonymousAuthentication": true, 42 | "iisExpress": { 43 | "applicationUrl": "http://localhost:52023", 44 | "sslPort": 44339 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/PublicApi/README.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | This folder demonstrates how to configure API endpoints as individual classes. You can compare it to the traditional controller-based approach found in /Web/Controllers/Api. 4 | 5 | -------------------------------------------------------------------------------- /src/PublicApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "https://localhost:5099/api/", 4 | "webBase": "https://localhost:5001/" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft": "Warning", 10 | "Microsoft.Hosting.Lifetime": "Information" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/PublicApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "https://localhost:5099/api/", 4 | "webBase": "https://localhost:5001/" 5 | }, 6 | "ConnectionStrings": { 7 | "CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;", 8 | "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;" 9 | }, 10 | "CatalogBaseUrl": "", 11 | "Logging": { 12 | "IncludeScopes": false, 13 | "LogLevel": { 14 | "Default": "Warning", 15 | "Microsoft": "Warning", 16 | "System": "Warning" 17 | }, 18 | "AllowedHosts": "*" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Web/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "7.0.1", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Web/Areas/Identity/IdentityHostingStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | 3 | [assembly: HostingStartup(typeof(Microsoft.eShopWeb.Web.Areas.Identity.IdentityHostingStartup))] 4 | namespace Microsoft.eShopWeb.Web.Areas.Identity; 5 | 6 | public class IdentityHostingStartup : IHostingStartup 7 | { 8 | public void Configure(IWebHostBuilder builder) 9 | { 10 | builder.ConfigureServices((context, services) => 11 | { 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailModel 3 | @{ 4 | ViewData["Title"] = "Confirm email"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

10 | Thank you for confirming your email. 11 |

12 |
13 | -------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.eShopWeb.Infrastructure.Identity; 10 | 11 | namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account; 12 | 13 | [AllowAnonymous] 14 | public class ConfirmEmailModel : PageModel 15 | { 16 | private readonly UserManager _userManager; 17 | 18 | public ConfirmEmailModel(UserManager userManager) 19 | { 20 | _userManager = userManager; 21 | } 22 | 23 | public async Task OnGetAsync(string userId, string code) 24 | { 25 | if (userId == null || code == null) 26 | { 27 | return RedirectToPage("/Index"); 28 | } 29 | 30 | var user = await _userManager.FindByIdAsync(userId); 31 | if (user == null) 32 | { 33 | return NotFound($"Unable to load user with ID '{userId}'."); 34 | } 35 | 36 | var result = await _userManager.ConfirmEmailAsync(user, code); 37 | if (!result.Succeeded) 38 | { 39 | throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':"); 40 | } 41 | 42 | return Page(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LogoutModel 3 | @{ 4 | ViewData["Title"] = "Log out"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

You have successfully logged out of the application.

10 |
-------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account -------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using Microsoft.eShopWeb.Web.Areas.Identity 3 | @using Microsoft.eShopWeb.Infrastructure.Identity 4 | @namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | -------------------------------------------------------------------------------- /src/Web/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /src/Web/Configuration/ConfigureCoreServices.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 2 | using Microsoft.eShopWeb.ApplicationCore.Services; 3 | using Microsoft.eShopWeb.Infrastructure.Data; 4 | using Microsoft.eShopWeb.Infrastructure.Data.Queries; 5 | using Microsoft.eShopWeb.Infrastructure.Logging; 6 | using Microsoft.eShopWeb.Infrastructure.Services; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace Microsoft.eShopWeb.Web.Configuration; 11 | 12 | public static class ConfigureCoreServices 13 | { 14 | public static IServiceCollection AddCoreServices(this IServiceCollection services, 15 | IConfiguration configuration) 16 | { 17 | services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); 18 | services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); 19 | 20 | services.AddScoped(); 21 | services.AddScoped(); 22 | services.AddScoped(); 23 | services.AddSingleton(new UriComposer(configuration.Get())); 24 | services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); 25 | services.AddTransient(); 26 | 27 | return services; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Web/Configuration/ConfigureWebServices.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.eShopWeb.Web.Interfaces; 3 | using Microsoft.eShopWeb.Web.Services; 4 | 5 | namespace Microsoft.eShopWeb.Web.Configuration; 6 | 7 | public static class ConfigureWebServices 8 | { 9 | public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration) 10 | { 11 | services.AddMediatR(typeof(BasketViewModelService).Assembly); 12 | services.AddScoped(); 13 | services.AddScoped(); 14 | services.AddScoped(); 15 | services.Configure(configuration); 16 | services.AddScoped(); 17 | 18 | return services; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Web/Configuration/RevokeAuthenticationEvents.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authentication; 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Microsoft.eShopWeb.Web.Configuration; 10 | 11 | //TODO : replace IMemoryCache with a distributed cache if you are in multi-host scenario 12 | public class RevokeAuthenticationEvents : CookieAuthenticationEvents 13 | { 14 | private readonly IMemoryCache _cache; 15 | private readonly ILogger _logger; 16 | 17 | public RevokeAuthenticationEvents(IMemoryCache cache, ILogger logger) 18 | { 19 | _cache = cache; 20 | _logger = logger; 21 | } 22 | 23 | public override async Task ValidatePrincipal(CookieValidatePrincipalContext context) 24 | { 25 | var userId = context.Principal?.Claims.First(c => c.Type == ClaimTypes.Name); 26 | var identityKey = context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName]; 27 | 28 | if (_cache.TryGetValue($"{userId?.Value}:{identityKey}", out var revokeKeys)) 29 | { 30 | _logger.LogDebug($"Access has been revoked for: {userId?.Value}."); 31 | context.RejectPrincipal(); 32 | await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Web/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web; 2 | 3 | public static class Constants 4 | { 5 | public const string BASKET_COOKIENAME = "eShop"; 6 | public const int ITEMS_PER_PAGE = 10; 7 | public const string DEFAULT_USERNAME = "Guest"; 8 | public const string BASKET_ID = "BasketId"; 9 | } 10 | -------------------------------------------------------------------------------- /src/Web/Controllers/Api/BaseApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Microsoft.eShopWeb.Web.Controllers.Api; 4 | 5 | // No longer used - shown for reference only if using full controllers instead of Endpoints for APIs 6 | [Route("api/[controller]/[action]")] 7 | [ApiController] 8 | public class BaseApiController : ControllerBase 9 | { } 10 | -------------------------------------------------------------------------------- /src/Web/Controllers/OrderController.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.eShopWeb.Web.Features.MyOrders; 6 | using Microsoft.eShopWeb.Web.Features.OrderDetails; 7 | 8 | namespace Microsoft.eShopWeb.Web.Controllers; 9 | 10 | [ApiExplorerSettings(IgnoreApi = true)] 11 | [Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages 12 | [Route("[controller]/[action]")] 13 | public class OrderController : Controller 14 | { 15 | private readonly IMediator _mediator; 16 | 17 | public OrderController(IMediator mediator) 18 | { 19 | _mediator = mediator; 20 | } 21 | 22 | [HttpGet] 23 | public async Task MyOrders() 24 | { 25 | Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name)); 26 | var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name)); 27 | 28 | return View(viewModel); 29 | } 30 | 31 | [HttpGet("{orderId}")] 32 | public async Task Detail(int orderId) 33 | { 34 | Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name)); 35 | var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId)); 36 | 37 | if (viewModel == null) 38 | { 39 | return BadRequest("No such order found for this user."); 40 | } 41 | 42 | return View(viewModel); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Web/Extensions/CacheHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.Web.Extensions; 4 | 5 | public static class CacheHelpers 6 | { 7 | public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromSeconds(30); 8 | private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}"; 9 | 10 | public static string GenerateCatalogItemCacheKey(int pageIndex, int itemsPage, int? brandId, int? typeId) 11 | { 12 | return string.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandId, typeId); 13 | } 14 | 15 | public static string GenerateBrandsCacheKey() 16 | { 17 | return "brands"; 18 | } 19 | 20 | public static string GenerateTypesCacheKey() 21 | { 22 | return "types"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/Extensions/EmailSenderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Threading.Tasks; 3 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | 5 | namespace Microsoft.eShopWeb.Web.Services; 6 | 7 | public static class EmailSenderExtensions 8 | { 9 | public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) 10 | { 11 | return emailSender.SendEmailAsync(email, "Confirm your email", 12 | $"Please confirm your account by clicking this link: link"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Web/Extensions/UrlHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | public static class UrlHelperExtensions 4 | { 5 | public static string? EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) 6 | { 7 | return urlHelper.Action( 8 | action: "GET", 9 | controller: "ConfirmEmail", 10 | values: new { userId, code }, 11 | protocol: scheme); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Features/MyOrders/GetMyOrders.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MediatR; 3 | using Microsoft.eShopWeb.Web.ViewModels; 4 | 5 | namespace Microsoft.eShopWeb.Web.Features.MyOrders; 6 | 7 | public class GetMyOrders : IRequest> 8 | { 9 | public string UserName { get; set; } 10 | 11 | public GetMyOrders(string userName) 12 | { 13 | UserName = userName; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Web/Features/OrderDetails/GetOrderDetails.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.eShopWeb.Web.ViewModels; 3 | 4 | namespace Microsoft.eShopWeb.Web.Features.OrderDetails; 5 | 6 | public class GetOrderDetails : IRequest 7 | { 8 | public string UserName { get; set; } 9 | public int OrderId { get; set; } 10 | 11 | public GetOrderDetails(string userName, int orderId) 12 | { 13 | UserName = userName; 14 | OrderId = orderId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Web/HealthChecks/ApiHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using BlazorShared; 5 | using Microsoft.Extensions.Diagnostics.HealthChecks; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Microsoft.eShopWeb.Web.HealthChecks; 9 | 10 | public class ApiHealthCheck : IHealthCheck 11 | { 12 | private readonly BaseUrlConfiguration _baseUrlConfiguration; 13 | 14 | public ApiHealthCheck(IOptions baseUrlConfiguration) 15 | { 16 | _baseUrlConfiguration = baseUrlConfiguration.Value; 17 | } 18 | 19 | public async Task CheckHealthAsync( 20 | HealthCheckContext context, 21 | CancellationToken cancellationToken = default(CancellationToken)) 22 | { 23 | string myUrl = _baseUrlConfiguration.ApiBase + "catalog-items"; 24 | var client = new HttpClient(); 25 | var response = await client.GetAsync(myUrl); 26 | var pageContents = await response.Content.ReadAsStringAsync(); 27 | if (pageContents.Contains(".NET Bot Black Sweatshirt")) 28 | { 29 | return HealthCheckResult.Healthy("The check indicates a healthy result."); 30 | } 31 | 32 | return HealthCheckResult.Unhealthy("The check indicates an unhealthy result."); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Web/HealthChecks/HomePageHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Diagnostics.HealthChecks; 6 | 7 | namespace Microsoft.eShopWeb.Web.HealthChecks; 8 | 9 | public class HomePageHealthCheck : IHealthCheck 10 | { 11 | private readonly IHttpContextAccessor _httpContextAccessor; 12 | 13 | public HomePageHealthCheck(IHttpContextAccessor httpContextAccessor) 14 | { 15 | _httpContextAccessor = httpContextAccessor; 16 | } 17 | 18 | public async Task CheckHealthAsync( 19 | HealthCheckContext context, 20 | CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | var request = _httpContextAccessor.HttpContext?.Request; 23 | string myUrl = request?.Scheme + "://" + request?.Host.ToString(); 24 | 25 | var client = new HttpClient(); 26 | var response = await client.GetAsync(myUrl); 27 | var pageContents = await response.Content.ReadAsStringAsync(); 28 | if (pageContents.Contains(".NET Bot Black Sweatshirt")) 29 | { 30 | return HealthCheckResult.Healthy("The check indicates a healthy result."); 31 | } 32 | 33 | return HealthCheckResult.Unhealthy("The check indicates an unhealthy result."); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Web/Interfaces/IBasketViewModelService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 2 | using Microsoft.eShopWeb.Web.Pages.Basket; 3 | 4 | namespace Microsoft.eShopWeb.Web.Interfaces; 5 | 6 | public interface IBasketViewModelService 7 | { 8 | Task GetOrCreateBasketForUser(string userName); 9 | Task CountTotalBasketItems(string username); 10 | Task Map(Basket basket); 11 | } 12 | -------------------------------------------------------------------------------- /src/Web/Interfaces/ICatalogItemViewModelService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.eShopWeb.Web.ViewModels; 3 | 4 | namespace Microsoft.eShopWeb.Web.Interfaces; 5 | 6 | public interface ICatalogItemViewModelService 7 | { 8 | Task UpdateCatalogItem(CatalogItemViewModel viewModel); 9 | } 10 | -------------------------------------------------------------------------------- /src/Web/Interfaces/ICatalogViewModelService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc.Rendering; 4 | using Microsoft.eShopWeb.Web.ViewModels; 5 | 6 | namespace Microsoft.eShopWeb.Web.Services; 7 | 8 | public interface ICatalogViewModelService 9 | { 10 | Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId); 11 | Task> GetBrands(); 12 | Task> GetTypes(); 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Pages/Admin/EditCatalogItem.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.eShopWeb.ApplicationCore.Constants; 6 | using Microsoft.eShopWeb.Web.Interfaces; 7 | using Microsoft.eShopWeb.Web.ViewModels; 8 | 9 | namespace Microsoft.eShopWeb.Web.Pages.Admin; 10 | 11 | [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)] 12 | public class EditCatalogItemModel : PageModel 13 | { 14 | private readonly ICatalogItemViewModelService _catalogItemViewModelService; 15 | 16 | public EditCatalogItemModel(ICatalogItemViewModelService catalogItemViewModelService) 17 | { 18 | _catalogItemViewModelService = catalogItemViewModelService; 19 | } 20 | 21 | [BindProperty] 22 | public CatalogItemViewModel CatalogModel { get; set; } = new CatalogItemViewModel(); 23 | 24 | public void OnGet(CatalogItemViewModel catalogModel) 25 | { 26 | CatalogModel = catalogModel; 27 | } 28 | 29 | public async Task OnPostAsync() 30 | { 31 | if (ModelState.IsValid) 32 | { 33 | await _catalogItemViewModelService.UpdateCatalogItem(CatalogModel); 34 | } 35 | 36 | return RedirectToPage("/Admin/Index"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Web/Pages/Admin/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.eShopWeb.ApplicationCore.Constants; 5 | using Microsoft.eShopWeb.Web.Extensions; 6 | using Microsoft.eShopWeb.Web.Services; 7 | using Microsoft.eShopWeb.Web.ViewModels; 8 | using Microsoft.Extensions.Caching.Memory; 9 | 10 | namespace Microsoft.eShopWeb.Web.Pages.Admin; 11 | 12 | [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)] 13 | public class IndexModel : PageModel 14 | { 15 | public IndexModel() 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Web/Pages/Basket/BasketItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.Pages.Basket; 4 | 5 | public class BasketItemViewModel 6 | { 7 | public int Id { get; set; } 8 | public int CatalogItemId { get; set; } 9 | public string? ProductName { get; set; } 10 | public decimal UnitPrice { get; set; } 11 | public decimal OldUnitPrice { get; set; } 12 | 13 | [Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")] 14 | public int Quantity { get; set; } 15 | 16 | public string? PictureUrl { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/Pages/Basket/BasketViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.Pages.Basket; 2 | 3 | public class BasketViewModel 4 | { 5 | public int Id { get; set; } 6 | public List Items { get; set; } = new List(); 7 | public string? BuyerId { get; set; } 8 | 9 | public decimal Total() 10 | { 11 | return Math.Round(Items.Sum(x => x.UnitPrice * x.Quantity), 2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Pages/Basket/Success.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model SuccessModel 3 | @{ 4 | ViewData["Title"] = "Checkout Complete"; 5 | } 6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |

Thanks for your Order!

15 | 16 | Continue Shopping... 17 |
-------------------------------------------------------------------------------- /src/Web/Pages/Basket/Success.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace Microsoft.eShopWeb.Web.Pages.Basket; 10 | 11 | [Authorize] 12 | public class SuccessModel : PageModel 13 | { 14 | public void OnGet() 15 | { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Web/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

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

27 | -------------------------------------------------------------------------------- /src/Web/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Microsoft.eShopWeb.Web.Pages; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | public class ErrorModel : PageModel 9 | { 10 | public string? RequestId { get; set; } 11 | 12 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 13 | 14 | public void OnGet() 15 | { 16 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Web/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | using Microsoft.eShopWeb.Web.Services; 3 | using Microsoft.eShopWeb.Web.ViewModels; 4 | 5 | namespace Microsoft.eShopWeb.Web.Pages; 6 | 7 | public class IndexModel : PageModel 8 | { 9 | private readonly ICatalogViewModelService _catalogViewModelService; 10 | 11 | public IndexModel(ICatalogViewModelService catalogViewModelService) 12 | { 13 | _catalogViewModelService = catalogViewModelService; 14 | } 15 | 16 | public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); 17 | 18 | public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) 19 | { 20 | CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Web/Pages/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PrivacyModel 3 | @{ 4 | ViewData["Title"] = "Privacy Policy"; 5 | } 6 |

@ViewData["Title"]

7 | 8 |

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

9 | -------------------------------------------------------------------------------- /src/Web/Pages/Privacy.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace Microsoft.eShopWeb.Web.Pages; 4 | 5 | public class PrivacyModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Web/Pages/Shared/Components/BasketComponent/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model BasketComponentViewModel 2 | @{ 3 | ViewData["Title"] = "My Basket"; 4 | } 5 | 7 |
8 | 9 |
10 |
11 | @Model.ItemsCount 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/Web/Pages/Shared/_editCatalog.cshtml: -------------------------------------------------------------------------------- 1 | @model CatalogItemViewModel 2 | 3 |
4 |
5 | 6 |
7 | @Model.Name 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/Web/Pages/Shared/_product.cshtml: -------------------------------------------------------------------------------- 1 | @model CatalogItemViewModel 2 | 3 |
4 | 5 | 6 |
7 | @Model.Name 8 |
9 |
10 | @Model.Price.ToString("N2") 11 |
12 | 13 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /src/Web/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.eShopWeb.Web 2 | @using Microsoft.eShopWeb.Web.ViewModels 3 | @using Microsoft.eShopWeb.Web.ViewModels.Account 4 | @using Microsoft.eShopWeb.Web.ViewModels.Manage 5 | @using Microsoft.eShopWeb.Web.Pages 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.eShopWeb.Infrastructure.Identity 8 | @namespace Microsoft.eShopWeb.Web.Pages 9 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 10 | -------------------------------------------------------------------------------- /src/Web/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17469", 7 | "sslPort": 44315 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | }, 18 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 19 | }, 20 | "Web": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 28 | }, 29 | "Web - PROD": { 30 | "commandName": "Project", 31 | "launchBrowser": true, 32 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Production" 35 | }, 36 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Web/Services/CatalogItemViewModelService.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities; 3 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | using Microsoft.eShopWeb.Web.Interfaces; 5 | using Microsoft.eShopWeb.Web.ViewModels; 6 | 7 | namespace Microsoft.eShopWeb.Web.Services; 8 | 9 | public class CatalogItemViewModelService : ICatalogItemViewModelService 10 | { 11 | private readonly IRepository _catalogItemRepository; 12 | 13 | public CatalogItemViewModelService(IRepository catalogItemRepository) 14 | { 15 | _catalogItemRepository = catalogItemRepository; 16 | } 17 | 18 | public async Task UpdateCatalogItem(CatalogItemViewModel viewModel) 19 | { 20 | var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id); 21 | 22 | Guard.Against.Null(existingCatalogItem, nameof(existingCatalogItem)); 23 | 24 | CatalogItem.CatalogItemDetails details = new(viewModel.Name, existingCatalogItem.Description, viewModel.Price); 25 | existingCatalogItem.UpdateDetails(details); 26 | await _catalogItemRepository.UpdateAsync(existingCatalogItem); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Web/SlugifyParameterTransformer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.AspNetCore.Routing; 3 | 4 | namespace Microsoft.eShopWeb.Web; 5 | 6 | public class SlugifyParameterTransformer : IOutboundParameterTransformer 7 | { 8 | public string? TransformOutbound(object? value) 9 | { 10 | if (value == null) { return null; } 11 | string? str = value.ToString(); 12 | if (string.IsNullOrEmpty(str)) { return null; } 13 | 14 | // Slugify value 15 | return Regex.Replace(str, "([a-z])([A-Z])", "$1-$2").ToLower(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Account; 4 | 5 | public class LoginViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string? Email { get; set; } 10 | 11 | [Required] 12 | [DataType(DataType.Password)] 13 | public string? Password { get; set; } 14 | 15 | [Display(Name = "Remember me?")] 16 | public bool RememberMe { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Account/LoginWith2faViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Account; 4 | 5 | public class LoginWith2faViewModel 6 | { 7 | [Required] 8 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 9 | [DataType(DataType.Text)] 10 | [Display(Name = "Authenticator code")] 11 | public string? TwoFactorCode { get; set; } 12 | 13 | [Display(Name = "Remember this machine")] 14 | public bool RememberMachine { get; set; } 15 | 16 | public bool RememberMe { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Account/RegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Account; 4 | 5 | public class RegisterViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | [Display(Name = "Email")] 10 | public string? Email { get; set; } 11 | 12 | [Required] 13 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 14 | [DataType(DataType.Password)] 15 | [Display(Name = "Password")] 16 | public string? Password { get; set; } 17 | 18 | [DataType(DataType.Password)] 19 | [Display(Name = "Confirm password")] 20 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 21 | public string? ConfirmPassword { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Account/ResetPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Account; 4 | 5 | public class ResetPasswordViewModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string? Email { get; set; } 10 | 11 | [Required] 12 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 13 | [DataType(DataType.Password)] 14 | public string? Password { get; set; } 15 | 16 | [DataType(DataType.Password)] 17 | [Display(Name = "Confirm password")] 18 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 19 | public string? ConfirmPassword { get; set; } 20 | 21 | public string? Code { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Web/ViewModels/BasketComponentViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels; 2 | 3 | public class BasketComponentViewModel 4 | { 5 | public int ItemsCount { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Web/ViewModels/CatalogIndexViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Mvc.Rendering; 3 | 4 | namespace Microsoft.eShopWeb.Web.ViewModels; 5 | 6 | public class CatalogIndexViewModel 7 | { 8 | public List? CatalogItems { get; set; } 9 | public List? Brands { get; set; } 10 | public List? Types { get; set; } 11 | public int? BrandFilterApplied { get; set; } 12 | public int? TypesFilterApplied { get; set; } 13 | public PaginationInfoViewModel? PaginationInfo { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Web/ViewModels/CatalogItemViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels; 2 | 3 | public class CatalogItemViewModel 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | public string? PictureUri { get; set; } 8 | public decimal Price { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Web/ViewModels/File/FileViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels.File; 2 | 3 | public class FileViewModel 4 | { 5 | public string? FileName { get; set; } 6 | public string? Url { get; set; } 7 | public string? DataBase64 { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/ChangePasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 4 | 5 | public class ChangePasswordViewModel 6 | { 7 | [Required] 8 | [DataType(DataType.Password)] 9 | [Display(Name = "Current password")] 10 | public string? OldPassword { get; set; } 11 | 12 | [Required] 13 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 14 | [DataType(DataType.Password)] 15 | [Display(Name = "New password")] 16 | public string? NewPassword { get; set; } 17 | 18 | [DataType(DataType.Password)] 19 | [Display(Name = "Confirm new password")] 20 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 21 | public string? ConfirmPassword { get; set; } 22 | 23 | public string? StatusMessage { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/EnableAuthenticatorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | using Microsoft.AspNetCore.Mvc.ModelBinding; 4 | 5 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 6 | 7 | public class EnableAuthenticatorViewModel 8 | { 9 | [Required] 10 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 11 | [DataType(DataType.Text)] 12 | [Display(Name = "Verification Code")] 13 | public string? Code { get; set; } 14 | 15 | [BindNever] 16 | public string? SharedKey { get; set; } 17 | 18 | [BindNever] 19 | public string? AuthenticatorUri { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/ExternalLoginsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Identity; 4 | 5 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 6 | 7 | public class ExternalLoginsViewModel 8 | { 9 | public IList? CurrentLogins { get; set; } 10 | public IList? OtherLogins { get; set; } 11 | public bool ShowRemoveButton { get; set; } 12 | public string? StatusMessage { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 4 | 5 | public class IndexViewModel 6 | { 7 | public string? Username { get; set; } 8 | 9 | public bool IsEmailConfirmed { get; set; } 10 | 11 | [Required] 12 | [EmailAddress] 13 | public string? Email { get; set; } 14 | 15 | [Phone] 16 | [Display(Name = "Phone number")] 17 | public string? PhoneNumber { get; set; } 18 | 19 | public string? StatusMessage { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/RemoveLoginViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 2 | 3 | public class RemoveLoginViewModel 4 | { 5 | public string? LoginProvider { get; set; } 6 | public string? ProviderKey { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/SetPasswordViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 4 | 5 | public class SetPasswordViewModel 6 | { 7 | [Required] 8 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 9 | [DataType(DataType.Password)] 10 | [Display(Name = "New password")] 11 | public string? NewPassword { get; set; } 12 | 13 | [DataType(DataType.Password)] 14 | [Display(Name = "Confirm new password")] 15 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 16 | public string? ConfirmPassword { get; set; } 17 | 18 | public string? StatusMessage { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/ShowRecoveryCodesViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 2 | 3 | public class ShowRecoveryCodesViewModel 4 | { 5 | public string[]? RecoveryCodes { get; set; } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/Web/ViewModels/Manage/TwoFactorAuthenticationViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 2 | 3 | public class TwoFactorAuthenticationViewModel 4 | { 5 | public bool HasAuthenticator { get; set; } 6 | public int RecoveryCodesLeft { get; set; } 7 | public bool Is2faEnabled { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Web/ViewModels/OrderItemViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels; 2 | 3 | public class OrderItemViewModel 4 | { 5 | public int ProductId { get; set; } 6 | public string? ProductName { get; set; } 7 | public decimal UnitPrice { get; set; } 8 | public decimal Discount => 0; 9 | public int Units { get; set; } 10 | public string? PictureUrl { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Web/ViewModels/OrderViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 4 | 5 | namespace Microsoft.eShopWeb.Web.ViewModels; 6 | 7 | public class OrderViewModel 8 | { 9 | private const string DEFAULT_STATUS = "Pending"; 10 | 11 | public int OrderNumber { get; set; } 12 | public DateTimeOffset OrderDate { get; set; } 13 | public decimal Total { get; set; } 14 | public string Status => DEFAULT_STATUS; 15 | public Address? ShippingAddress { get; set; } 16 | public List OrderItems { get; set; } = new List(); 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/ViewModels/PaginationInfoViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels; 2 | 3 | public class PaginationInfoViewModel 4 | { 5 | public int TotalItems { get; set; } 6 | public int ItemsPerPage { get; set; } 7 | public int ActualPage { get; set; } 8 | public int TotalPages { get; set; } 9 | public string? Previous { get; set; } 10 | public string? Next { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Web/Views/Account/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Locked out"; 3 | } 4 | 5 |
6 |

@ViewData["Title"]

7 |

This account has been locked out, please try again later.

8 |
9 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @model ChangePasswordViewModel 2 | @{ 3 | ViewData["Title"] = "Change password"; 4 | ViewData.AddActivePage(ManageNavPages.ChangePassword); 5 | } 6 | 7 |

@ViewData["Title"]

8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | @section Scripts { 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/Disable2fa.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Disable two-factor authentication (2FA)"; 3 | ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); 4 | } 5 | 6 |

@ViewData["Title"]

7 | 8 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/GenerateRecoveryCodes.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; 3 | ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); 4 | } 5 | 6 |

@ViewData["Title"]

7 | 8 | 21 | 22 |
23 |
24 | 25 |
26 |
-------------------------------------------------------------------------------- /src/Web/Views/Manage/ManageNavPages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.Rendering; 3 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 4 | 5 | namespace Microsoft.eShopWeb.Web.Views.Manage; 6 | 7 | public static class ManageNavPages 8 | { 9 | public static string ActivePageKey => "ActivePage"; 10 | 11 | public static string Index => "Index"; 12 | 13 | public static string ChangePassword => "ChangePassword"; 14 | 15 | public static string ExternalLogins => "ExternalLogins"; 16 | 17 | public static string TwoFactorAuthentication => "TwoFactorAuthentication"; 18 | 19 | public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); 20 | 21 | public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); 22 | 23 | public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); 24 | 25 | public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); 26 | 27 | public static string PageNavClass(ViewContext viewContext, string page) 28 | { 29 | var activePage = viewContext.ViewData["ActivePage"] as string; 30 | return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : string.Empty; 31 | } 32 | 33 | public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage; 34 | } 35 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/ResetAuthenticator.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Reset authenticator key"; 3 | ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); 4 | } 5 | 6 |

@ViewData["Title"]

7 | 17 |
18 |
19 | 20 |
21 |
-------------------------------------------------------------------------------- /src/Web/Views/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SetPasswordViewModel 2 | @{ 3 | ViewData["Title"] = "Set password"; 4 | ViewData.AddActivePage(ManageNavPages.ChangePassword); 5 | } 6 | 7 |

Set your password

8 | 9 |

10 | You do not have a local username/password for this site. Add a local 11 | account so you can log in without an external login. 12 |

13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 | @section Scripts { 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/ShowRecoverCodes.cshtml: -------------------------------------------------------------------------------- 1 | @model ShowRecoveryCodesViewModel 2 | @{ 3 | ViewData["Title"] = "Recovery codes"; 4 | ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); 5 | } 6 | 7 |

@ViewData["Title"]

8 | 17 |
18 |
19 | @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) 20 | { 21 | @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
22 | } 23 |
24 |
25 | © 2021 GitHub, Inc. -------------------------------------------------------------------------------- /src/Web/Views/Manage/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } 4 | 5 |
6 |

Manage your account

7 |

Change your account settings

8 |
9 |
10 |
11 | 12 |
13 |
14 | @RenderBody() 15 |
16 |
17 |
18 | 19 | @section Scripts { 20 | @RenderSection("Scripts", required: false) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/_ManageNav.cshtml: -------------------------------------------------------------------------------- 1 | @inject SignInManager SignInManager 2 | @{ 3 | var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 4 | } 5 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!String.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } 11 | -------------------------------------------------------------------------------- /src/Web/Views/Manage/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.eShopWeb.Web.Views.Manage -------------------------------------------------------------------------------- /src/Web/Views/Shared/Components/Basket/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model BasketComponentViewModel 2 | 3 | @{ 4 | ViewData["Title"] = "My Basket"; 5 | } 6 | 7 | 8 |
9 | 10 |
11 |
12 | @Model.ItemsCount 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 |

Development Mode

9 |

10 | Swapping to Development environment will display more detailed information about the error that occurred. 11 |

12 |

13 | 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. 14 |

15 | -------------------------------------------------------------------------------- /src/Web/Views/Shared/_CookieConsentPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http.Features 2 | 3 | @{ 4 | var consentFeature = Context.Features.Get(); 5 | var showBanner = !consentFeature?.CanTrack ?? false; 6 | var cookieString = consentFeature?.CreateConsentCookie(); 7 | } 8 | 9 | @if (showBanner) 10 | { 11 | 17 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Web/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.eShopWeb.Web 2 | @using Microsoft.eShopWeb.Web.ViewModels 3 | @using Microsoft.eShopWeb.Web.ViewModels.Account 4 | @using Microsoft.eShopWeb.Web.ViewModels.Manage 5 | @using Microsoft.eShopWeb.Web.Pages 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.eShopWeb.Infrastructure.Identity 8 | @namespace Microsoft.eShopWeb.Web.Pages 9 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 10 | -------------------------------------------------------------------------------- /src/Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "https://localhost:5099/api/", 4 | "webBase": "https://localhost:44315/" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Debug", 9 | "System": "Information", 10 | "Microsoft": "Information" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "https://localhost:5099/api/", 4 | "webBase": "https://localhost:44315/" 5 | }, 6 | "ConnectionStrings": { 7 | "CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;", 8 | "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;" 9 | }, 10 | "CatalogBaseUrl": "", 11 | "Logging": { 12 | "IncludeScopes": false, 13 | "LogLevel": { 14 | "Default": "Warning", 15 | "Microsoft": "Warning", 16 | "System": "Warning" 17 | }, 18 | "AllowedHosts": "*" 19 | } 20 | } -------------------------------------------------------------------------------- /src/Web/bundleconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outputFileName": "wwwroot/css/site.min.css", 4 | "inputFiles": [ 5 | "wwwroot/css/app.css", 6 | "wwwroot/css/app.component.css", 7 | "wwwroot/css/shared/components/header/header.css", 8 | "wwwroot/css/shared/components/identity/identity.css", 9 | "wwwroot/css/shared/components/pager/pager.css", 10 | "wwwroot/css/basket/basket.component.css", 11 | "wwwroot/css/basket/basket-status/basket-status.component.css", 12 | "wwwroot/css/catalog/catalog.component.css", 13 | "wwwroot/css/orders/orders.component.css" 14 | ] 15 | }, 16 | { 17 | "outputFileName": "wwwroot/js/site.min.js", 18 | "inputFiles": [ 19 | "wwwroot/js/site.js" 20 | ], 21 | "minify": { 22 | "enabled": true, 23 | "renameLocals": true 24 | }, 25 | "sourceMap": false 26 | } 27 | ] -------------------------------------------------------------------------------- /src/Web/compilerconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outputFile": "wwwroot/css/shared/components/header/header.css", 4 | "inputFile": "wwwroot/css/shared/components/header/header.scss" 5 | }, 6 | { 7 | "outputFile": "wwwroot/css/orders/orders.component.css", 8 | "inputFile": "wwwroot/css/orders/orders.component.scss" 9 | }, 10 | { 11 | "outputFile": "wwwroot/css/catalog/catalog.component.css", 12 | "inputFile": "wwwroot/css/catalog/catalog.component.scss" 13 | }, 14 | { 15 | "outputFile": "wwwroot/css/basket/basket.component.css", 16 | "inputFile": "wwwroot/css/basket/basket.component.scss" 17 | }, 18 | { 19 | "outputFile": "wwwroot/css/basket/basket-status/basket-status.component.css", 20 | "inputFile": "wwwroot/css/basket/basket-status/basket-status.component.scss" 21 | }, 22 | { 23 | "outputFile": "wwwroot/css/app.component.css", 24 | "inputFile": "wwwroot/css/app.component.scss" 25 | }, 26 | { 27 | "outputFile": "wwwroot/css/_variables.css", 28 | "inputFile": "wwwroot/css/_variables.scss" 29 | }, 30 | { 31 | "outputFile": "wwwroot/css/shared/components/pager/pager.css", 32 | "inputFile": "wwwroot/css/shared/components/pager/pager.scss" 33 | } 34 | ] -------------------------------------------------------------------------------- /src/Web/key-768c1632-cf7b-41a9-bb7a-bff228ae8fba.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 2021-12-01T14:37:52.0438755Z 4 | 2021-12-01T14:37:52.0246578Z 5 | 2022-03-01T14:37:52.0246578Z 6 | 7 | 8 | 9 | 10 | 11 | 12 | PF3GdfO7PnvHYvXyD5nxmoQ91pY9qfA0rjRsdXHdUQbE1Mg9Xok2gXLY2zn8XemsySH37UGrGknht8u/PlehWg== 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Web/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [ 5 | { 6 | "library": "jquery@3.6.3", 7 | "destination": "wwwroot/lib/jquery/" 8 | }, 9 | { 10 | "library": "twitter-bootstrap@5.2.3", 11 | "files": [ 12 | "css/bootstrap.css", 13 | "css/bootstrap.css.map", 14 | "css/bootstrap.min.css", 15 | "css/bootstrap.min.css.map", 16 | "js/bootstrap.js", 17 | "js/bootstrap.min.js" 18 | ], 19 | "destination": "wwwroot/lib/bootstrap/dist/" 20 | }, 21 | { 22 | "library": "jquery-validation-unobtrusive@4.0.0", 23 | "destination": "wwwroot/lib/jquery-validation-unobtrusive/" 24 | }, 25 | { 26 | "library": "jquery-validate@1.19.5", 27 | "destination": "wwwroot/lib/jquery-validate/", 28 | "files": [ 29 | "jquery.validate.min.js", 30 | "jquery.validate.js" 31 | ] 32 | }, 33 | { 34 | "library": "toastr.js@2.1.4", 35 | "destination": "wwwroot/lib/toastr/" 36 | }, 37 | { 38 | "library": "aspnet-signalr@1.0.27", 39 | "files": [ 40 | "signalr.js", 41 | "signalr.min.js" 42 | ], 43 | "destination": "wwwroot/lib/@aspnet/signalr/dist/browser/" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/_variables.css: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/Web/wwwroot/css/_variables.min.css: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/Web/wwwroot/css/app.component.css: -------------------------------------------------------------------------------- 1 | .esh-app-footer { 2 | background-color: #000000; 3 | border-top: 1px solid #EEEEEE; 4 | margin-top: 2.5rem; 5 | padding-bottom: 2.5rem; 6 | padding-top: 2.5rem; 7 | width: 100%; 8 | bottom: 0; } 9 | .esh-app-footer-brand { 10 | height: 50px; 11 | width: 230px; } 12 | 13 | .esh-app-header { 14 | margin: 15px; } 15 | 16 | .esh-app-wrapper { 17 | display: flex; 18 | min-height: 100vh; 19 | flex-direction: column; 20 | justify-content: space-between; } 21 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/app.component.min.css: -------------------------------------------------------------------------------- 1 | .esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;bottom:0;}.esh-app-footer-brand{height:50px;width:230px;}.esh-app-header{margin:15px;}.esh-app-wrapper{display:flex;min-height:100vh;flex-direction:column;justify-content:space-between;} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/app.component.scss: -------------------------------------------------------------------------------- 1 | @import './_variables.scss'; 2 | 3 | .esh-app { 4 | &-footer { 5 | $margin: 2.5rem; 6 | $padding: 2.5rem; 7 | 8 | background-color: $color-background-darker; 9 | border-top: $border-light solid $color-foreground-bright; 10 | margin-top: $margin; 11 | padding-bottom: $padding; 12 | padding-top: $padding; 13 | width: 100%; 14 | bottom: 0; 15 | $height: 50px; 16 | 17 | &-brand { 18 | height: $height; 19 | width: 230px; 20 | } 21 | } 22 | 23 | &-header { 24 | margin: 15px; 25 | } 26 | 27 | &-wrapper { 28 | display: flex; 29 | min-height: 100vh; 30 | flex-direction: column; 31 | justify-content: space-between 32 | } 33 | } -------------------------------------------------------------------------------- /src/Web/wwwroot/css/app.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/basket/basket-status/basket-status.component.css: -------------------------------------------------------------------------------- 1 | .esh-basketstatus { 2 | cursor: pointer; 3 | display: inline-block; 4 | float: right; 5 | position: relative; 6 | transition: all 0.35s; } 7 | .esh-basketstatus.is-disabled { 8 | opacity: .5; 9 | pointer-events: none; } 10 | .esh-basketstatus-image { 11 | height: 36px; 12 | margin-top: .5rem; } 13 | .esh-basketstatus-badge { 14 | background-color: #83D01B; 15 | border-radius: 50%; 16 | color: #FFFFFF; 17 | display: block; 18 | height: 1.5rem; 19 | left: 50%; 20 | position: absolute; 21 | text-align: center; 22 | top: 0; 23 | transform: translateX(-38%); 24 | transition: all 0.35s; 25 | width: 1.5rem; } 26 | .esh-basketstatus-badge-inoperative { 27 | background-color: #ff0000; 28 | border-radius: 50%; 29 | color: #FFFFFF; 30 | display: block; 31 | height: 1.5rem; 32 | left: 50%; 33 | position: absolute; 34 | text-align: center; 35 | top: 0; 36 | transform: translateX(-38%); 37 | transition: all 0.35s; 38 | width: 1.5rem; } 39 | .esh-basketstatus:hover .esh-basketstatus-badge { 40 | background-color: transparent; 41 | color: #75b918; 42 | transition: all 0.35s; } 43 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/basket/basket-status/basket-status.component.min.css: -------------------------------------------------------------------------------- 1 | .esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s;}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none;}.esh-basketstatus-image{height:36px;margin-top:.5rem;}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem;}.esh-basketstatus-badge-inoperative{background-color:#f00;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem;}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s;} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/basket/basket.component.min.css: -------------------------------------------------------------------------------- 1 | .esh-basket{min-height:80vh;}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem;}.esh-basket-titles--clean{padding-bottom:0;padding-top:0;}.esh-basket-title{text-transform:uppercase;}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0;}.esh-basket-items--border:last-of-type{border-color:transparent;}.esh-basket-items-margin-left1{margin-left:1px;}.esh-basket-item{font-size:1rem;font-weight:300;}.esh-basket-item--middle{line-height:8rem;}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem;}}.esh-basket-item--mark{color:#00a69c;}.esh-basket-image{height:8rem;}.esh-basket-input{line-height:1rem;width:100%;}.esh-basket-checkout{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s;}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s;}.esh-basket-checkout:visited{color:#fff;} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/catalog/pager.css: -------------------------------------------------------------------------------- 1 | .esh-pager-wrapper { 2 | padding-top: 1rem; 3 | text-align: center; 4 | } 5 | 6 | .esh-pager-item-left { 7 | float: left; 8 | } 9 | 10 | .esh-pager-item-right { 11 | float: right; 12 | } 13 | 14 | .esh-pager-item--navigable { 15 | display: inline-block; 16 | cursor: pointer; 17 | } 18 | 19 | .esh-pager-item--navigable.is-disabled { 20 | opacity: 0; 21 | pointer-events: none; 22 | } 23 | 24 | .esh-pager-item--navigable:hover { 25 | color: #83D01B; 26 | } 27 | 28 | @media screen and (max-width: 1280px) { 29 | .esh-pager-item { 30 | font-size: 0.85rem; 31 | } 32 | } 33 | 34 | @media screen and (max-width: 1024px) { 35 | .esh-pager-item { 36 | margin: 0 4vw; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/orders/orders.component.min.css: -------------------------------------------------------------------------------- 1 | .esh-orders{min-height:80vh;overflow-x:hidden;}.esh-orders-header{background-color:#00a69c;height:4rem;}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s;}.esh-orders-back:hover{color:#fff;transition:color .35s;}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem;}.esh-orders-title{text-transform:uppercase;}.esh-orders-items{height:2rem;line-height:2rem;position:relative;}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1;}.esh-orders-item{font-weight:300;}.esh-orders-item--hover{opacity:0;pointer-events:none;}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all;}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s;}.esh-orders-link:hover{color:#75b918;transition:color .35s;}.esh-orders-detail-section{padding-bottom:30px;}.esh-orders-detail-title{font-size:25px;} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/header/header.css: -------------------------------------------------------------------------------- 1 | .esh-header { 2 | background-color: #00A69C; 3 | height: 4rem; } 4 | .esh-header-back { 5 | color: rgba(255, 255, 255, 0.5); 6 | line-height: 4rem; 7 | text-decoration: none; 8 | text-transform: uppercase; 9 | transition: color 0.35s; } 10 | .esh-header-back:hover { 11 | color: #FFFFFF; 12 | transition: color 0.35s; } 13 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/header/header.min.css: -------------------------------------------------------------------------------- 1 | .esh-header{background-color:#00a69c;height:4rem;}.esh-header-back{color:rgba(255,255,255,.5);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s;}.esh-header-back:hover{color:#fff;transition:color .35s;} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/header/header.scss: -------------------------------------------------------------------------------- 1 | @import '../../../variables.scss'; 2 | 3 | .esh-header { 4 | $header-height: 4rem; 5 | 6 | background-color: $color-brand; 7 | height: $header-height; 8 | 9 | &-back { 10 | color: rgba($color-foreground-brighter, .5); 11 | line-height: $header-height; 12 | text-decoration: none; 13 | text-transform: uppercase; 14 | transition: color $animation-speed-default; 15 | 16 | &:hover { 17 | color: $color-foreground-brighter; 18 | transition: color $animation-speed-default; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/identity/identity.css: -------------------------------------------------------------------------------- 1 | .esh-identity { 2 | line-height: 3rem; 3 | position: relative; 4 | text-align: right; 5 | } 6 | 7 | .esh-identity-section { 8 | display: inline-block; 9 | width: 100%; 10 | } 11 | 12 | .esh-identity-name { 13 | display: inline-block; 14 | } 15 | 16 | .esh-identity-name--upper { 17 | text-transform: uppercase; 18 | } 19 | 20 | @media screen and (max-width: 768px) { 21 | .esh-identity-name { 22 | font-size: 0.85rem; 23 | } 24 | } 25 | 26 | .esh-identity-image { 27 | display: inline-block; 28 | } 29 | 30 | .esh-identity-drop { 31 | background: #FFFFFF; 32 | height: 10px; 33 | width: 10rem; 34 | overflow: hidden; 35 | padding: .5rem; 36 | position: absolute; 37 | right: 0; 38 | top: 2.5rem; 39 | transition: height 0.35s; 40 | } 41 | 42 | .esh-identity:hover .esh-identity-drop { 43 | border: 1px solid #EEEEEE; 44 | height: 14rem; 45 | transition: height 0.35s; 46 | z-index: 10; 47 | } 48 | 49 | .esh-identity-item { 50 | cursor: pointer; 51 | transition: color 0.35s; 52 | } 53 | 54 | .esh-identity-item:hover { 55 | color: #75b918; 56 | transition: color 0.35s; 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/identity/identity.min.css: -------------------------------------------------------------------------------- 1 | .esh-identity{line-height:3rem;position:relative;text-align:right;}.esh-identity-section{display:inline-block;width:100%;}.esh-identity-name{display:inline-block;}.esh-identity-name--upper{text-transform:uppercase;}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem;}}.esh-identity-image{display:inline-block;}.esh-identity-drop{background:#fff;height:10px;width:10rem;overflow:hidden;padding:.5rem;position:absolute;right:0;top:2.5rem;transition:height .35s;}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:10rem;transition:height .35s;}.esh-identity-item{cursor:pointer;transition:color .35s;}.esh-identity-item:hover{color:#75b918;transition:color .35s;} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/identity/identity.scss: -------------------------------------------------------------------------------- 1 | @import '../../../variables.scss'; 2 | 3 | .esh-identity { 4 | line-height: 3rem; 5 | position: relative; 6 | text-align: right; 7 | 8 | &-section { 9 | display: inline-block; 10 | width: 100%; 11 | } 12 | 13 | &-name { 14 | display: inline-block; 15 | 16 | &--upper { 17 | text-transform: uppercase; 18 | } 19 | 20 | @media screen and (max-width: $media-screen-s) { 21 | font-size: $font-size-s; 22 | } 23 | } 24 | 25 | &-image { 26 | display: inline-block; 27 | } 28 | 29 | &-drop { 30 | background: $color-background-brighter; 31 | height: 10px; 32 | width: 10rem; 33 | overflow: hidden; 34 | padding: .5rem; 35 | position: absolute; 36 | right: 0; 37 | top: 2.5rem; 38 | transition: height $animation-speed-default; 39 | } 40 | 41 | &:hover &-drop { 42 | border: $border-light solid $color-foreground-bright; 43 | height: 10rem; 44 | transition: height $animation-speed-default; 45 | } 46 | 47 | &-item { 48 | cursor: pointer; 49 | transition: color $animation-speed-default; 50 | 51 | &:hover { 52 | color: $color-secondary-dark; 53 | transition: color $animation-speed-default; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/pager/pager.css: -------------------------------------------------------------------------------- 1 | .esh-pager-wrapper { 2 | padding-top: 1rem; 3 | text-align: center; } 4 | 5 | .esh-pager-item { 6 | margin: 0 5vw; } 7 | .esh-pager-item.is-disabled { 8 | opacity: .5; 9 | pointer-events: none; } 10 | .esh-pager-item--navigable { 11 | cursor: pointer; 12 | display: inline-block; } 13 | .esh-pager-item--navigable:hover { 14 | color: #83D01B; } 15 | @media screen and (max-width: 1280px) { 16 | .esh-pager-item { 17 | font-size: 0.85rem; } } 18 | @media screen and (max-width: 1024px) { 19 | .esh-pager-item { 20 | margin: 0 2.5vw; } } 21 | -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/pager/pager.min.css: -------------------------------------------------------------------------------- 1 | .esh-pager-wrapper{padding-top:1rem;text-align:center;}.esh-pager-item{margin:0 5vw;}.esh-pager-item.is-disabled{opacity:.5;pointer-events:none;}.esh-pager-item--navigable{cursor:pointer;display:inline-block;}.esh-pager-item--navigable:hover{color:#83d01b;}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem;}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 2.5vw;}} -------------------------------------------------------------------------------- /src/Web/wwwroot/css/shared/components/pager/pager.scss: -------------------------------------------------------------------------------- 1 | @import '../../../variables.scss'; 2 | 3 | .esh-pager { 4 | 5 | &-wrapper { 6 | padding-top: 1rem; 7 | text-align: center; 8 | } 9 | 10 | &-item { 11 | $margin: 5vw; 12 | margin: 0 $margin; 13 | 14 | &.is-disabled { 15 | opacity: .5; 16 | pointer-events: none; 17 | } 18 | 19 | &--navigable { 20 | cursor: pointer; 21 | display: inline-block; 22 | 23 | &:hover { 24 | color: $color-secondary; 25 | } 26 | } 27 | 28 | @media screen and (max-width: $media-screen-l) { 29 | font-size: $font-size-s; 30 | } 31 | 32 | @media screen and (max-width: $media-screen-m) { 33 | margin: 0 $margin / 2; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Bold.eot -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Bold.woff -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Regular.eot -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Regular.woff -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/fonts/Montserrat-Regular.woff2 -------------------------------------------------------------------------------- /src/Web/wwwroot/images/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/arrow-down.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Web/wwwroot/images/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/brand.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/cart.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/logout.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/main_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/main_banner.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/main_banner_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/main_banner_text.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/my_orders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/my_orders.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/1.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/10.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/11.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/12.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/2.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/3.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/4.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/5.jpg -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/5.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/6.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/7.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/8.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/9.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/eCatalog-item-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/images/products/eCatalog-item-default.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Web/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your Javascript code. 2 | -------------------------------------------------------------------------------- /src/Web/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/eShopOnAKS/ce762d8a057a62cc83c38126fac1c13d18f06a9c/src/Web/wwwroot/js/site.min.js -------------------------------------------------------------------------------- /tests/FunctionalTests/FunctionalTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.eShopWeb.FunctionalTests 5 | false 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/FunctionalTests/Web/Controllers/CatalogControllerIndex.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; 6 | 7 | [Collection("Sequential")] 8 | public class CatalogControllerIndex : IClassFixture 9 | { 10 | public CatalogControllerIndex(TestApplication factory) 11 | { 12 | Client = factory.CreateClient(); 13 | } 14 | 15 | public HttpClient Client { get; } 16 | 17 | [Fact] 18 | public async Task ReturnsHomePageWithProductListing() 19 | { 20 | // Arrange & Act 21 | var response = await Client.GetAsync("/"); 22 | response.EnsureSuccessStatusCode(); 23 | var stringResponse = await response.Content.ReadAsStringAsync(); 24 | 25 | // Assert 26 | Assert.Contains(".NET Bot Black Sweatshirt", stringResponse); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc.Testing; 5 | using Xunit; 6 | 7 | namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; 8 | 9 | [Collection("Sequential")] 10 | public class OrderIndexOnGet : IClassFixture 11 | { 12 | public OrderIndexOnGet(TestApplication factory) 13 | { 14 | Client = factory.CreateClient(new WebApplicationFactoryClientOptions 15 | { 16 | AllowAutoRedirect = false 17 | }); 18 | } 19 | 20 | public HttpClient Client { get; } 21 | 22 | [Fact] 23 | public async Task ReturnsRedirectGivenAnonymousUser() 24 | { 25 | var response = await Client.GetAsync("/order/my-orders"); 26 | var redirectLocation = response.Headers.Location.OriginalString; 27 | 28 | Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); 29 | Assert.Contains("/Account/Login", redirectLocation); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/FunctionalTests/Web/Pages/HomePageOnGet.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.FunctionalTests.Web; 2 | using Xunit; 3 | 4 | namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages; 5 | 6 | [Collection("Sequential")] 7 | public class HomePageOnGet : IClassFixture 8 | { 9 | public HomePageOnGet(TestApplication factory) 10 | { 11 | Client = factory.CreateClient(); 12 | } 13 | 14 | public HttpClient Client { get; } 15 | 16 | [Fact] 17 | public async Task ReturnsHomePageWithProductListing() 18 | { 19 | // Arrange & Act 20 | var response = await Client.GetAsync("/"); 21 | response.EnsureSuccessStatusCode(); 22 | var stringResponse = await response.Content.ReadAsStringAsync(); 23 | 24 | // Assert 25 | Assert.Contains(".NET Bot Black Sweatshirt", stringResponse); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/FunctionalTests/Web/WebPageHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Microsoft.eShopWeb.FunctionalTests.Web; 4 | 5 | public static class WebPageHelpers 6 | { 7 | public static string TokenTag = "__RequestVerificationToken"; 8 | 9 | public static string GetRequestVerificationToken(string input) 10 | { 11 | string regexpression = @"name=""__RequestVerificationToken"" type=""hidden"" value=""([-A-Za-z0-9+=/\\_]+?)"""; 12 | return RegexSearch(regexpression, input); 13 | } 14 | 15 | public static string GetId(string input) 16 | { 17 | string regexpression = @"name=""Items\[0\].Id"" value=""(\d)"""; 18 | return RegexSearch(regexpression, input); 19 | } 20 | 21 | private static string RegexSearch(string regexpression, string input) 22 | { 23 | var regex = new Regex(regexpression); 24 | var match = regex.Match(input); 25 | return match.Groups.Values.LastOrDefault().Value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/IntegrationTests/IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.eShopWeb.IntegrationTests 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb; 2 | using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | 7 | namespace PublicApiIntegrationTests.CatalogItemEndpoints 8 | { 9 | [TestClass] 10 | public class CatalogItemGetByIdEndpointTest 11 | { 12 | [TestMethod] 13 | public async Task ReturnsItemGivenValidId() 14 | { 15 | var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5"); 16 | response.EnsureSuccessStatusCode(); 17 | var stringResponse = await response.Content.ReadAsStringAsync(); 18 | var model = stringResponse.FromJson(); 19 | 20 | Assert.AreEqual(5, model.CatalogItem.Id); 21 | Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name); 22 | } 23 | 24 | [TestMethod] 25 | public async Task ReturnsNotFoundGivenInvalidId() 26 | { 27 | var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0"); 28 | 29 | Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/PublicApiIntegrationTests/ProgramTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Net.Http; 4 | 5 | namespace PublicApiIntegrationTests 6 | { 7 | [TestClass] 8 | public class ProgramTest 9 | { 10 | private static WebApplicationFactory _application; 11 | 12 | public static HttpClient NewClient 13 | { 14 | get 15 | { 16 | return _application.CreateClient(); 17 | } 18 | } 19 | 20 | [AssemblyInitialize] 21 | public static void AssemblyInitialize(TestContext _) 22 | { 23 | _application = new WebApplicationFactory(); 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | true 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/PublicApiIntegrationTests/appsettings.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "UseOnlyInMemoryDatabase": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketRemoveEmptyItems.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 2 | using Xunit; 3 | 4 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests; 5 | 6 | public class BasketRemoveEmptyItems 7 | { 8 | private readonly int _testCatalogItemId = 123; 9 | private readonly decimal _testUnitPrice = 1.23m; 10 | private readonly string _buyerId = "Test buyerId"; 11 | 12 | [Fact] 13 | public void RemovesEmptyBasketItems() 14 | { 15 | var basket = new Basket(_buyerId); 16 | basket.AddItem(_testCatalogItemId, _testUnitPrice, 0); 17 | basket.RemoveEmptyItems(); 18 | 19 | Assert.Equal(0, basket.Items.Count); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketTotalItems.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 2 | using Xunit; 3 | 4 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests; 5 | 6 | public class BasketTotalItems 7 | { 8 | private readonly int _testCatalogItemId = 123; 9 | private readonly decimal _testUnitPrice = 1.23m; 10 | private readonly int _testQuantity = 2; 11 | private readonly string _buyerId = "Test buyerId"; 12 | 13 | [Fact] 14 | public void ReturnsTotalQuantityWithOneItem() 15 | { 16 | var basket = new Basket(_buyerId); 17 | basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); 18 | 19 | var result = basket.TotalItems; 20 | 21 | Assert.Equal(_testQuantity, result); 22 | } 23 | 24 | [Fact] 25 | public void ReturnsTotalQuantityWithMultipleItems() 26 | { 27 | var basket = new Basket(_buyerId); 28 | basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); 29 | basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity*2); 30 | 31 | var result = basket.TotalItems; 32 | 33 | Assert.Equal(_testQuantity*3, result); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Entities/OrderTests/OrderTotal.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 3 | using Microsoft.eShopWeb.UnitTests.Builders; 4 | using Xunit; 5 | 6 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.OrderTests; 7 | 8 | public class OrderTotal 9 | { 10 | private decimal _testUnitPrice = 42m; 11 | 12 | [Fact] 13 | public void IsZeroForNewOrder() 14 | { 15 | var order = new OrderBuilder().WithNoItems(); 16 | 17 | Assert.Equal(0, order.Total()); 18 | } 19 | 20 | [Fact] 21 | public void IsCorrectGiven1Item() 22 | { 23 | var builder = new OrderBuilder(); 24 | var items = new List 25 | { 26 | new OrderItem(builder.TestCatalogItemOrdered, _testUnitPrice, 1) 27 | }; 28 | var order = new OrderBuilder().WithItems(items); 29 | Assert.Equal(_testUnitPrice, order.Total()); 30 | } 31 | 32 | [Fact] 33 | public void IsCorrectGiven3Items() 34 | { 35 | var builder = new OrderBuilder(); 36 | var order = builder.WithDefaultValues(); 37 | 38 | Assert.Equal(builder.TestUnitPrice * builder.TestUnits, order.Total()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Extensions/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; 4 | 5 | public class JsonExtensions 6 | { 7 | [Fact] 8 | public void CorrectlySerializesAndDeserializesObject() 9 | { 10 | var testParent = new TestParent 11 | { 12 | Id = 7, 13 | Name = "Test name", 14 | Children = new[] 15 | { 16 | new TestChild(), 17 | new TestChild(), 18 | new TestChild() 19 | } 20 | }; 21 | 22 | var json = testParent.ToJson(); 23 | var result = json.FromJson(); 24 | Assert.Equal(testParent, result); 25 | } 26 | 27 | [ 28 | Theory, 29 | InlineData("{ \"id\": 9, \"name\": \"Another test\" }", 9, "Another test"), 30 | InlineData("{ \"id\": 3124, \"name\": \"Test Value 1\" }", 3124, "Test Value 1"), 31 | ] 32 | public void CorrectlyDeserializesJson(string json, int expectedId, string expectedName) => 33 | Assert.Equal(new TestParent { Id = expectedId, Name = expectedName }, json.FromJson()); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Extensions/TestChild.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; 6 | 7 | [DebuggerDisplay("Id={Id}, Date={Date}")] 8 | public class TestChild : IEquatable 9 | { 10 | public Guid Id { get; set; } = Guid.NewGuid(); 11 | 12 | public DateTime Date { get; set; } = DateTime.UtcNow; 13 | 14 | public bool Equals([AllowNull] TestChild other) => 15 | other?.Date == Date && other?.Id == Id; 16 | } 17 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Extensions/TestParent.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; 4 | 5 | public class TestParent : IEquatable 6 | { 7 | public int Id { get; set; } 8 | 9 | public string? Name { get; set; } 10 | 11 | public IEnumerable? Children { get; set; } 12 | 13 | public bool Equals([AllowNull] TestParent other) 14 | { 15 | if (other?.Id == Id && other?.Name == Name) 16 | { 17 | if (Children is null) 18 | { 19 | return other?.Children is null; 20 | } 21 | 22 | return other?.Children?.Zip(Children).All(t => t.First?.Equals(t.Second) ?? false) ?? false; 23 | } 24 | 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 3 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | using Microsoft.eShopWeb.ApplicationCore.Services; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; 9 | 10 | public class DeleteBasket 11 | { 12 | private readonly string _buyerId = "Test buyerId"; 13 | private readonly Mock> _mockBasketRepo = new(); 14 | private readonly Mock> _mockLogger = new(); 15 | 16 | [Fact] 17 | public async Task ShouldInvokeBasketRepositoryDeleteAsyncOnce() 18 | { 19 | var basket = new Basket(_buyerId); 20 | basket.AddItem(1, It.IsAny(), It.IsAny()); 21 | basket.AddItem(2, It.IsAny(), It.IsAny()); 22 | _mockBasketRepo.Setup(x => x.GetByIdAsync(It.IsAny(), default)) 23 | .ReturnsAsync(basket); 24 | var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object); 25 | 26 | await basketService.DeleteBasketAsync(It.IsAny()); 27 | 28 | _mockBasketRepo.Verify(x => x.DeleteAsync(It.IsAny(), default), Times.Once); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/UnitTests/ApplicationCore/Specifications/CatalogFilterSpecification.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.eShopWeb.ApplicationCore.Entities; 4 | using Xunit; 5 | 6 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; 7 | 8 | public class CatalogFilterSpecification 9 | { 10 | [Theory] 11 | [InlineData(null, null, 5)] 12 | [InlineData(1, null, 3)] 13 | [InlineData(2, null, 2)] 14 | [InlineData(null, 1, 2)] 15 | [InlineData(null, 3, 1)] 16 | [InlineData(1, 3, 1)] 17 | [InlineData(2, 3, 0)] 18 | public void MatchesExpectedNumberOfItems(int? brandId, int? typeId, int expectedCount) 19 | { 20 | var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterSpecification(brandId, typeId); 21 | 22 | var result = spec.Evaluate(GetTestItemCollection()).ToList(); 23 | 24 | Assert.Equal(expectedCount, result.Count()); 25 | } 26 | 27 | public List GetTestItemCollection() 28 | { 29 | return new List() 30 | { 31 | new CatalogItem(1, 1, "Description", "Name", 0, "FakePath"), 32 | new CatalogItem(2, 1, "Description", "Name", 0, "FakePath"), 33 | new CatalogItem(3, 1, "Description", "Name", 0, "FakePath"), 34 | new CatalogItem(1, 2, "Description", "Name", 0, "FakePath"), 35 | new CatalogItem(2, 2, "Description", "Name", 0, "FakePath"), 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/UnitTests/Builders/AddressBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 2 | 3 | namespace Microsoft.eShopWeb.UnitTests.Builders; 4 | 5 | public class AddressBuilder 6 | { 7 | private Address _address; 8 | public string TestStreet => "123 Main St."; 9 | public string TestCity => "Kent"; 10 | public string TestState => "OH"; 11 | public string TestCountry => "USA"; 12 | public string TestZipCode => "44240"; 13 | 14 | public AddressBuilder() 15 | { 16 | _address = WithDefaultValues(); 17 | } 18 | public Address Build() 19 | { 20 | return _address; 21 | } 22 | public Address WithDefaultValues() 23 | { 24 | _address = new Address(TestStreet, TestCity, TestState, TestCountry, TestZipCode); 25 | return _address; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/UnitTests/Builders/BasketBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; 2 | using Moq; 3 | 4 | namespace Microsoft.eShopWeb.UnitTests.Builders; 5 | 6 | public class BasketBuilder 7 | { 8 | private Basket _basket; 9 | public string BasketBuyerId => "testbuyerId@test.com"; 10 | 11 | public int BasketId => 1; 12 | 13 | public BasketBuilder() 14 | { 15 | _basket = WithNoItems(); 16 | } 17 | 18 | public Basket Build() 19 | { 20 | return _basket; 21 | } 22 | 23 | public Basket WithNoItems() 24 | { 25 | var basketMock = new Mock(BasketBuyerId); 26 | basketMock.SetupGet(s => s.Id).Returns(BasketId); 27 | 28 | _basket = basketMock.Object; 29 | return _basket; 30 | } 31 | 32 | public Basket WithOneBasketItem() 33 | { 34 | var basketMock = new Mock(BasketBuyerId); 35 | _basket = basketMock.Object; 36 | _basket.AddItem(2, 3.40m, 4); 37 | return _basket; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | Microsoft.eShopWeb.UnitTests 6 | false 7 | latest 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateBrandsCacheKey.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.Web.Extensions; 2 | using Xunit; 3 | 4 | namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests; 5 | 6 | public class GenerateBrandsCacheKey 7 | { 8 | [Fact] 9 | public void ReturnsBrandsCacheKey() 10 | { 11 | var result = CacheHelpers.GenerateBrandsCacheKey(); 12 | 13 | Assert.Equal("brands", result); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateCatalogItemCacheKey.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.Web; 2 | using Microsoft.eShopWeb.Web.Extensions; 3 | using Xunit; 4 | 5 | namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests; 6 | 7 | public class GenerateCatalogItemCacheKey 8 | { 9 | [Fact] 10 | public void ReturnsCatalogItemCacheKey() 11 | { 12 | var pageIndex = 0; 13 | int? brandId = null; 14 | int? typeId = null; 15 | 16 | var result = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId); 17 | 18 | Assert.Equal("items-0-10--", result); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateTypesCacheKey.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.eShopWeb.Web.Extensions; 2 | using Xunit; 3 | 4 | namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests; 5 | 6 | public class GenerateTypesCacheKey 7 | { 8 | [Fact] 9 | public void ReturnsTypesCacheKey() 10 | { 11 | var result = CacheHelpers.GenerateTypesCacheKey(); 12 | 13 | Assert.Equal("types", result); 14 | } 15 | } 16 | --------------------------------------------------------------------------------