├── .devcontainer ├── devcontainer.json └── devcontainerreadme.md ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── dotnetcore.yml │ └── richnav.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CodeCoverage.runsettings ├── Directory.Packages.props ├── Everything.sln ├── LICENSE ├── README.md ├── azure.yaml ├── docker-compose.dcproj ├── docker-compose.override.yml ├── docker-compose.yml ├── eShopOnWeb.sln ├── global.json ├── infra ├── abbreviations.json ├── core │ ├── database │ │ └── sqlserver │ │ │ └── sqlserver.bicep │ ├── host │ │ ├── appservice.bicep │ │ └── appserviceplan.bicep │ └── security │ │ ├── keyvault-access.bicep │ │ └── keyvault.bicep ├── main.bicep └── main.parameters.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 │ │ ├── CustomerOrdersSpecification.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.Docker.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 │ │ └── UserNotFoundException.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 │ ├── Dockerfile │ ├── ImageValidators.cs │ ├── MappingProfile.cs │ ├── Middleware │ │ └── ExceptionMiddleware.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── PublicApi.csproj │ ├── README.md │ ├── appsettings.Development.json │ ├── appsettings.Docker.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 │ ├── Dockerfile │ ├── 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 │ ├── OrderDetailViewModel.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.Docker.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 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.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: '8.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 - Code Index 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 8.0.x 16 | 17 | - name: Build with dotnet 18 | run: dotnet build ./Everything.sln --configuration Release /bl 19 | 20 | - uses: microsoft/RichCodeNavIndexer@v0.1 21 | with: 22 | repo-token: ${{ github.token }} 23 | languages: 'csharp' 24 | environment: 'internal' 25 | -------------------------------------------------------------------------------- /.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 | "ms-azuretools.azure-dev" 9 | ] 10 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/wbreza/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: eShopOnWeb 4 | services: 5 | web: 6 | project: ./src/Web 7 | language: csharp 8 | host: appservice -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | {1FCBE191-34FE-4B2E-8915-CA81553958AD} 7 | True 8 | {Scheme}://localhost:{ServicePort} 9 | eshopwebmvc 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | eshopwebmvc: 4 | environment: 5 | - ASPNETCORE_ENVIRONMENT=Docker 6 | - ASPNETCORE_URLS=http://+:8080 7 | ports: 8 | - "5106:8080" 9 | volumes: 10 | - ~/.aspnet/https:/root/.aspnet/https:ro 11 | - ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro 12 | eshoppublicapi: 13 | environment: 14 | - ASPNETCORE_ENVIRONMENT=Docker 15 | - ASPNETCORE_URLS=http://+:8080 16 | ports: 17 | - "5200:8080" 18 | volumes: 19 | - ~/.aspnet/https:/root/.aspnet/https:ro 20 | - ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | eshopwebmvc: 5 | image: ${DOCKER_REGISTRY-}eshopwebmvc 6 | build: 7 | context: . 8 | dockerfile: src/Web/Dockerfile 9 | depends_on: 10 | - "sqlserver" 11 | eshoppublicapi: 12 | image: ${DOCKER_REGISTRY-}eshoppublicapi 13 | build: 14 | context: . 15 | dockerfile: src/PublicApi/Dockerfile 16 | depends_on: 17 | - "sqlserver" 18 | sqlserver: 19 | image: mcr.microsoft.com/azure-sql-edge 20 | ports: 21 | - "1433:1433" 22 | environment: 23 | - SA_PASSWORD=@someThingComplicated1234 24 | - ACCEPT_EULA=Y 25 | 26 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.x", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param kind string = '' 6 | param reserved bool = true 7 | param sku object 8 | 9 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 10 | name: name 11 | location: location 12 | tags: tags 13 | sku: sku 14 | kind: kind 15 | properties: { 16 | reserved: reserved 17 | } 18 | } 19 | 20 | output id string = appServicePlan.id 21 | -------------------------------------------------------------------------------- /infra/core/security/keyvault-access.bicep: -------------------------------------------------------------------------------- 1 | param name string = 'add' 2 | 3 | param keyVaultName string 4 | param permissions object = { secrets: [ 'get', 'list' ] } 5 | param principalId string 6 | 7 | resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { 8 | parent: keyVault 9 | name: name 10 | properties: { 11 | accessPolicies: [ { 12 | objectId: principalId 13 | tenantId: subscription().tenantId 14 | permissions: permissions 15 | } ] 16 | } 17 | } 18 | 19 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 20 | name: keyVaultName 21 | } 22 | -------------------------------------------------------------------------------- /infra/core/security/keyvault.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param principalId string = '' 6 | 7 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { 8 | name: name 9 | location: location 10 | tags: tags 11 | properties: { 12 | tenantId: subscription().tenantId 13 | sku: { family: 'A', name: 'standard' } 14 | accessPolicies: !empty(principalId) ? [ 15 | { 16 | objectId: principalId 17 | permissions: { secrets: [ 'get', 'list' ] } 18 | tenantId: subscription().tenantId 19 | } 20 | ] : [] 21 | } 22 | } 23 | 24 | output endpoint string = keyVault.properties.vaultUri 25 | output name string = keyVault.name 26 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | }, 14 | "sqlAdminPassword": { 15 | "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} sqlAdminPassword)" 16 | }, 17 | "appUserPassword": { 18 | "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} appUserPassword)" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /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/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 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/CustomerOrdersSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 3 | 4 | namespace Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | 6 | public class CustomerOrdersSpecification : Specification 7 | { 8 | public CustomerOrdersSpecification(string buyerId) 9 | { 10 | Query.Where(o => o.BuyerId == buyerId) 11 | .Include(o => o.OrderItems); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 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.Docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrls": { 3 | "apiBase": "http://localhost:5200/api/", 4 | "webBase": "http://host.docker.internal:5106/" 5 | } 6 | } -------------------------------------------------------------------------------- /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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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/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/Identity/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.eShopWeb.Infrastructure.Identity; 4 | 5 | public class UserNotFoundException : Exception 6 | { 7 | public UserNotFoundException(string userName) : base($"No user found with username: {userName}") 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; 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; } = string.Empty; 16 | public string Value { get; set; } = string.Empty; 17 | } 18 | -------------------------------------------------------------------------------- /src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; 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; } = string.Empty; 10 | public string RoleClaimType { get; set; } = string.Empty; 11 | public IEnumerable Claims { get; set; } = new List(); 12 | } 13 | -------------------------------------------------------------------------------- /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/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 9 | WORKDIR /app 10 | COPY . . 11 | #COPY ["src/PublicApi/PublicApi.csproj", "./PublicApi/"] 12 | #RUN dotnet restore "./PublicApi/PublicApi.csproj" 13 | #COPY . . 14 | WORKDIR "/app/src/PublicApi" 15 | RUN dotnet restore 16 | 17 | RUN dotnet build "./PublicApi.csproj" -c Release -o /app/build 18 | 19 | FROM build AS publish 20 | RUN dotnet publish "./PublicApi.csproj" -c Release -o /app/publish 21 | 22 | FROM base AS final 23 | WORKDIR /app 24 | COPY --from=publish /app/publish . 25 | ENTRYPOINT ["dotnet", "PublicApi.dll"] -------------------------------------------------------------------------------- /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/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.Docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;", 4 | "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;" 5 | }, 6 | "baseUrls": { 7 | "apiBase": "http://localhost:5200/api/", 8 | "webBase": "http://host.docker.internal:5106/" 9 | }, 10 | "Logging": { 11 | "LogLevel": { 12 | "Default": "Information", 13 | "Microsoft": "Warning", 14 | "Microsoft.Hosting.Lifetime": "Information" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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": "8.0.0", 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 | 8 | namespace Microsoft.eShopWeb.Web.Configuration; 9 | 10 | public static class ConfigureCoreServices 11 | { 12 | public static IServiceCollection AddCoreServices(this IServiceCollection services, 13 | IConfiguration configuration) 14 | { 15 | services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); 16 | services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); 17 | 18 | services.AddScoped(); 19 | services.AddScoped(); 20 | services.AddScoped(); 21 | 22 | var catalogSettings = configuration.Get() ?? new CatalogSettings(); 23 | services.AddSingleton(new UriComposer(catalogSettings)); 24 | 25 | services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); 26 | services.AddTransient(); 27 | 28 | return services; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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(cfg => 12 | cfg.RegisterServicesFromAssembly(typeof(BasketViewModelService).Assembly)); 13 | services.AddScoped(); 14 | services.AddScoped(); 15 | services.AddScoped(); 16 | services.Configure(configuration); 17 | services.AddScoped(); 18 | 19 | return services; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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/Dockerfile: -------------------------------------------------------------------------------- 1 | # RUN ALL CONTAINERS FROM ROOT (folder with .sln file): 2 | # docker-compose build 3 | # docker-compose up 4 | # 5 | # RUN JUST THIS CONTAINER FROM ROOT (folder with .sln file): 6 | # docker build --pull -t web -f src/Web/Dockerfile . 7 | # 8 | # RUN COMMAND 9 | # docker run --name eshopweb --rm -it -p 5106:5106 web 10 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 11 | WORKDIR /app 12 | 13 | COPY *.sln . 14 | COPY . . 15 | WORKDIR /app/src/Web 16 | RUN dotnet restore 17 | 18 | RUN dotnet publish -c Release -o out 19 | 20 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime 21 | WORKDIR /app 22 | COPY --from=build /app/src/Web/out ./ 23 | 24 | # Optional: Set this here if not setting it from docker-compose.yml 25 | # ENV ASPNETCORE_ENVIRONMENT Development 26 | 27 | ENTRYPOINT ["dotnet", "Web.dll"] 28 | -------------------------------------------------------------------------------- /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 MediatR; 2 | using Microsoft.eShopWeb.Web.ViewModels; 3 | 4 | namespace Microsoft.eShopWeb.Web.Features.MyOrders; 5 | 6 | public class GetMyOrders : IRequest> 7 | { 8 | public string UserName { get; set; } 9 | 10 | public GetMyOrders(string userName) 11 | { 12 | UserName = userName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Web/Features/MyOrders/GetMyOrdersHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 3 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 4 | using Microsoft.eShopWeb.ApplicationCore.Specifications; 5 | using Microsoft.eShopWeb.Web.ViewModels; 6 | 7 | namespace Microsoft.eShopWeb.Web.Features.MyOrders; 8 | 9 | public class GetMyOrdersHandler : IRequestHandler> 10 | { 11 | private readonly IReadRepository _orderRepository; 12 | 13 | public GetMyOrdersHandler(IReadRepository orderRepository) 14 | { 15 | _orderRepository = orderRepository; 16 | } 17 | 18 | public async Task> Handle(GetMyOrders request, 19 | CancellationToken cancellationToken) 20 | { 21 | var specification = new CustomerOrdersSpecification(request.UserName); 22 | var orders = await _orderRepository.ListAsync(specification, cancellationToken); 23 | 24 | return orders.Select(o => new OrderViewModel 25 | { 26 | OrderDate = o.OrderDate, 27 | OrderNumber = o.Id, 28 | ShippingAddress = o.ShipToAddress, 29 | Total = o.Total() 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 required 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 Microsoft.AspNetCore.Mvc.Rendering; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels; 4 | 5 | public class CatalogIndexViewModel 6 | { 7 | public List CatalogItems { get; set; } = new List(); 8 | public List? Brands { get; set; } = new List(); 9 | public List? Types { get; set; } = new List(); 10 | public int? BrandFilterApplied { get; set; } 11 | public int? TypesFilterApplied { get; set; } 12 | public PaginationInfoViewModel? PaginationInfo { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /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 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels.Manage; 4 | 5 | public class RemoveLoginViewModel 6 | { 7 | [Required] 8 | public string LoginProvider { get; set; } = string.Empty; 9 | [Required] 10 | public string ProviderKey { get; set; } = string.Empty; 11 | } 12 | -------------------------------------------------------------------------------- /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/OrderDetailViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.eShopWeb.Web.ViewModels; 2 | 3 | public class OrderDetailViewModel : OrderViewModel 4 | { 5 | public List OrderItems { get; set; } = new(); 6 | } 7 | -------------------------------------------------------------------------------- /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 Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 2 | 3 | namespace Microsoft.eShopWeb.Web.ViewModels; 4 | 5 | public class OrderViewModel 6 | { 7 | private const string DEFAULT_STATUS = "Pending"; 8 | 9 | public int OrderNumber { get; set; } 10 | public DateTimeOffset OrderDate { get; set; } 11 | public decimal Total { get; set; } 12 | public string Status => DEFAULT_STATUS; 13 | public Address? ShippingAddress { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /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/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/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 | @if (Model.RecoveryCodes != null) 20 | { 21 | @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) 22 | { 23 | @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
24 | } 25 | } 26 |
27 |
28 | © 2023 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.Docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;", 4 | "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;" 5 | }, 6 | "baseUrls": { 7 | "apiBase": "http://localhost:5200/api/", 8 | "webBase": "http://host.docker.internal:5106/" 9 | }, 10 | "Logging": { 11 | "LogLevel": { 12 | "Default": "Debug", 13 | "System": "Information", 14 | "Microsoft": "Information" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | } 21 | -------------------------------------------------------------------------------- /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@3.4.1", 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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Bold.eot -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Bold.woff -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Regular.eot -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Regular.woff -------------------------------------------------------------------------------- /src/Web/wwwroot/fonts/Montserrat-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/fonts/Montserrat-Regular.woff2 -------------------------------------------------------------------------------- /src/Web/wwwroot/images/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/brand.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/cart.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/logout.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/main_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/main_banner.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/main_banner_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/main_banner_text.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/my_orders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/my_orders.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/1.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/10.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/11.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/12.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/2.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/3.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/4.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/5.jpg -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/5.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/6.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/7.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/8.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/src/Web/wwwroot/images/products/9.png -------------------------------------------------------------------------------- /src/Web/wwwroot/images/products/eCatalog-item-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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/dotnet-architecture/eShopOnWeb/4da8212117e87d808d4bbc7da6286fd2147ce606/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 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = new(); 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 | -------------------------------------------------------------------------------- /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 NSubstitute; 7 | using Xunit; 8 | 9 | namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; 10 | 11 | public class DeleteBasket 12 | { 13 | private readonly string _buyerId = "Test buyerId"; 14 | private readonly IRepository _mockBasketRepo = Substitute.For>(); 15 | private readonly IAppLogger _mockLogger = Substitute.For>(); 16 | 17 | [Fact] 18 | public async Task ShouldInvokeBasketRepositoryDeleteAsyncOnce() 19 | { 20 | var basket = new Basket(_buyerId); 21 | basket.AddItem(1, 1.1m, 1); 22 | basket.AddItem(2, 1.1m, 1); 23 | _mockBasketRepo.GetByIdAsync(Arg.Any(), default) 24 | .Returns(basket); 25 | var basketService = new BasketService(_mockBasketRepo, _mockLogger); 26 | 27 | await basketService.DeleteBasketAsync(1); 28 | 29 | await _mockBasketRepo.Received().DeleteAsync(Arg.Any(), default); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 NSubstitute; 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 = Substitute.For(BasketBuyerId); 26 | basketMock.Id.Returns(BasketId); 27 | 28 | _basket = basketMock; 29 | return _basket; 30 | } 31 | 32 | public Basket WithOneBasketItem() 33 | { 34 | var basketMock = Substitute.For(BasketBuyerId); 35 | _basket = basketMock; 36 | _basket.AddItem(2, 3.40m, 4); 37 | return _basket; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Ardalis.Specification; 5 | using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; 6 | using Microsoft.eShopWeb.ApplicationCore.Interfaces; 7 | using Microsoft.eShopWeb.Web.Features.MyOrders; 8 | using NSubstitute; 9 | using Xunit; 10 | 11 | namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests; 12 | 13 | public class GetMyOrders 14 | { 15 | private readonly IReadRepository _mockOrderRepository = Substitute.For>(); 16 | 17 | public GetMyOrders() 18 | { 19 | var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); 20 | var address = new Address("", "", "", "", ""); 21 | Order order = new Order("buyerId", address, new List { item }); 22 | 23 | _mockOrderRepository.ListAsync(Arg.Any>(), default).Returns(new List { order }); 24 | } 25 | 26 | [Fact] 27 | public async Task NotReturnNullIfOrdersArePresIent() 28 | { 29 | var request = new eShopWeb.Web.Features.MyOrders.GetMyOrders("SomeUserName"); 30 | 31 | var handler = new GetMyOrdersHandler(_mockOrderRepository); 32 | 33 | var result = await handler.Handle(request, CancellationToken.None); 34 | 35 | Assert.NotNull(result); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------