Repository: MicrosoftLearning/eShopOnWeb Branch: main Commit: 20179a783d34 Files: 424 Total size: 611.6 KB Directory structure: gitextract_58gu5nma/ ├── .ado/ │ ├── eshoponweb-cd-aci.yml │ ├── eshoponweb-cd-webapp-code.yml │ ├── eshoponweb-cd-webapp-docker.yml │ ├── eshoponweb-cd-webapp-dockercompose.yml │ ├── eshoponweb-cd-windows-cm.yml │ ├── eshoponweb-ci-docker.yml │ ├── eshoponweb-ci-dockercompose.yml │ ├── eshoponweb-ci-mend.yml │ ├── eshoponweb-ci-pr.yml │ ├── eshoponweb-ci.yml │ └── eshoponweb-sonar-ci.yml ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── dotnetcore.yml │ ├── eshoponweb-cicd.yml │ └── richnav.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── CodeCoverage.runsettings ├── Directory.Packages.props ├── LICENSE ├── MTT-Notes.md ├── README.md ├── azure.yaml ├── docker-compose-webapp.yml ├── docker-compose.dcproj ├── docker-compose.override.yml ├── docker-compose.yml ├── eShopOnWeb.sln ├── global.json ├── infra/ │ ├── abbreviations.json │ ├── aci.bicep │ ├── acr.bicep │ ├── core/ │ │ ├── database/ │ │ │ └── sqlserver/ │ │ │ └── sqlserver.bicep │ │ ├── host/ │ │ │ ├── appservice.bicep │ │ │ └── appserviceplan.bicep │ │ └── security/ │ │ ├── keyvault-access.bicep │ │ └── keyvault.bicep │ ├── main.bicep │ ├── main.parameters.json │ ├── simple-windows-vm.bicep │ ├── webapp-docker.bicep │ ├── webapp-to-acr-roleassignment.bicep │ └── webapp.bicep ├── 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 │ │ └── open-iconic/ │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font/ │ │ └── fonts/ │ │ └── open-iconic.otf │ ├── 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 │ │ ├── FeatureDiagnostics.cshtml │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── Privacy.cshtml │ │ ├── Privacy.cshtml.cs │ │ ├── SettingsViewModel.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.scss │ │ ├── app.component.css │ │ ├── app.component.scss │ │ ├── app.css │ │ ├── basket/ │ │ │ ├── basket-status/ │ │ │ │ ├── basket-status.component.css │ │ │ │ └── basket-status.component.scss │ │ │ ├── basket.component.css │ │ │ └── basket.component.scss │ │ ├── catalog/ │ │ │ ├── catalog.component.css │ │ │ ├── catalog.component.scss │ │ │ └── pager.css │ │ ├── orders/ │ │ │ ├── orders.component.css │ │ │ └── orders.component.scss │ │ └── shared/ │ │ └── components/ │ │ ├── header/ │ │ │ ├── header.css │ │ │ └── header.scss │ │ ├── identity/ │ │ │ ├── identity.css │ │ │ └── identity.scss │ │ └── pager/ │ │ ├── pager.css │ │ └── pager.scss │ └── js/ │ └── site.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ado/eshoponweb-cd-aci.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") # trigger: none resources: pipelines: - pipeline: eshoponweb-ci-dockercompose source: eshoponweb-ci-dockercompose # given pipeline name trigger: true repositories: - repository: self trigger: none variables: location: 'centralus' templateFile: 'infra/aci.bicep' subscriptionid: 'YOUR-SUBSCRIPTION-ID' azureserviceconnection: 'azure subs' webappname: 'az400eshop-NAME' acr-login-server: 'YOUR-ACR.azurecr.io' acr-username: 'ACR-USERNAME' resource-group: 'AZ400-EWebShop-NAME' stages: - stage: Deploy displayName: Docker Compose to ACI #variable group referencing KV secret variables: - group: 'eshopweb-vg' jobs: - job: Deploy pool: vmImage: ubuntu-latest steps: # Deploy Azure Container Instance using Bicep - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy ACI Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: '$(azureserviceconnection)' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(resource-group)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: '$(templateFile)' overrideParameters: ' -name $(webappname) -image $(acr-login-server)/eshopwebmvc:latest -server $(acr-login-server) -username $(acr-username) -password $(acr-secret)' deploymentMode: 'Incremental' # deploymentOutputs: 'asp-json' ================================================ FILE: .ado/eshoponweb-cd-webapp-code.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") # Trigger CD when CI executed succesfully resources: pipelines: - pipeline: eshoponweb-ci source: eshoponweb-ci # given pipeline name trigger: true variables: resource-group: 'AZ400-EWebShop-NAME' location: 'centralus' templateFile: 'webapp.bicep' subscriptionid: 'YOUR-SUBSCRIPTION-ID' azureserviceconnection: 'azure subs' webappname: 'az400-webapp-NAME' # webappname: 'webapp-windows-eshop' stages: - stage: Deploy displayName: Deploy to WebApp jobs: - job: Deploy pool: vmImage: windows-latest steps: #download artifacts - download: eshoponweb-ci # Deploy App Service Plan + App Service using Bicep - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy App Service Plan Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: '$(azureserviceconnection)' subscriptionId: '$(subscriptionid)' action: 'Create Or Update Resource Group' resourceGroupName: '$(resource-group)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: '$(Pipeline.Workspace)/eshoponweb-ci/Bicep/$(templateFile)' overrideParameters: '-webAppName $(webappname) -location $(location)' deploymentMode: 'Incremental' deploymentOutputs: 'asp-json' #Publish Website to Azure WebApp - task: AzureRmWebAppDeployment@4 displayName: Publish Website to WebApp inputs: ConnectionType: 'AzureRM' azureSubscription: '$(azureserviceconnection)' appType: 'webApp' WebAppName: '$(webappname)' packageForLinux: '$(Pipeline.Workspace)/eshoponweb-ci/Website/Web.zip' ================================================ FILE: .ado/eshoponweb-cd-webapp-docker.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") resources: repositories: - repository: self trigger: none variables: azureServiceConnection: 'azure subs' resourceGroup: 'rg-az400-container-NAME' location: 'centralus' subscriptionId: 'YOUR-SUBSCRIPTION-ID' stages: - stage: Deploy jobs: - job: Deploy pool: vmImage: ubuntu-latest steps: - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy App Service using Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: $(azureServiceConnection) subscriptionId: $(subscriptionId) action: 'Create Or Update Resource Group' resourceGroupName: '$(resourceGroup)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: 'infra/webapp-docker.bicep' deploymentMode: 'Incremental' - task: AzureResourceManagerTemplateDeployment@3 displayName: Add Role Assignment using Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: $(azureServiceConnection) subscriptionId: $(subscriptionId) action: 'Create Or Update Resource Group' resourceGroupName: '$(resourceGroup)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: 'infra/webapp-to-acr-roleassignment.bicep' deploymentMode: 'Incremental' ================================================ FILE: .ado/eshoponweb-cd-webapp-dockercompose.yml ================================================ #NOT WORKING YET #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") #locally docker compose works, deploying to Azure WebApp is succesful but something still fails # trigger: none resources: repositories: - repository: self trigger: none variables: tag: '$(Build.BuildId)' resource-group: 'AZ400-EWebShop-NAME' location: 'centralus' templateFile: '.ado/IaC/app-plan.bicep' subscriptionid: 'YOUR-SUBSCRIPTION-ID' azureserviceconnection: 'azure subs' webappname: 'az400-webapp-compose-NAME' composeFile: 'docker-compose-webapp.yml' acr-login-server: 'YOUR-ACR.azurecr.io' acr-username: 'YOUR-ACR' stages: - stage: Deploy displayName: Docker Compose to WebApp jobs: - job: Deploy pool: vmImage: ubuntu-latest steps: # Deploy App Service Plan using Bicep - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy App Service Plan Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: $(azureserviceconnection) subscriptionId: $(subscriptionid) action: 'Create Or Update Resource Group' resourceGroupName: '$(resource-group)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: '$(templateFile)' deploymentMode: 'Incremental' deploymentOutputs: 'asp-json' #Parse App Service Plan variable - task: PowerShell@2 displayName: Parse Bicep Output inputs: targetType: 'inline' script: | $var=ConvertFrom-Json '$(asp-json)' $value=$var.asplan.value Write-Host "##vso[task.setvariable variable=appserviceplan;]$value" echo $appserviceplan #Replace tokens in Docker Compose file - task: qetza.replacetokens.replacetokens-task.replacetokens@3 inputs: targetFiles: 'docker-compose-webapp.yml' encoding: 'auto' writeBOM: true actionOnMissing: 'warn' keepToken: false actionOnNoFiles: 'continue' enableTransforms: false tokenPrefix: '__' tokenSuffix: '__' enableRecursion: false useLegacyPattern: false enableTelemetry: true #Docker Login into private ACR (TODO alternative, using Docker task and docker ADO Service Connection) #Deploy using Docker Compose - task: AzureCLI@2 inputs: azureSubscription: '$(azureserviceconnection)' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az webapp create --resource-group $(resource-group) --plan $(appserviceplan) --name $(webappname) --multicontainer-config-type compose --multicontainer-config-file $(composeFile) az webapp config container set --resource-group $(resource-group) --name $(webappname) --docker-registry-server-user $(acr-username) --docker-registry-server-password $(acr-secret) --docker-registry-server-url https://$(acr-login-server) ================================================ FILE: .ado/eshoponweb-cd-windows-cm.yml ================================================ variables: resource-group: 'AZ400-EWebShop-NAME' location: 'centralus' templateFile: 'infra/simple-windows-vm.bicep' azureserviceconnection: 'azure subs' stages: - stage: Deploy displayName: Deploy the Bicep template jobs: - job: Deploy pool: vmImage: ubuntu-latest steps: - checkout: self - task: AzureCLI@2 inputs: azureSubscription: $(azureserviceconnection) scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az group create -l $(location) -n $(resource-group) az deployment group create -f $(templateFile) -g $(resource-group) ================================================ FILE: .ado/eshoponweb-ci-docker.yml ================================================ # NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") resources: repositories: - repository: self trigger: none variables: azureServiceConnection: 'azure subs' subscriptionId: 'YOUR-SUBSCRIPTION-ID' resourceGroup: 'rg-az400-container-NAME' location: 'centralus' stages: - stage: Build jobs: - job: Build pool: vmImage: 'ubuntu-latest' steps: - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy ACR using Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: $(azureServiceConnection) subscriptionId: $(subscriptionId) action: 'Create Or Update Resource Group' resourceGroupName: '$(resourceGroup)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: 'infra/acr.bicep' deploymentMode: 'Incremental' deploymentOutputs: 'outputJson' - task: PowerShell@2 displayName: Parse Bicep Output inputs: targetType: 'inline' script: | $var=ConvertFrom-Json '$(outputJson)' $value=$var.acrLoginServer.value Write-Host "##vso[task.setvariable variable=acrLoginServer;]$value" - task: Docker@0 displayName: 'Build the docker image' inputs: azureSubscription: $(azureServiceConnection) azureContainerRegistry: $(acrLoginServer) dockerFile: 'src/Web/Dockerfile' defaultContext: false context: $(Build.SourcesDirectory) includeLatestTag: true imageName: eshoponweb/web:$(Build.BuildId) - task: Docker@0 displayName: 'Push the docker images' inputs: azureSubscription: $(azureServiceConnection) azureContainerRegistry: $(acrLoginServer) action: 'Push an image' imageName: eshoponweb/web:$(Build.BuildId) includeLatestTag: true ================================================ FILE: .ado/eshoponweb-ci-dockercompose.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") # Docker # Build a Docker image # https://docs.microsoft.com/azure/devops/pipelines/languages/docker # trigger: # - main resources: repositories: - repository: self trigger: none variables: tag: '$(Build.BuildId)' resource-group: 'AZ400-EWebShop-NAME' location: 'centralus' templateFile: 'infra/acr.bicep' azureserviceconnection: 'azure subs' subscriptionId: 'YOUR-SUBSCRIPTION-ID' stages: - stage: Build displayName: Create ACR for images jobs: - job: Build pool: vmImage: ubuntu-latest steps: #Create ACR to keep docker images - task: AzureResourceManagerTemplateDeployment@3 displayName: Deploy ACR Bicep inputs: deploymentScope: 'Resource Group' azureResourceManagerConnection: $(azureserviceconnection) subscriptionId: $(subscriptionId) action: 'Create Or Update Resource Group' resourceGroupName: '$(resource-group)' location: '$(location)' templateLocation: 'Linked artifact' csmFile: '$(templateFile)' deploymentMode: 'Incremental' deploymentOutputs: 'acr-json' #Parse ACR login server variable - task: PowerShell@2 displayName: Parse Bicep Output inputs: targetType: 'inline' script: | $var=ConvertFrom-Json '$(acr-json)' $value=$var.acrloginServer.value Write-Host "##vso[task.setvariable variable=acrloginserver;]$value" echo $acrloginserver # docker compose build images - task: DockerCompose@1 displayName: Build Docker Compose inputs: containerregistrytype: 'Azure Container Registry' azureSubscription: '$(azureserviceconnection)' azureContainerRegistry: '$(acrloginserver)' dockerComposeFile: '**/docker-compose.yml' projectName: action: 'Build services' additionalImageTags: '$(Build.BuildNumber)' includeLatestTag: true - task: DockerCompose@1 displayName: Push Docker Compose inputs: containerregistrytype: 'Azure Container Registry' azureSubscription: $(azureserviceconnection) azureContainerRegistry: '$(acrloginserver)' dockerComposeFile: '**/docker-compose.yml' projectName: action: 'Push services' additionalImageTags: '$(Build.BuildNumber)' includeLatestTag: true ================================================ FILE: .ado/eshoponweb-ci-mend.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") # trigger: # - main resources: repositories: - repository: self trigger: none stages: - stage: Build displayName: Build .Net Core Solution jobs: - job: Build pool: vmImage: ubuntu-latest steps: - task: DotNetCoreCLI@2 displayName: Restore inputs: command: 'restore' projects: '**/*.sln' feedsToUse: 'select' - task: DotNetCoreCLI@2 displayName: Build inputs: command: 'build' projects: '**/*.sln' - task: DotNetCoreCLI@2 displayName: Test inputs: command: 'test' projects: 'tests/UnitTests/*.csproj' - task: WhiteSource@21 inputs: cwd: '$(System.DefaultWorkingDirectory)' projectName: 'eShopOnWeb' - task: DotNetCoreCLI@2 displayName: Publish inputs: command: 'publish' publishWebProjects: true arguments: '-o $(Build.ArtifactStagingDirectory)' - task: PublishBuildArtifacts@1 displayName: Publish Artifacts ADO - Website inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)' artifactName: Website ================================================ FILE: .ado/eshoponweb-ci-pr.yml ================================================ resources: repositories: - repository: self trigger: none stages: - stage: Build displayName: Build .Net Core Solution jobs: - job: Build pool: vmImage: ubuntu-latest steps: - task: DotNetCoreCLI@2 displayName: Restore inputs: command: 'restore' projects: '**/*.sln' feedsToUse: 'select' - task: DotNetCoreCLI@2 displayName: Build inputs: command: 'build' projects: '**/*.sln' - task: DotNetCoreCLI@2 displayName: Test inputs: command: 'test' projects: 'tests/UnitTests/*.csproj' - task: DotNetCoreCLI@2 displayName: Publish inputs: command: 'publish' publishWebProjects: true arguments: '-o $(Build.ArtifactStagingDirectory)' ================================================ FILE: .ado/eshoponweb-ci.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") # trigger: # - main resources: repositories: - repository: self trigger: none stages: - stage: Build displayName: Build .Net Core Solution jobs: - job: Build pool: vmImage: ubuntu-latest steps: - task: DotNetCoreCLI@2 displayName: Restore inputs: command: 'restore' projects: '**/*.sln' feedsToUse: 'select' - task: DotNetCoreCLI@2 displayName: Build inputs: command: 'build' projects: '**/*.sln' - task: DotNetCoreCLI@2 displayName: Test inputs: command: 'test' projects: 'tests/UnitTests/*.csproj' - task: DotNetCoreCLI@2 displayName: Publish inputs: command: 'publish' publishWebProjects: true arguments: '-o $(Build.ArtifactStagingDirectory)' - task: PublishPipelineArtifact@1 displayName: Publish Artifacts ADO - Website inputs: targetPath: '$(Build.ArtifactStagingDirectory)' artifact: 'Website' publishLocation: 'pipeline' - task: PublishPipelineArtifact@1 displayName: Publish Artifacts ADO - Bicep inputs: targetPath: '$(Build.SourcesDirectory)/infra/webapp.bicep' artifact: 'Bicep' publishLocation: 'pipeline' ================================================ FILE: .ado/eshoponweb-sonar-ci.yml ================================================ #NAME THE PIPELINE SAME AS FILE (WITHOUT ".yml") # trigger: # - main resources: repositories: - repository: self trigger: none stages: - stage: Build displayName: Build .Net Core Solution jobs: - job: Build pool: vmImage: ubuntu-latest steps: - checkout: self fetchDepth: 0 - task: DotNetCoreCLI@2 displayName: Restore inputs: command: 'restore' projects: '**/*.sln' feedsToUse: 'select' - task: SonarCloudPrepare@1 inputs: SonarCloud: 'SonarSC' organization: 'Your Sonarcloud org' scannerMode: 'MSBuild' projectKey: 'your sonarcloud project key' projectName: 'your sonarcloud project name' - task: DotNetCoreCLI@2 displayName: Build inputs: command: 'build' projects: '**/*.sln' - task: DotNetCoreCLI@2 displayName: Test inputs: command: 'test' projects: 'tests/UnitTests/*.csproj' - task: SonarCloudAnalyze@1 - task: SonarCloudPublish@1 inputs: pollingTimeoutSec: '300' - task: DotNetCoreCLI@2 displayName: Publish inputs: command: 'publish' publishWebProjects: true arguments: '-o $(Build.ArtifactStagingDirectory)' - task: PublishBuildArtifacts@1 displayName: Publish Artifacts ADO - Website inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)' artifactName: Website ================================================ FILE: .dockerignore ================================================ .dockerignore .env .git .gitignore .vs .vscode */bin */obj **/.toolstarget ================================================ FILE: .editorconfig ================================================ ############################### # Core EditorConfig Options # ############################### root = true # All files [*] indent_style = space # XML project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 # XML config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] indent_size = 2 # Code files [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true charset = utf-8-bom ############################### # .NET Coding Conventions # ############################### [*.{cs,vb}] # Organize usings dotnet_sort_system_directives_first = true # this. preferences dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_event = false:silent # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:silent dotnet_style_predefined_type_for_member_access = true:silent # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent dotnet_style_readonly_field = true:suggestion # Expression-level preferences dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_auto_properties = true:silent dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent ############################### # Naming Conventions # ############################### # Style Definitions dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Use PascalCase for constant fields dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_accessibilities = * dotnet_naming_symbols.constant_fields.required_modifiers = const # Instance fields are camelCase and start with _ dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style dotnet_naming_symbols.instance_fields.applicable_kinds = field dotnet_naming_style.instance_field_style.capitalization = camel_case dotnet_naming_style.instance_field_style.required_prefix = _ ############################### # C# Coding Conventions # ############################### [*.cs] # var preferences csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent csharp_style_var_elsewhere = true:silent # Expression-bodied members csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:silent csharp_style_expression_bodied_indexers = true:silent csharp_style_expression_bodied_accessors = true:silent # Pattern matching preferences csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion # Null-checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion # Expression-level preferences csharp_prefer_braces = true:silent csharp_style_deconstructed_variable_declaration = true:suggestion csharp_prefer_simple_default_expression = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion # Namespaces csharp_style_namespace_declarations = file_scoped:warning ############################### # C# Formatting Rules # ############################### # New line preferences csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = flush_left # Space preferences csharp_space_after_cast = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_after_colon_in_inheritance_clause = true csharp_space_around_binary_operators = before_and_after csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true ############################### # VB Coding Conventions # ############################### [*.vb] # Modifier preferences visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion############################### ###################################### # Configure Nullable Reference Types # ###################################### [{**/*Dto.cs,**/*Request.cs,**/*Response.cs}] # CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. dotnet_diagnostic.CS8618.severity = none ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.{cmd,[cC][mM][dD]} text eol=crlf *.{bat,[bB][aA][tT]} text eol=crlf ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "nuget" directory: "/" schedule: interval: "monthly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" ================================================ FILE: .github/workflows/dotnetcore.yml ================================================ name: eShopOnWeb Build and Test #Triggers (uncomment line below to use it) #on: [push, pull_request, workflow_dispatch] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: "8.0.x" dotnet-quality: "preview" - name: Build with dotnet run: dotnet build ./eShopOnWeb.sln --configuration Release - name: Test with dotnet run: dotnet test ./eShopOnWeb.sln --configuration Release ================================================ FILE: .github/workflows/eshoponweb-cicd.yml ================================================ name: eShopOnWeb Build and Test #Triggers (uncomment line below to use it) #on: [push, workflow_dispatch] #Environment variables https://docs.github.com/en/actions/learn-github-actions/environment-variables env: RESOURCE-GROUP: rg-eshoponweb-NAME LOCATION: westeurope TEMPLATE-FILE: infra/webapp.bicep SUBSCRIPTION-ID: YOUR-SUBS-ID WEBAPP-NAME: eshoponweb-webapp-NAME jobs: #Build, test and publish .net web project in repository buildandtest: runs-on: ubuntu-latest steps: #checkout the repository - uses: actions/checkout@v5 #prepare runner for desired .net version SDK - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: "8.0.x" dotnet-quality: "preview" #Build/Test/Publish the .net project - name: Build with dotnet run: dotnet build ./eShopOnWeb.sln --configuration Release - name: Test with dotnet run: dotnet test ./eShopOnWeb.sln --configuration Release - name: dotnet publish run: | dotnet publish ./src/Web/Web.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp cd ${{env.DOTNET_ROOT}}/myapp zip -r ../app.zip . # upload the published website code artifacts - name: Upload artifact for deployment job uses: actions/upload-artifact@v5 with: name: .net-app path: ${{env.DOTNET_ROOT}}/app.zip # upload the bicep template as artifacts for next job - name: Upload artifact for deployment job uses: actions/upload-artifact@v5 with: name: bicep-template path: ${{ env.TEMPLATE-FILE }} # Use Bicep to deploy infrastructure + Publish webapp deploy: runs-on: ubuntu-latest needs: buildandtest environment: name: "Development" steps: #Download the publish files created in previous job - name: Download artifact from build job uses: actions/download-artifact@v6 with: name: .net-app path: .net-app #Download the bicep templates from previous job - name: Download artifact from build job uses: actions/download-artifact@v6 with: name: bicep-template path: bicep-template #Login in your azure subscription using a service principal (credentials stored as GitHub Secret in repo) - name: Azure Login uses: azure/login@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} # Deploy Azure WebApp using Bicep file - name: deploy uses: azure/arm-deploy@v2 with: subscriptionId: ${{ env.SUBSCRIPTION-ID }} resourceGroupName: ${{ env.RESOURCE-GROUP }} template: bicep-template/webapp.bicep parameters: "webAppName=${{ env.WEBAPP-NAME }} location=${{ env.LOCATION }}" failOnStdErr: false # Publish website to Azure App Service (WebApp) # Step disabled due to issue where the site sometimes can't be found: https://github.com/microsoft/pipelines-appservice-lib/issues/56. Instead deploy using CLI - name: Publish Website to WebApp if: false #Disable step due to comment above uses: Azure/webapps-deploy@v3 with: type: ZIP app-name: ${{ env.WEBAPP-NAME }} package: .net-app/app.zip # Publish website to Azure App Service using CLI (WebApp) - name: Publish Website to WebApp uses: Azure/cli@v2 with: inlineScript: | az webapp deploy --name ${{ env.WEBAPP-NAME }} --resource-group ${{ env.RESOURCE-GROUP }} --src-path .net-app/app.zip --type zip ================================================ FILE: .github/workflows/richnav.yml ================================================ name: eShopOnWeb - Code Index on: workflow_dispatch jobs: build: runs-on: windows-latest steps: - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v5 with: dotnet-version: 8.0.x - name: Build with dotnet run: dotnet build ./Everything.sln --configuration Release /bl - uses: microsoft/RichCodeNavIndexer@v0.1 with: repo-token: ${{ github.token }} languages: "csharp" environment: "internal" ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot **/wwwroot/lib/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml pub/ /src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 /src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 /src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json #Ignore marker-file used to know which docker files we have. .eshopdocker_* .devcontainer .azure ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "ms-dotnettools.csharp", "formulahendry.dotnet-test-explorer", "ms-vscode.vscode-node-azure-pack", "ms-kubernetes-tools.vscode-kubernetes-tools", "redhat.vscode-yaml", "ms-azuretools.azure-dev" ] } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (web)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/src/Web/bin/Debug/net8.0/Web.dll", "args": [], "cwd": "${workspaceFolder}/src/Web", "stopAtEntry": false, // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser "serverReadyAction": { "action": "openExternally", "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" } }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ] } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "label": "build", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/src/Web/Web.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "publish", "command": "dotnet", "type": "process", "args": [ "publish", "${workspaceFolder}/src/Web/Web.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "watch", "command": "dotnet", "type": "process", "args": [ "watch", "run", "${workspaceFolder}/src/Web/Web.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" } ] } ================================================ FILE: CodeCoverage.runsettings ================================================  .*guardclauses\.dll$ .*moq\.dll$ .*unittests\.dll$ .*microsoft.* True True True False ================================================ FILE: Directory.Packages.props ================================================ true net8.0 8.0.0 8.0.1 8.0.0 9.0.0 all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers all runtime; build; native; contentfiles; analyzers ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) .NET Foundation and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MTT-Notes.md ================================================ # App modified For architecture simplicity (and problems related to deploy SQL on free subscriptions), inmemory database option is selected. By default, the project uses a real database. If you want an in memory database, /src/Infrastructure/Dependencies.cs line 13: ``` var useOnlyInMemoryDatabase = true; ``` # ADO Pipelines Located under the folder ".ado", you can find the following YAML pipelines: - **main-ci.yml** : Dotnet CI pipeline. Build + Test + Publish. Upload artifacts for website and webapp bicep file. - **main-ci-containers-compose.yml** : It first creates an ACR and build/push docker containers based on docker compose. WARNING: you need to have an existing RG. - **main-cd-web-aci.yml** : deploys container image on ACI using Bicep template. WARNING: you need to provide ACR username and password (using variable group / key vault).Contributor role is for management plane operations to manage key vaults. It does not allow access to keys, secrets and certificates. **READ/LIST access needed for used Service Principal in ADO VG**. TODO: using managed identity. - **main-cd-web-webapp.yml** : triggered by **main-ci.yml** and deploys app artifacts created by **main-ci.yml** to Azure Web App (linux). It uses bicep to create App Service Plan and WebApp, and publishes code to the webapp. # Deployment options ## Azure App Service single container for only WEB (TODO) Deployment of the Web solution works with the following settings: ![webapp container web](.images/webapp-container-web.png) ================================================ FILE: README.md ================================================ [![Build Status](https://github.com/dotnet-architecture/eShopOnWeb/workflows/eShopOnWeb%20Build%20and%20Test/badge.svg)](https://github.com/dotnet-architecture/eShopOnWeb/actions) # Microsoft eShopOnWeb ASP.NET Core Reference Application Sample ASP.NET Core reference application, powered by Microsoft, demonstrating a single-process (monolithic) application architecture and deployment model. If you're new to .NET development, read the [Getting Started for Beginners](https://github.com/dotnet-architecture/eShopOnWeb/wiki/Getting-Started-for-Beginners) guide. A list of Frequently Asked Questions about this repository can be found [here](https://github.com/dotnet-architecture/eShopOnWeb/wiki/Frequently-Asked-Questions). ## Overview Video [Steve "ardalis" Smith](https://twitter.com/ardalis) recorded [a live stream providing an overview of the eShopOnWeb reference app](https://www.youtube.com/watch?v=vRZ8ucGac8M&ab_channel=Ardalis) in October 2020. ## eBook This reference application is meant to support the free .PDF download ebook: [Architecting Modern Web Applications with ASP.NET Core and Azure](https://aka.ms/webappebook), updated to **ASP.NET Core 8.0**. [Also available in ePub/mobi formats](https://dotnet.microsoft.com/learn/web/aspnet-architecture). You can also read the book in online pages at the .NET docs here: https://docs.microsoft.com/dotnet/architecture/modern-web-apps-azure/ [](https://dotnet.microsoft.com/learn/web/aspnet-architecture) The **eShopOnWeb** sample is related to the [eShopOnContainers](https://github.com/dotnet/eShopOnContainers) sample application which, in that case, focuses on a microservices/containers-based application architecture. However, **eShopOnWeb** is much simpler in regards to its current functionality and focuses on traditional Web Application Development with a single deployment. The goal for this sample is to demonstrate some of the principles and patterns described in the [eBook](https://aka.ms/webappebook). It is not meant to be an eCommerce reference application, and as such it does not implement many features that would be obvious and/or essential to a real eCommerce application. > ### VERSIONS > #### The `main` branch is currently running ASP.NET Core 8.0. > #### Older versions are tagged. ## Topics (eBook TOC) - Introduction - Characteristics of Modern Web Applications - Choosing Between Traditional Web Apps and SPAs - Architectural Principles - Common Web Application Architectures - Common Client Side Technologies - Developing ASP.NET Core MVC Apps - Working with Data in ASP.NET Core Apps - Testing ASP.NET Core MVC Apps - Development Process for Azure-Hosted ASP.NET Core Apps - Azure Hosting Recommendations for ASP.NET Core Web Apps ## Running the sample using Azd template The store's home page should look like this: ![eShopOnWeb home page screenshot](https://user-images.githubusercontent.com/782127/88414268-92d83a00-cdaa-11ea-9b4c-db67d95be039.png) The Azure Developer CLI (`azd`) is a developer-centric command-line interface (CLI) tool for creating Azure applications. You need to install it before running and deploying with Azure Developer CLI. ### Windows ```powershell powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" ``` ### Linux/MacOS ``` curl -fsSL https://aka.ms/install-azd.sh | bash ``` And you can also install with package managers, like winget, choco, and brew. For more details, you can follow the documentation: https://aka.ms/azure-dev/install. After logging in with the following command, you will be able to use the azd cli to quickly provision and deploy the application. ``` azd auth login ``` Then, execute the `azd init` command to initialize the environment. ``` azd init -t dotnet-architecture/eShopOnWeb ``` Run `azd up` to provision all the resources to Azure and deploy the code to those resources. ``` azd up ``` According to the prompt, enter an `env name`, and select `subscription` and `location`, these are the necessary parameters when you create resources. Wait a moment for the resource deployment to complete, click the web endpoint and you will see the home page. **Notes:** 1. Considering security, we store its related data (id, password) in the **Azure Key Vault** when we create the database, and obtain it from the Key Vault when we use it. This is different from directly deploying applications locally. 2. The resource group name created in azure portal will be **rg-{env name}**. You can also run the sample directly locally (See below). ## Running the sample locally Most of the site's functionality works with just the web application running. However, the site's Admin page relies on Blazor WebAssembly running in the browser, and it must communicate with the server using the site's PublicApi web application. You'll need to also run this project. You can configure Visual Studio to start multiple projects, or just go to the PublicApi folder in a terminal window and run `dotnet run` from there. After that from the Web folder you should run `dotnet run --launch-profile Web`. Now you should be able to browse to `https://localhost:5001/`. The admin part in Blazor is accessible to `https://localhost:5001/admin` Note that if you use this approach, you'll need to stop the application manually in order to build the solution (otherwise you'll get file locking errors). After cloning or downloading the sample you must setup your database. To use the sample with a persistent database, you will need to run its Entity Framework Core migrations before you will be able to run the app. You can also run the samples in Docker (see below). ### Configuring the sample to use SQL Server 1. By default, the project uses a real database. If you want an in memory database, you can add in the `appsettings.json` file in the Web folder ```json { "UseOnlyInMemoryDatabase": true } ``` 1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance. 1. Ensure the tool EF was already installed. You can find some help [here](https://docs.microsoft.com/ef/core/miscellaneous/cli/dotnet) ``` dotnet tool update --global dotnet-ef ``` 1. Open a command prompt in the Web folder and execute the following commands: ``` dotnet restore dotnet tool restore dotnet ef database update -c catalogcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj dotnet ef database update -c appidentitydbcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj ``` These commands will create two separate databases, one for the store's catalog data and shopping cart information, and one for the app's user credentials and identity data. 1. Run the application. The first time you run the application, it will seed both databases with data such that you should see products in the store, and you should be able to log in using the demouser@microsoft.com account. Note: If you need to create migrations, you can use these commands: ``` -- create migration (from Web folder CLI) dotnet ef migrations add InitialModel --context catalogcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj -o Data/Migrations dotnet ef migrations add InitialIdentityModel --context appidentitydbcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj -o Identity/Migrations ``` ## Running the sample in the dev container This project includes a `.devcontainer` folder with a [dev container configuration](https://containers.dev/), which lets you use a container as a full-featured dev environment. You can use the dev container to build and run the app without needing to install any of its tools locally! You can work in GitHub Codespaces or the VS Code Dev Containers extension. Learn more about using the dev container in its [readme](/.devcontainer/devcontainerreadme.md). ## Running the sample using Docker You can run the Web sample by running these commands from the root folder (where the .sln file is located): ``` docker-compose build docker-compose up ``` You should be able to make requests to localhost:5106 for the Web project, and localhost:5200 for the Public API project once these commands complete. If you have any problems, especially with login, try from a new guest or incognito browser instance. You can also run the applications by using the instructions located in their `Dockerfile` file in the root of each project. Again, run these commands from the root of the solution (where the .sln file is located). ## Community Extensions We have some great contributions from the community, and while these aren't maintained by Microsoft we still want to highlight them. [eShopOnWeb VB.NET](https://github.com/VBAndCs/eShopOnWeb_VB.NET) by Mohammad Hamdy Ghanem ================================================ FILE: azure.yaml ================================================ name: eShopOnWeb services: web: project: ./src/Web language: csharp host: appservice ================================================ FILE: docker-compose-webapp.yml ================================================ version: '3.4' services: eshopwebmvc: image: __acr-login-server__/eshopwebmvc build: context: . dockerfile: src/Web/Dockerfile depends_on: - "sqlserver" eshoppublicapi: image: __acr-login-server__/eshoppublicapi build: context: . dockerfile: src/PublicApi/Dockerfile depends_on: - "sqlserver" sqlserver: image: mcr.microsoft.com/azure-sql-edge ports: - "1433:1433" environment: - SA_PASSWORD=@someThingComplicated1234 - ACCEPT_EULA=Y ================================================ FILE: docker-compose.dcproj ================================================ 2.1 Linux {1FCBE191-34FE-4B2E-8915-CA81553958AD} True {Scheme}://localhost:{ServicePort} eshopwebmvc docker-compose.yml ================================================ FILE: docker-compose.override.yml ================================================ version: '3.4' services: eshopwebmvc: environment: - ASPNETCORE_ENVIRONMENT=Docker - ASPNETCORE_URLS=http://+:8080 ports: - "5106:8080" volumes: - ~/.aspnet/https:/root/.aspnet/https:ro - ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro eshoppublicapi: environment: - ASPNETCORE_ENVIRONMENT=Docker - ASPNETCORE_URLS=http://+:8080 ports: - "5200:8080" volumes: - ~/.aspnet/https:/root/.aspnet/https:ro - ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro ================================================ FILE: docker-compose.yml ================================================ services: eshopwebmvc: image: ${DOCKER_REGISTRY-}eshopwebmvc build: context: . dockerfile: src/Web/Dockerfile depends_on: - "sqlserver" eshoppublicapi: image: ${DOCKER_REGISTRY-}eshoppublicapi build: context: . dockerfile: src/PublicApi/Dockerfile depends_on: - "sqlserver" sqlserver: image: mcr.microsoft.com/azure-sql-edge ports: - "1433:1433" environment: - SA_PASSWORD=@someThingComplicated1234 - ACCEPT_EULA=Y ================================================ FILE: eShopOnWeb.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{419A6ACE-0419-4315-A6FB-B0E63D39432E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{227CF035-29B0-448D-97E4-944F9EA850E5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{7C461394-ABDC-43CD-A798-71249C58BA67}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationCore", "src\ApplicationCore\ApplicationCore.csproj", "{7FED7440-2311-4D1E-958B-3E887C585CD2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{15EA4737-125B-4E6E-A806-E13B7EBCDCCF}" ProjectSection(SolutionItems) = preProject CodeCoverage.runsettings = CodeCoverage.runsettings EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{0F576306-7E2D-49B7-87B1-EB5D94CFD5FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionalTests", "tests\FunctionalTests\FunctionalTests.csproj", "{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0BD72BEA-EF42-4B72-8B69-12A39EC76FBA}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Packages.props = Directory.Packages.props docker-compose.override.yml = docker-compose.override.yml docker-compose.yml = docker-compose.yml .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml README.md = README.md EndProjectSection EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorAdmin", "src\BlazorAdmin\BlazorAdmin.csproj", "{71368733-80A4-4869-B215-3A7001878577}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorShared", "src\BlazorShared\BlazorShared.csproj", "{715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApiIntegrationTests", "tests\PublicApiIntegrationTests\PublicApiIntegrationTests.csproj", "{D53EF010-8F8C-4337-A059-456E19D8AE63}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.Build.0 = Release|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.Build.0 = Release|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Release|Any CPU.ActiveCfg = Release|Any CPU {7FED7440-2311-4D1E-958B-3E887C585CD2}.Release|Any CPU.Build.0 = Release|Any CPU {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6}.Release|Any CPU.Build.0 = Release|Any CPU {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC}.Release|Any CPU.Build.0 = Release|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.Build.0 = Release|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.Build.0 = Release|Any CPU {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.Build.0 = Release|Any CPU {71368733-80A4-4869-B215-3A7001878577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {71368733-80A4-4869-B215-3A7001878577}.Debug|Any CPU.Build.0 = Debug|Any CPU {71368733-80A4-4869-B215-3A7001878577}.Release|Any CPU.ActiveCfg = Release|Any CPU {71368733-80A4-4869-B215-3A7001878577}.Release|Any CPU.Build.0 = Release|Any CPU {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Release|Any CPU.Build.0 = Release|Any CPU {D53EF010-8F8C-4337-A059-456E19D8AE63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D53EF010-8F8C-4337-A059-456E19D8AE63}.Debug|Any CPU.Build.0 = Debug|Any CPU {D53EF010-8F8C-4337-A059-456E19D8AE63}.Release|Any CPU.ActiveCfg = Release|Any CPU {D53EF010-8F8C-4337-A059-456E19D8AE63}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {7C461394-ABDC-43CD-A798-71249C58BA67} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {7FED7440-2311-4D1E-958B-3E887C585CD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {71368733-80A4-4869-B215-3A7001878577} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {D53EF010-8F8C-4337-A059-456E19D8AE63} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B} EndGlobalSection EndGlobal ================================================ FILE: global.json ================================================ { "sdk": { "version": "8.0.x", "rollForward": "latestFeature" } } ================================================ FILE: infra/abbreviations.json ================================================ { "analysisServicesServers": "as", "apiManagementService": "apim-", "appConfigurationConfigurationStores": "appcs-", "appManagedEnvironments": "cae-", "appContainerApps": "ca-", "authorizationPolicyDefinitions": "policy-", "automationAutomationAccounts": "aa-", "blueprintBlueprints": "bp-", "blueprintBlueprintsArtifacts": "bpa-", "cacheRedis": "redis-", "cdnProfiles": "cdnp-", "cdnProfilesEndpoints": "cdne-", "cognitiveServicesAccounts": "cog-", "cognitiveServicesFormRecognizer": "cog-fr-", "cognitiveServicesTextAnalytics": "cog-ta-", "computeAvailabilitySets": "avail-", "computeCloudServices": "cld-", "computeDiskEncryptionSets": "des", "computeDisks": "disk", "computeDisksOs": "osdisk", "computeGalleries": "gal", "computeSnapshots": "snap-", "computeVirtualMachines": "vm", "computeVirtualMachineScaleSets": "vmss-", "containerInstanceContainerGroups": "ci", "containerRegistryRegistries": "cr", "containerServiceManagedClusters": "aks-", "databricksWorkspaces": "dbw-", "dataFactoryFactories": "adf-", "dataLakeAnalyticsAccounts": "dla", "dataLakeStoreAccounts": "dls", "dataMigrationServices": "dms-", "dBforMySQLServers": "mysql-", "dBforPostgreSQLServers": "psql-", "devicesIotHubs": "iot-", "devicesProvisioningServices": "provs-", "devicesProvisioningServicesCertificates": "pcert-", "documentDBDatabaseAccounts": "cosmos-", "eventGridDomains": "evgd-", "eventGridDomainsTopics": "evgt-", "eventGridEventSubscriptions": "evgs-", "eventHubNamespaces": "evhns-", "eventHubNamespacesEventHubs": "evh-", "hdInsightClustersHadoop": "hadoop-", "hdInsightClustersHbase": "hbase-", "hdInsightClustersKafka": "kafka-", "hdInsightClustersMl": "mls-", "hdInsightClustersSpark": "spark-", "hdInsightClustersStorm": "storm-", "hybridComputeMachines": "arcs-", "insightsActionGroups": "ag-", "insightsComponents": "appi-", "keyVaultVaults": "kv-", "kubernetesConnectedClusters": "arck", "kustoClusters": "dec", "kustoClustersDatabases": "dedb", "logicIntegrationAccounts": "ia-", "logicWorkflows": "logic-", "machineLearningServicesWorkspaces": "mlw-", "managedIdentityUserAssignedIdentities": "id-", "managementManagementGroups": "mg-", "migrateAssessmentProjects": "migr-", "networkApplicationGateways": "agw-", "networkApplicationSecurityGroups": "asg-", "networkAzureFirewalls": "afw-", "networkBastionHosts": "bas-", "networkConnections": "con-", "networkDnsZones": "dnsz-", "networkExpressRouteCircuits": "erc-", "networkFirewallPolicies": "afwp-", "networkFirewallPoliciesWebApplication": "waf", "networkFirewallPoliciesRuleGroups": "wafrg", "networkFrontDoors": "fd-", "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", "networkLoadBalancersExternal": "lbe-", "networkLoadBalancersInternal": "lbi-", "networkLoadBalancersInboundNatRules": "rule-", "networkLocalNetworkGateways": "lgw-", "networkNatGateways": "ng-", "networkNetworkInterfaces": "nic-", "networkNetworkSecurityGroups": "nsg-", "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", "networkNetworkWatchers": "nw-", "networkPrivateDnsZones": "pdnsz-", "networkPrivateLinkServices": "pl-", "networkPublicIPAddresses": "pip-", "networkPublicIPPrefixes": "ippre-", "networkRouteFilters": "rf-", "networkRouteTables": "rt-", "networkRouteTablesRoutes": "udr-", "networkTrafficManagerProfiles": "traf-", "networkVirtualNetworkGateways": "vgw-", "networkVirtualNetworks": "vnet-", "networkVirtualNetworksSubnets": "snet-", "networkVirtualNetworksVirtualNetworkPeerings": "peer-", "networkVirtualWans": "vwan-", "networkVpnGateways": "vpng-", "networkVpnGatewaysVpnConnections": "vcn-", "networkVpnGatewaysVpnSites": "vst-", "notificationHubsNamespaces": "ntfns-", "notificationHubsNamespacesNotificationHubs": "ntf-", "operationalInsightsWorkspaces": "log-", "portalDashboards": "dash-", "powerBIDedicatedCapacities": "pbi-", "purviewAccounts": "pview-", "recoveryServicesVaults": "rsv-", "resourcesResourceGroups": "rg-", "searchSearchServices": "srch-", "serviceBusNamespaces": "sb-", "serviceBusNamespacesQueues": "sbq-", "serviceBusNamespacesTopics": "sbt-", "serviceEndPointPolicies": "se-", "serviceFabricClusters": "sf-", "signalRServiceSignalR": "sigr", "sqlManagedInstances": "sqlmi-", "sqlServers": "sql-", "sqlServersDataWarehouse": "sqldw-", "sqlServersDatabases": "sqldb-", "sqlServersDatabasesStretch": "sqlstrdb-", "storageStorageAccounts": "st", "storageStorageAccountsVm": "stvm", "storSimpleManagers": "ssimp", "streamAnalyticsCluster": "asa-", "synapseWorkspaces": "syn", "synapseWorkspacesAnalyticsWorkspaces": "synw", "synapseWorkspacesSqlPoolsDedicated": "syndp", "synapseWorkspacesSqlPoolsSpark": "synsp", "timeSeriesInsightsEnvironments": "tsi-", "webServerFarms": "plan-", "webSitesAppService": "app-", "webSitesAppServiceEnvironment": "ase-", "webSitesFunctions": "func-", "webStaticSites": "stapp-" } ================================================ FILE: infra/aci.bicep ================================================ @description('Name for the container group') param name string = 'eshopcontainer' @description('Location for all resources.') param location string = resourceGroup().location @description('Container image to deploy. Should be of the form repoName/imagename:tag for images stored in public Docker Hub, or a fully qualified URI for other registries. Images from private registries require additional registry credentials.') param image string = 'mcr.microsoft.com/azuredocs/aci-helloworld' @description('Port to open on the container and the public IP address.') param port int = 5106 @description('The number of CPU cores to allocate to the container.') param cpuCores int = 1 @description('The amount of memory to allocate to the container in gigabytes.') param memoryInGb int = 2 @description('The behavior of Azure runtime if container has stopped.') @allowed([ 'Always' 'Never' 'OnFailure' ]) param restartPolicy string = 'Always' @secure() param password string param username string param server string resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2021-09-01' = { name: name location: location properties: { containers: [ { name: name properties: { image: image ports: [ { port: port protocol: 'TCP' } { port: 80 protocol: 'TCP' } ] resources: { requests: { cpu: cpuCores memoryInGB: memoryInGb } } environmentVariables: [ { name: 'ASPNETCORE_ENVIRONMENT' value: 'Docker' } { name: 'UseOnlyInMemoryDatabase' value: 'true' } { name: 'ASPNETCORE_HTTP_PORTS' value: '80' } ] } } ] osType: 'Linux' restartPolicy: restartPolicy ipAddress: { type: 'Public' ports: [ { port: port protocol: 'TCP' } { port: 80 protocol: 'TCP' } ] } imageRegistryCredentials: [ { password: password server: server username: username } ] } } output containerIPv4Address string = containerGroup.properties.ipAddress.ip ================================================ FILE: infra/acr.bicep ================================================ @description('Generate a Suffix based on the Resource Group ID') param suffix string = uniqueString(resourceGroup().id) @description('Use the Resource Group Location') param location string = resourceGroup().location resource acr 'Microsoft.ContainerRegistry/registries@2021-09-01' = { name: 'cr${suffix}' location: location sku: { name: 'Basic' } properties: { adminUserEnabled: false } } @description('Output the login server property for later use') output acrLoginServer string = acr.properties.loginServer ================================================ FILE: infra/core/database/sqlserver/sqlserver.bicep ================================================ param name string param location string = resourceGroup().location param tags object = {} param appUser string = 'appUser' param databaseName string param keyVaultName string param sqlAdmin string = 'sqlAdmin' param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' @secure() param sqlAdminPassword string @secure() param appUserPassword string resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { name: name location: location tags: tags properties: { version: '12.0' minimalTlsVersion: '1.2' publicNetworkAccess: 'Enabled' administratorLogin: sqlAdmin administratorLoginPassword: sqlAdminPassword } resource database 'databases' = { name: databaseName location: location } resource firewall 'firewallRules' = { name: 'Azure Services' properties: { // Allow all clients // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. startIpAddress: '0.0.0.1' endIpAddress: '255.255.255.254' } } } resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { name: '${name}-deployment-script' location: location kind: 'AzureCLI' properties: { azCliVersion: '2.37.0' retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running timeout: 'PT5M' // Five minutes cleanupPreference: 'OnSuccess' environmentVariables: [ { name: 'APPUSERNAME' value: appUser } { name: 'APPUSERPASSWORD' secureValue: appUserPassword } { name: 'DBNAME' value: databaseName } { name: 'DBSERVER' value: sqlServer.properties.fullyQualifiedDomainName } { name: 'SQLCMDPASSWORD' secureValue: sqlAdminPassword } { name: 'SQLADMIN' value: sqlAdmin } ] scriptContent: ''' wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . cat < ./initDb.sql drop user ${APPUSERNAME} go create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' go alter role db_owner add member ${APPUSERNAME} go SCRIPT_END ./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql ''' } } resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { parent: keyVault name: 'sqlAdminPassword' properties: { value: sqlAdminPassword } } resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { parent: keyVault name: 'appUserPassword' properties: { value: appUserPassword } } resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { parent: keyVault name: connectionStringKey properties: { value: '${connectionString}; Password=${appUserPassword}' } } resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' output connectionStringKey string = connectionStringKey output databaseName string = sqlServer::database.name ================================================ FILE: infra/core/host/appservice.bicep ================================================ param name string param location string = resourceGroup().location param tags object = {} // Reference Properties param applicationInsightsName string = '' param appServicePlanId string param keyVaultName string = '' param managedIdentity bool = !empty(keyVaultName) // Runtime Properties @allowed([ 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' ]) param runtimeName string param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' param runtimeVersion string // Microsoft.Web/sites Properties param kind string = 'app,linux' // Microsoft.Web/sites/config param allowedOrigins array = [] param alwaysOn bool = true param appCommandLine string = '' param appSettings object = {} param clientAffinityEnabled bool = false param enableOryxBuild bool = contains(kind, 'linux') param functionAppScaleLimit int = -1 param linuxFxVersion string = runtimeNameAndVersion param minimumElasticInstanceCount int = -1 param numberOfWorkers int = -1 param scmDoBuildDuringDeployment bool = false param use32BitWorkerProcess bool = false param ftpsState string = 'FtpsOnly' param healthCheckPath string = '' resource appService 'Microsoft.Web/sites@2022-03-01' = { name: name location: location tags: tags kind: kind properties: { serverFarmId: appServicePlanId siteConfig: { linuxFxVersion: linuxFxVersion alwaysOn: alwaysOn ftpsState: ftpsState minTlsVersion: '1.2' appCommandLine: appCommandLine numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null use32BitWorkerProcess: use32BitWorkerProcess functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null healthCheckPath: healthCheckPath cors: { allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) } } clientAffinityEnabled: clientAffinityEnabled httpsOnly: true } identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } resource configAppSettings 'config' = { name: 'appsettings' properties: union(appSettings, { SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) ENABLE_ORYX_BUILD: string(enableOryxBuild) }, !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) } resource configLogs 'config' = { name: 'logs' properties: { applicationLogs: { fileSystem: { level: 'Verbose' } } detailedErrorMessages: { enabled: true } failedRequestsTracing: { enabled: true } httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } } dependsOn: [ configAppSettings ] } } resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { name: keyVaultName } resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { name: applicationInsightsName } output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' output name string = appService.name output uri string = 'https://${appService.properties.defaultHostName}' ================================================ FILE: infra/core/host/appserviceplan.bicep ================================================ param name string param location string = resourceGroup().location param tags object = {} param kind string = '' param reserved bool = true param sku object resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { name: name location: location tags: tags sku: sku kind: kind properties: { reserved: reserved } } output id string = appServicePlan.id ================================================ FILE: infra/core/security/keyvault-access.bicep ================================================ param name string = 'add' param keyVaultName string param permissions object = { secrets: [ 'get', 'list' ] } param principalId string resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { parent: keyVault name: name properties: { accessPolicies: [ { objectId: principalId tenantId: subscription().tenantId permissions: permissions } ] } } resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } ================================================ FILE: infra/core/security/keyvault.bicep ================================================ param name string param location string = resourceGroup().location param tags object = {} param principalId string = '' resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { name: name location: location tags: tags properties: { tenantId: subscription().tenantId sku: { family: 'A', name: 'standard' } accessPolicies: !empty(principalId) ? [ { objectId: principalId permissions: { secrets: [ 'get', 'list' ] } tenantId: subscription().tenantId } ] : [] } } output endpoint string = keyVault.properties.vaultUri output name string = keyVault.name ================================================ FILE: infra/main.bicep ================================================ targetScope = 'subscription' @minLength(1) @maxLength(64) @description('Name of the the environment which is used to generate a short unique hash used in all resources.') param environmentName string @minLength(1) @description('Primary location for all resources') param location string // Optional parameters to override the default azd resource naming conventions. Update the main.parameters.json file to provide values. e.g.,: // "resourceGroupName": { // "value": "myGroupName" // } param resourceGroupName string = '' param webServiceName string = '' param catalogDatabaseName string = 'catalogDatabase' param catalogDatabaseServerName string = '' param identityDatabaseName string = 'identityDatabase' param identityDatabaseServerName string = '' param appServicePlanName string = '' param keyVaultName string = '' @description('Id of the user or app to assign application roles') param principalId string = '' @secure() @description('SQL Server administrator password') param sqlAdminPassword string @secure() @description('Application user password') param appUserPassword string var abbrs = loadJsonContent('./abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' location: location tags: tags } // The application frontend module web './core/host/appservice.bicep' = { name: 'web' scope: rg params: { name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' location: location appServicePlanId: appServicePlan.outputs.id keyVaultName: keyVault.outputs.name runtimeName: 'dotnetcore' runtimeVersion: '8.0' tags: union(tags, { 'azd-service-name': 'web' }) appSettings: { AZURE_SQL_CATALOG_CONNECTION_STRING_KEY: 'AZURE-SQL-CATALOG-CONNECTION-STRING' AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY: 'AZURE-SQL-IDENTITY-CONNECTION-STRING' AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.endpoint } } } module apiKeyVaultAccess './core/security/keyvault-access.bicep' = { name: 'api-keyvault-access' scope: rg params: { keyVaultName: keyVault.outputs.name principalId: web.outputs.identityPrincipalId } } // The application database: Catalog module catalogDb './core/database/sqlserver/sqlserver.bicep' = { name: 'sql-catalog' scope: rg params: { name: !empty(catalogDatabaseServerName) ? catalogDatabaseServerName : '${abbrs.sqlServers}catalog-${resourceToken}' databaseName: catalogDatabaseName location: location tags: tags sqlAdminPassword: sqlAdminPassword appUserPassword: appUserPassword keyVaultName: keyVault.outputs.name connectionStringKey: 'AZURE-SQL-CATALOG-CONNECTION-STRING' } } // The application database: Identity module identityDb './core/database/sqlserver/sqlserver.bicep' = { name: 'sql-identity' scope: rg params: { name: !empty(identityDatabaseServerName) ? identityDatabaseServerName : '${abbrs.sqlServers}identity-${resourceToken}' databaseName: identityDatabaseName location: location tags: tags sqlAdminPassword: sqlAdminPassword appUserPassword: appUserPassword keyVaultName: keyVault.outputs.name connectionStringKey: 'AZURE-SQL-IDENTITY-CONNECTION-STRING' } } // Store secrets in a keyvault module keyVault './core/security/keyvault.bicep' = { name: 'keyvault' scope: rg params: { name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' location: location tags: tags principalId: principalId } } // Create an App Service Plan to group applications under the same payment plan and SKU module appServicePlan './core/host/appserviceplan.bicep' = { name: 'appserviceplan' scope: rg params: { name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' location: location tags: tags sku: { name: 'B1' } } } // Data outputs output AZURE_SQL_CATALOG_CONNECTION_STRING_KEY string = catalogDb.outputs.connectionStringKey output AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY string = identityDb.outputs.connectionStringKey output AZURE_SQL_CATALOG_DATABASE_NAME string = catalogDb.outputs.databaseName output AZURE_SQL_IDENTITY_DATABASE_NAME string = identityDb.outputs.databaseName // App outputs output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name ================================================ FILE: infra/main.parameters.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "environmentName": { "value": "${AZURE_ENV_NAME}" }, "location": { "value": "${AZURE_LOCATION}" }, "principalId": { "value": "${AZURE_PRINCIPAL_ID}" }, "sqlAdminPassword": { "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} sqlAdminPassword)" }, "appUserPassword": { "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} appUserPassword)" } } } ================================================ FILE: infra/simple-windows-vm.bicep ================================================ @description('Username for the Virtual Machine.') param adminUsername string = 'Student' @description('Password for the Virtual Machine.') @minLength(12) @secure() param adminPassword string = 'P@s${uniqueString(newGuid())}!' @description('Unique DNS Name for the Public IP used to access the Virtual Machine.') param dnsLabelPrefix string = toLower('${vmName}-${uniqueString(resourceGroup().id, vmName)}') @description('Name for the Public IP used to access the Virtual Machine.') param publicIpName string = 'myPublicIP' @description('Allocation method for the Public IP used to access the Virtual Machine.') @allowed([ 'Dynamic' 'Static' ]) param publicIPAllocationMethod string = 'Dynamic' @description('SKU for the Public IP used to access the Virtual Machine.') @allowed([ 'Basic' 'Standard' ]) param publicIpSku string = 'Basic' @description('The Windows version for the VM. This will pick a fully patched image of this given Windows version.') @allowed([ '2016-datacenter-gensecond' '2016-datacenter-server-core-g2' '2016-datacenter-server-core-smalldisk-g2' '2016-datacenter-smalldisk-g2' '2016-datacenter-with-containers-g2' '2016-datacenter-zhcn-g2' '2019-datacenter-core-g2' '2019-datacenter-core-smalldisk-g2' '2019-datacenter-core-with-containers-g2' '2019-datacenter-core-with-containers-smalldisk-g2' '2019-datacenter-gensecond' '2019-datacenter-smalldisk-g2' '2019-datacenter-with-containers-g2' '2019-datacenter-with-containers-smalldisk-g2' '2019-datacenter-zhcn-g2' '2022-datacenter-azure-edition' '2022-datacenter-azure-edition-core' '2022-datacenter-azure-edition-core-smalldisk' '2022-datacenter-azure-edition-smalldisk' '2022-datacenter-core-g2' '2022-datacenter-core-smalldisk-g2' '2022-datacenter-g2' '2022-datacenter-smalldisk-g2' ]) param OSVersion string = '2022-datacenter-azure-edition' @description('Size of the virtual machine.') param vmSize string = 'Standard_B1ms' @description('Location for all resources.') param location string = resourceGroup().location @description('Name of the virtual machine.') param vmName string = 'simple-vm' @description('Security Type of the Virtual Machine.') @allowed([ 'Standard' 'TrustedLaunch' ]) param securityType string = 'TrustedLaunch' var storageAccountName = 'bootdiags${uniqueString(resourceGroup().id)}' var nicName = 'myVMNic' var addressPrefix = '10.0.0.0/16' var subnetName = 'Subnet' var subnetPrefix = '10.0.0.0/24' var virtualNetworkName = 'MyVNET' var networkSecurityGroupName = 'default-NSG' var securityProfileJson = { uefiSettings: { secureBootEnabled: true vTpmEnabled: true } securityType: securityType } var extensionName = 'GuestAttestation' var extensionPublisher = 'Microsoft.Azure.Security.WindowsAttestation' var extensionVersion = '1.0' var maaTenantName = 'GuestAttestation' var maaEndpoint = substring('emptyString', 0, 0) resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = { name: storageAccountName location: location sku: { name: 'Standard_LRS' } kind: 'Storage' } resource publicIp 'Microsoft.Network/publicIPAddresses@2022-05-01' = { name: publicIpName location: location sku: { name: publicIpSku } properties: { publicIPAllocationMethod: publicIPAllocationMethod dnsSettings: { domainNameLabel: dnsLabelPrefix } } } resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2022-05-01' = { name: networkSecurityGroupName location: location properties: { securityRules: [ { name: 'default-allow-3389' properties: { priority: 1000 access: 'Allow' direction: 'Inbound' destinationPortRange: '3389' protocol: 'Tcp' sourcePortRange: '*' sourceAddressPrefix: '*' destinationAddressPrefix: '*' } } ] } } resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-05-01' = { name: virtualNetworkName location: location properties: { addressSpace: { addressPrefixes: [ addressPrefix ] } subnets: [ { name: subnetName properties: { addressPrefix: subnetPrefix networkSecurityGroup: { id: networkSecurityGroup.id } } } ] } } resource nic 'Microsoft.Network/networkInterfaces@2022-05-01' = { name: nicName location: location properties: { ipConfigurations: [ { name: 'ipconfig1' properties: { privateIPAllocationMethod: 'Dynamic' publicIPAddress: { id: publicIp.id } subnet: { id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName) } } } ] } dependsOn: [ virtualNetwork ] } resource vm 'Microsoft.Compute/virtualMachines@2022-03-01' = { name: vmName location: location properties: { hardwareProfile: { vmSize: vmSize } osProfile: { computerName: vmName adminUsername: adminUsername adminPassword: adminPassword } storageProfile: { imageReference: { publisher: 'MicrosoftWindowsServer' offer: 'WindowsServer' sku: OSVersion version: 'latest' } osDisk: { createOption: 'FromImage' managedDisk: { storageAccountType: 'StandardSSD_LRS' } } dataDisks: [ { diskSizeGB: 1023 lun: 0 createOption: 'Empty' } ] } networkProfile: { networkInterfaces: [ { id: nic.id } ] } diagnosticsProfile: { bootDiagnostics: { enabled: true storageUri: storageAccount.properties.primaryEndpoints.blob } } securityProfile: ((securityType == 'TrustedLaunch') ? securityProfileJson : null) } } resource vmExtension 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = if ((securityType == 'TrustedLaunch') && ((securityProfileJson.uefiSettings.secureBootEnabled == true) && (securityProfileJson.uefiSettings.vTpmEnabled == true))) { parent: vm name: extensionName location: location properties: { publisher: extensionPublisher type: extensionName typeHandlerVersion: extensionVersion autoUpgradeMinorVersion: true enableAutomaticUpgrade: true settings: { AttestationConfig: { MaaSettings: { maaEndpoint: maaEndpoint maaTenantName: maaTenantName } } } } } output hostname string = publicIp.properties.dnsSettings.fqdn ================================================ FILE: infra/webapp-docker.bicep ================================================ @description('Generate a Suffix based on the Resource Group ID') param suffix string = uniqueString(resourceGroup().id) @description('Use the Resource Group Location') param location string = resourceGroup().location resource acr 'Microsoft.ContainerRegistry/registries@2021-09-01' existing = { name: 'cr${suffix}' } resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { name: 'asp-${suffix}' location: location kind: 'linux' properties: { reserved: true } sku: { name: 'B1' } } resource webApp 'Microsoft.Web/sites@2022-03-01' = { name: 'app-${suffix}' location: location tags: {} properties: { siteConfig: { acrUseManagedIdentityCreds: true appSettings: [ { name: 'UseOnlyInMemoryDatabase' value: 'true' } { name: 'ASPNETCORE_ENVIRONMENT' value: 'Docker' } { name: 'ASPNETCORE_HTTP_PORTS' value: '80' } ] linuxFxVersion: 'DOCKER|${acr.properties.loginServer}/eshoponweb/web:latest' } serverFarmId: appServicePlan.id } identity: { type: 'SystemAssigned' } } ================================================ FILE: infra/webapp-to-acr-roleassignment.bicep ================================================ @description('Generate a Suffix based on the Resource Group ID') param suffix string = uniqueString(resourceGroup().id) @description('Set the ACR Pull Role Definition ID') param acrPullRoleDefinitionID string = '7f951dda-4ed3-4680-a7ca-43fe172d538d' @description('Generate a unique GUID to use as name for the role assignment') var webAppToAcrRoleAssignmentName = guid(webApp.id, acrPullRoleDefinitionID, acr.id) resource acr 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { name: 'cr${suffix}' } resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { name: 'app-${suffix}' } resource webAppToAcrRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: acr name: webAppToAcrRoleAssignmentName properties: { roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleDefinitionID) principalId: webApp.identity.principalId } } ================================================ FILE: infra/webapp.bicep ================================================ param webAppName string // = uniqueString(resourceGroup().id) // unique String gets created from az cli instructions param sku string = 'S1' // The SKU of App Service Plan param location string = resourceGroup().location var appServicePlanName = toLower('AppServicePlan-${webAppName}') resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { name: appServicePlanName location: location properties: { reserved: true } sku: { name: sku } } resource appService 'Microsoft.Web/sites@2022-09-01' = { name: webAppName kind: 'app' location: location properties: { serverFarmId: appServicePlan.id siteConfig: { linuxFxVersion: 'DOTNETCORE|8.0' appSettings: [ { name: 'ASPNETCORE_ENVIRONMENT' value: 'Development' } { name: 'UseOnlyInMemoryDatabase' value: 'true' } ] } } } ================================================ FILE: src/ApplicationCore/ApplicationCore.csproj ================================================  Microsoft.eShopWeb.ApplicationCore enable ================================================ FILE: src/ApplicationCore/CatalogSettings.cs ================================================ namespace Microsoft.eShopWeb; public class CatalogSettings { public string? CatalogBaseUrl { get; set; } } ================================================ FILE: src/ApplicationCore/Constants/AuthorizationConstants.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Constants; public class AuthorizationConstants { public const string AUTH_KEY = "AuthKeyOfDoomThatMustBeAMinimumNumberOfBytes"; // TODO: Don't use this in production public const string DEFAULT_PASSWORD = "Pass@word1"; // TODO: Change this to an environment variable public const string JWT_SECRET_KEY = "SecretKeyOfDoomThatMustBeAMinimumNumberOfBytes"; } ================================================ FILE: src/ApplicationCore/Entities/BaseEntity.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Entities; // This can easily be modified to be BaseEntity and public T Id to support different key types. // Using non-generic integer types for simplicity and to ease caching logic public abstract class BaseEntity { public virtual int Id { get; protected set; } } ================================================ FILE: src/ApplicationCore/Entities/BasketAggregate/Basket.cs ================================================ using System.Collections.Generic; using System.Linq; using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; public class Basket : BaseEntity, IAggregateRoot { public string BuyerId { get; private set; } private readonly List _items = new List(); public IReadOnlyCollection Items => _items.AsReadOnly(); public int TotalItems => _items.Sum(i => i.Quantity); public Basket(string buyerId) { BuyerId = buyerId; } public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1) { if (!Items.Any(i => i.CatalogItemId == catalogItemId)) { _items.Add(new BasketItem(catalogItemId, quantity, unitPrice)); return; } var existingItem = Items.First(i => i.CatalogItemId == catalogItemId); existingItem.AddQuantity(quantity); } public void RemoveEmptyItems() { _items.RemoveAll(i => i.Quantity == 0); } public void SetNewBuyerId(string buyerId) { BuyerId = buyerId; } } ================================================ FILE: src/ApplicationCore/Entities/BasketAggregate/BasketItem.cs ================================================ using Ardalis.GuardClauses; namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; public class BasketItem : BaseEntity { public decimal UnitPrice { get; private set; } public int Quantity { get; private set; } public int CatalogItemId { get; private set; } public int BasketId { get; private set; } public BasketItem(int catalogItemId, int quantity, decimal unitPrice) { CatalogItemId = catalogItemId; UnitPrice = unitPrice; SetQuantity(quantity); } public void AddQuantity(int quantity) { Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue); Quantity += quantity; } public void SetQuantity(int quantity) { Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue); Quantity = quantity; } } ================================================ FILE: src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs ================================================ using System.Collections.Generic; using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate; public class Buyer : BaseEntity, IAggregateRoot { public string IdentityGuid { get; private set; } private List _paymentMethods = new List(); public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); #pragma warning disable CS8618 // Required by Entity Framework private Buyer() { } public Buyer(string identity) : this() { Guard.Against.NullOrEmpty(identity, nameof(identity)); IdentityGuid = identity; } } ================================================ FILE: src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate; public class PaymentMethod : BaseEntity { public string? Alias { get; private set; } public string? CardId { get; private set; } // actual card data must be stored in a PCI compliant system, like Stripe public string? Last4 { get; private set; } } ================================================ FILE: src/ApplicationCore/Entities/CatalogBrand.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities; public class CatalogBrand : BaseEntity, IAggregateRoot { public string Brand { get; private set; } public CatalogBrand(string brand) { Brand = brand; } } ================================================ FILE: src/ApplicationCore/Entities/CatalogItem.cs ================================================ using System; using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities; public class CatalogItem : BaseEntity, IAggregateRoot { public string Name { get; private set; } public string Description { get; private set; } public decimal Price { get; private set; } public string PictureUri { get; private set; } public int CatalogTypeId { get; private set; } public CatalogType? CatalogType { get; private set; } public int CatalogBrandId { get; private set; } public CatalogBrand? CatalogBrand { get; private set; } public CatalogItem(int catalogTypeId, int catalogBrandId, string description, string name, decimal price, string pictureUri) { CatalogTypeId = catalogTypeId; CatalogBrandId = catalogBrandId; Description = description; Name = name; Price = price; PictureUri = pictureUri; } public void UpdateDetails(CatalogItemDetails details) { Guard.Against.NullOrEmpty(details.Name, nameof(details.Name)); Guard.Against.NullOrEmpty(details.Description, nameof(details.Description)); Guard.Against.NegativeOrZero(details.Price, nameof(details.Price)); Name = details.Name; Description = details.Description; Price = details.Price; } public void UpdateBrand(int catalogBrandId) { Guard.Against.Zero(catalogBrandId, nameof(catalogBrandId)); CatalogBrandId = catalogBrandId; } public void UpdateType(int catalogTypeId) { Guard.Against.Zero(catalogTypeId, nameof(catalogTypeId)); CatalogTypeId = catalogTypeId; } public void UpdatePictureUri(string pictureName) { if (string.IsNullOrEmpty(pictureName)) { PictureUri = string.Empty; return; } PictureUri = $"images\\products\\{pictureName}?{new DateTime().Ticks}"; } public readonly record struct CatalogItemDetails { public string? Name { get; } public string? Description { get; } public decimal Price { get; } public CatalogItemDetails(string? name, string? description, decimal price) { Name = name; Description = description; Price = price; } } } ================================================ FILE: src/ApplicationCore/Entities/CatalogType.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities; public class CatalogType : BaseEntity, IAggregateRoot { public string Type { get; private set; } public CatalogType(string type) { Type = type; } } ================================================ FILE: src/ApplicationCore/Entities/EshopDiagram.cd ================================================  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAA= Entities\CatalogBrand.cs AAgAAAAAA4AgAwAAAAAAAAQAAAEAAAAAAAAAAQAACQA= Entities\CatalogItem.cs AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA= Entities\CatalogType.cs ================================================ FILE: src/ApplicationCore/Entities/OrderAggregate/Address.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; public class Address // ValueObject { public string Street { get; private set; } public string City { get; private set; } public string State { get; private set; } public string Country { get; private set; } public string ZipCode { get; private set; } #pragma warning disable CS8618 // Required by Entity Framework private Address() { } public Address(string street, string city, string state, string country, string zipcode) { Street = street; City = city; State = state; Country = country; ZipCode = zipcode; } } ================================================ FILE: src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs ================================================ using Ardalis.GuardClauses; namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; /// /// Represents a snapshot of the item that was ordered. If catalog item details change, details of /// the item that was part of a completed order should not change. /// public class CatalogItemOrdered // ValueObject { public CatalogItemOrdered(int catalogItemId, string productName, string pictureUri) { Guard.Against.OutOfRange(catalogItemId, nameof(catalogItemId), 1, int.MaxValue); Guard.Against.NullOrEmpty(productName, nameof(productName)); Guard.Against.NullOrEmpty(pictureUri, nameof(pictureUri)); CatalogItemId = catalogItemId; ProductName = productName; PictureUri = pictureUri; } #pragma warning disable CS8618 // Required by Entity Framework private CatalogItemOrdered() {} public int CatalogItemId { get; private set; } public string ProductName { get; private set; } public string PictureUri { get; private set; } } ================================================ FILE: src/ApplicationCore/Entities/OrderAggregate/Order.cs ================================================ using System; using System.Collections.Generic; using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; public class Order : BaseEntity, IAggregateRoot { #pragma warning disable CS8618 // Required by Entity Framework private Order() {} public Order(string buyerId, Address shipToAddress, List items) { Guard.Against.NullOrEmpty(buyerId, nameof(buyerId)); BuyerId = buyerId; ShipToAddress = shipToAddress; _orderItems = items; } public string BuyerId { get; private set; } public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now; public Address ShipToAddress { get; private set; } // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection, // but only through the method Order.AddOrderItem() which includes behavior. private readonly List _orderItems = new List(); // Using List<>.AsReadOnly() // This will create a read only wrapper around the private list so is protected against "external updates". // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx public IReadOnlyCollection OrderItems => _orderItems.AsReadOnly(); public decimal Total() { var total = 0m; foreach (var item in _orderItems) { total += item.UnitPrice * item.Units; } return total; } } ================================================ FILE: src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; public class OrderItem : BaseEntity { public CatalogItemOrdered ItemOrdered { get; private set; } public decimal UnitPrice { get; private set; } public int Units { get; private set; } #pragma warning disable CS8618 // Required by Entity Framework private OrderItem() {} public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) { ItemOrdered = itemOrdered; UnitPrice = unitPrice; Units = units; } } ================================================ FILE: src/ApplicationCore/Exceptions/BasketNotFoundException.cs ================================================ using System; namespace Microsoft.eShopWeb.ApplicationCore.Exceptions; public class BasketNotFoundException : Exception { public BasketNotFoundException(int basketId) : base($"No basket found with id {basketId}") { } } ================================================ FILE: src/ApplicationCore/Exceptions/DuplicateException.cs ================================================ using System; namespace Microsoft.eShopWeb.ApplicationCore.Exceptions; public class DuplicateException : Exception { public DuplicateException(string message) : base(message) { } } ================================================ FILE: src/ApplicationCore/Exceptions/EmptyBasketOnCheckoutException.cs ================================================ using System; namespace Microsoft.eShopWeb.ApplicationCore.Exceptions; public class EmptyBasketOnCheckoutException : Exception { public EmptyBasketOnCheckoutException() : base($"Basket cannot have 0 items on checkout") { } public EmptyBasketOnCheckoutException(string message) : base(message) { } public EmptyBasketOnCheckoutException(string message, Exception innerException) : base(message, innerException) { } } ================================================ FILE: src/ApplicationCore/Extensions/GuardExtensions.cs ================================================ using System.Collections.Generic; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Exceptions; namespace Ardalis.GuardClauses; public static class BasketGuards { public static void EmptyBasketOnCheckout(this IGuardClause guardClause, IReadOnlyCollection basketItems) { if (!basketItems.Any()) throw new EmptyBasketOnCheckoutException(); } } ================================================ FILE: src/ApplicationCore/Extensions/JsonExtensions.cs ================================================ using System.Text.Json; namespace Microsoft.eShopWeb; public static class JsonExtensions { private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public static T? FromJson(this string json) => JsonSerializer.Deserialize(json, _jsonOptions); public static string ToJson(this T obj) => JsonSerializer.Serialize(obj, _jsonOptions); } ================================================ FILE: src/ApplicationCore/Interfaces/IAggregateRoot.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IAggregateRoot { } ================================================ FILE: src/ApplicationCore/Interfaces/IAppLogger.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; /// /// This type eliminates the need to depend directly on the ASP.NET Core logging types. /// /// public interface IAppLogger { void LogInformation(string message, params object[] args); void LogWarning(string message, params object[] args); } ================================================ FILE: src/ApplicationCore/Interfaces/IBasketQueryService.cs ================================================ using System.Threading.Tasks; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; /// /// Specific query used to fetch count without running in memory /// public interface IBasketQueryService { Task CountTotalBasketItems(string username); } ================================================ FILE: src/ApplicationCore/Interfaces/IBasketService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using Ardalis.Result; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IBasketService { Task TransferBasketAsync(string anonymousId, string userName); Task AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1); Task> SetQuantities(int basketId, Dictionary quantities); Task DeleteBasketAsync(int basketId); } ================================================ FILE: src/ApplicationCore/Interfaces/IEmailSender.cs ================================================ using System.Threading.Tasks; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IEmailSender { Task SendEmailAsync(string email, string subject, string message); } ================================================ FILE: src/ApplicationCore/Interfaces/IOrderService.cs ================================================ using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IOrderService { Task CreateOrderAsync(int basketId, Address shippingAddress); } ================================================ FILE: src/ApplicationCore/Interfaces/IReadRepository.cs ================================================ using Ardalis.Specification; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IReadRepository : IReadRepositoryBase where T : class, IAggregateRoot { } ================================================ FILE: src/ApplicationCore/Interfaces/IRepository.cs ================================================ using Ardalis.Specification; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IRepository : IRepositoryBase where T : class, IAggregateRoot { } ================================================ FILE: src/ApplicationCore/Interfaces/ITokenClaimsService.cs ================================================ using System.Threading.Tasks; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface ITokenClaimsService { Task GetTokenAsync(string userName); } ================================================ FILE: src/ApplicationCore/Interfaces/IUriComposer.cs ================================================ namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; public interface IUriComposer { string ComposePicUri(string uriTemplate); } ================================================ FILE: src/ApplicationCore/Services/BasketService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using Ardalis.GuardClauses; using Ardalis.Result; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; namespace Microsoft.eShopWeb.ApplicationCore.Services; public class BasketService : IBasketService { private readonly IRepository _basketRepository; private readonly IAppLogger _logger; public BasketService(IRepository basketRepository, IAppLogger logger) { _basketRepository = basketRepository; _logger = logger; } public async Task AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1) { var basketSpec = new BasketWithItemsSpecification(username); var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); if (basket == null) { basket = new Basket(username); await _basketRepository.AddAsync(basket); } basket.AddItem(catalogItemId, price, quantity); await _basketRepository.UpdateAsync(basket); return basket; } public async Task DeleteBasketAsync(int basketId) { var basket = await _basketRepository.GetByIdAsync(basketId); Guard.Against.Null(basket, nameof(basket)); await _basketRepository.DeleteAsync(basket); } public async Task> SetQuantities(int basketId, Dictionary quantities) { var basketSpec = new BasketWithItemsSpecification(basketId); var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); if (basket == null) return Result.NotFound(); foreach (var item in basket.Items) { if (quantities.TryGetValue(item.Id.ToString(), out var quantity)) { if (_logger != null) _logger.LogInformation($"Updating quantity of item ID:{item.Id} to {quantity}."); item.SetQuantity(quantity); } } basket.RemoveEmptyItems(); await _basketRepository.UpdateAsync(basket); return basket; } public async Task TransferBasketAsync(string anonymousId, string userName) { var anonymousBasketSpec = new BasketWithItemsSpecification(anonymousId); var anonymousBasket = await _basketRepository.FirstOrDefaultAsync(anonymousBasketSpec); if (anonymousBasket == null) return; var userBasketSpec = new BasketWithItemsSpecification(userName); var userBasket = await _basketRepository.FirstOrDefaultAsync(userBasketSpec); if (userBasket == null) { userBasket = new Basket(userName); await _basketRepository.AddAsync(userBasket); } foreach (var item in anonymousBasket.Items) { userBasket.AddItem(item.CatalogItemId, item.UnitPrice, item.Quantity); } await _basketRepository.UpdateAsync(userBasket); await _basketRepository.DeleteAsync(anonymousBasket); } } ================================================ FILE: src/ApplicationCore/Services/OrderService.cs ================================================ using System.Linq; using System.Threading.Tasks; using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; namespace Microsoft.eShopWeb.ApplicationCore.Services; public class OrderService : IOrderService { private readonly IRepository _orderRepository; private readonly IUriComposer _uriComposer; private readonly IRepository _basketRepository; private readonly IRepository _itemRepository; public OrderService(IRepository basketRepository, IRepository itemRepository, IRepository orderRepository, IUriComposer uriComposer) { _orderRepository = orderRepository; _uriComposer = uriComposer; _basketRepository = basketRepository; _itemRepository = itemRepository; } public async Task CreateOrderAsync(int basketId, Address shippingAddress) { var basketSpec = new BasketWithItemsSpecification(basketId); var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); Guard.Against.Null(basket, nameof(basket)); Guard.Against.EmptyBasketOnCheckout(basket.Items); var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray()); var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification); var items = basket.Items.Select(basketItem => { var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId); var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, _uriComposer.ComposePicUri(catalogItem.PictureUri)); var orderItem = new OrderItem(itemOrdered, basketItem.UnitPrice, basketItem.Quantity); return orderItem; }).ToList(); var order = new Order(basket.BuyerId, shippingAddress, items); await _orderRepository.AddAsync(order); } } ================================================ FILE: src/ApplicationCore/Services/UriComposer.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Services; public class UriComposer : IUriComposer { private readonly CatalogSettings _catalogSettings; public UriComposer(CatalogSettings catalogSettings) => _catalogSettings = catalogSettings; public string ComposePicUri(string uriTemplate) { return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl); } } ================================================ FILE: src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public sealed class BasketWithItemsSpecification : Specification { public BasketWithItemsSpecification(int basketId) { Query .Where(b => b.Id == basketId) .Include(b => b.Items); } public BasketWithItemsSpecification(string buyerId) { Query .Where(b => b.BuyerId == buyerId) .Include(b => b.Items); } } ================================================ FILE: src/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class CatalogFilterPaginatedSpecification : Specification { public CatalogFilterPaginatedSpecification(int skip, int take, int? brandId, int? typeId) : base() { if (take == 0) { take = int.MaxValue; } Query .Where(i => (!brandId.HasValue || i.CatalogBrandId == brandId) && (!typeId.HasValue || i.CatalogTypeId == typeId)) .Skip(skip).Take(take); } } ================================================ FILE: src/ApplicationCore/Specifications/CatalogFilterSpecification.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class CatalogFilterSpecification : Specification { public CatalogFilterSpecification(int? brandId, int? typeId) { Query.Where(i => (!brandId.HasValue || i.CatalogBrandId == brandId) && (!typeId.HasValue || i.CatalogTypeId == typeId)); } } ================================================ FILE: src/ApplicationCore/Specifications/CatalogItemNameSpecification.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class CatalogItemNameSpecification : Specification { public CatalogItemNameSpecification(string catalogItemName) { Query.Where(item => catalogItemName == item.Name); } } ================================================ FILE: src/ApplicationCore/Specifications/CatalogItemsSpecification.cs ================================================ using System; using System.Linq; using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class CatalogItemsSpecification : Specification { public CatalogItemsSpecification(params int[] ids) { Query.Where(c => ids.Contains(c.Id)); } } ================================================ FILE: src/ApplicationCore/Specifications/CustomerOrdersSpecification.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class CustomerOrdersSpecification : Specification { public CustomerOrdersSpecification(string buyerId) { Query.Where(o => o.BuyerId == buyerId) .Include(o => o.OrderItems); } } ================================================ FILE: src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class CustomerOrdersWithItemsSpecification : Specification { public CustomerOrdersWithItemsSpecification(string buyerId) { Query.Where(o => o.BuyerId == buyerId) .Include(o => o.OrderItems) .ThenInclude(i => i.ItemOrdered); } } ================================================ FILE: src/ApplicationCore/Specifications/OrderWithItemsByIdSpec.cs ================================================ using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Specifications; public class OrderWithItemsByIdSpec : Specification { public OrderWithItemsByIdSpec(int orderId) { Query .Where(order => order.Id == orderId) .Include(o => o.OrderItems) .ThenInclude(i => i.ItemOrdered); } } ================================================ FILE: src/BlazorAdmin/App.razor ================================================ @if (!context.User.Identity.IsAuthenticated) { } else {

Not Authorized

You are not authorized to access this resource. Return to eShop

}

Sorry, there's nothing at this address.

================================================ FILE: src/BlazorAdmin/BlazorAdmin.csproj ================================================  Delete.cs GetById.cs Edit.cs ================================================ FILE: src/BlazorAdmin/CustomAuthStateProvider.cs ================================================ using System; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Claims; using System.Threading.Tasks; using BlazorShared.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; namespace BlazorAdmin; public class CustomAuthStateProvider : AuthenticationStateProvider { // TODO: Get Default Cache Duration from Config private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60); private readonly HttpClient _httpClient; private readonly ILogger _logger; private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0); private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity()); public CustomAuthStateProvider(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } public override async Task GetAuthenticationStateAsync() { return new AuthenticationState(await GetUser(useCache: true)); } private async ValueTask GetUser(bool useCache = false) { var now = DateTimeOffset.Now; if (useCache && now < _userLastCheck + UserCacheRefreshInterval) { return _cachedUser; } _cachedUser = await FetchUser(); _userLastCheck = now; return _cachedUser; } private async Task FetchUser() { UserInfo user = null; try { _logger.LogInformation("Fetching user details from web api."); user = await _httpClient.GetFromJsonAsync("User"); } catch (Exception exc) { _logger.LogWarning(exc, "Fetching user failed."); } if (user == null || !user.IsAuthenticated) { return new ClaimsPrincipal(new ClaimsIdentity()); } var identity = new ClaimsIdentity( nameof(CustomAuthStateProvider), user.NameClaimType, user.RoleClaimType); if (user.Claims != null) { foreach (var claim in user.Claims) { identity.AddClaim(new Claim(claim.Type, claim.Value)); } } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Token); return new ClaimsPrincipal(identity); } } ================================================ FILE: src/BlazorAdmin/Helpers/BlazorComponent.cs ================================================ using Microsoft.AspNetCore.Components; namespace BlazorAdmin.Helpers; public class BlazorComponent : ComponentBase { private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance; protected override void OnInitialized() { _refresh.RefreshRequested += DoRefresh; base.OnInitialized(); } public void CallRequestRefresh() { _refresh.CallRequestRefresh(); } private void DoRefresh() { StateHasChanged(); } } ================================================ FILE: src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs ================================================ using Microsoft.AspNetCore.Components; namespace BlazorAdmin.Helpers; public class BlazorLayoutComponent : LayoutComponentBase { private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance; protected override void OnInitialized() { _refresh.RefreshRequested += DoRefresh; base.OnInitialized(); } public void CallRequestRefresh() { _refresh.CallRequestRefresh(); } private void DoRefresh() { StateHasChanged(); } } ================================================ FILE: src/BlazorAdmin/Helpers/RefreshBroadcast.cs ================================================ using System; namespace BlazorAdmin.Helpers; internal sealed class RefreshBroadcast { private static readonly Lazy Lazy = new Lazy (() => new RefreshBroadcast()); public static RefreshBroadcast Instance => Lazy.Value; private RefreshBroadcast() { } public event Action RefreshRequested; public void CallRequestRefresh() { RefreshRequested?.Invoke(); } } ================================================ FILE: src/BlazorAdmin/Helpers/ToastComponent.cs ================================================ using System; using BlazorAdmin.Services; using Microsoft.AspNetCore.Components; namespace BlazorAdmin.Helpers; public class ToastComponent : ComponentBase, IDisposable { [Inject] ToastService ToastService { get; set; } protected string Heading { get; set; } protected string Message { get; set; } protected bool IsVisible { get; set; } protected string BackgroundCssClass { get; set; } protected string IconCssClass { get; set; } protected override void OnInitialized() { ToastService.OnShow += ShowToast; ToastService.OnHide += HideToast; } private void ShowToast(string message, ToastLevel level) { BuildToastSettings(level, message); IsVisible = true; StateHasChanged(); } private void HideToast() { IsVisible = false; StateHasChanged(); } private void BuildToastSettings(ToastLevel level, string message) { switch (level) { case ToastLevel.Info: BackgroundCssClass = "bg-info"; IconCssClass = "info"; Heading = "Info"; break; case ToastLevel.Success: BackgroundCssClass = "bg-success"; IconCssClass = "check"; Heading = "Success"; break; case ToastLevel.Warning: BackgroundCssClass = "bg-warning"; IconCssClass = "exclamation"; Heading = "Warning"; break; case ToastLevel.Error: BackgroundCssClass = "bg-danger"; IconCssClass = "times"; Heading = "Error"; break; } Message = message; } public void Dispose() { ToastService.OnShow -= ShowToast; } } ================================================ FILE: src/BlazorAdmin/JavaScript/Cookies.cs ================================================ using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorAdmin.JavaScript; public class Cookies { private readonly IJSRuntime _jsRuntime; public Cookies(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async Task DeleteCookie(string name) { await _jsRuntime.InvokeAsync(JSInteropConstants.DeleteCookie, name); } public async Task GetCookie(string name) { return await _jsRuntime.InvokeAsync(JSInteropConstants.GetCookie, name); } } ================================================ FILE: src/BlazorAdmin/JavaScript/Css.cs ================================================ using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorAdmin.JavaScript; public class Css { private readonly IJSRuntime _jsRuntime; public Css(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async Task ShowBodyOverflow() { await _jsRuntime.InvokeAsync(JSInteropConstants.ShowBodyOverflow); } public async Task HideBodyOverflow() { return await _jsRuntime.InvokeAsync(JSInteropConstants.HideBodyOverflow); } } ================================================ FILE: src/BlazorAdmin/JavaScript/JSInteropConstants.cs ================================================ namespace BlazorAdmin.JavaScript; public static class JSInteropConstants { public static string DeleteCookie => "deleteCookie"; public static string GetCookie => "getCookie"; public static string RouteOutside => "routeOutside"; public static string HideBodyOverflow => "hideBodyOverflow"; public static string ShowBodyOverflow => "showBodyOverflow"; } ================================================ FILE: src/BlazorAdmin/JavaScript/Route.cs ================================================ using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorAdmin.JavaScript; public class Route { private readonly IJSRuntime _jsRuntime; public Route(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async Task RouteOutside(string path) { await _jsRuntime.InvokeAsync(JSInteropConstants.RouteOutside, path); } } ================================================ FILE: src/BlazorAdmin/Pages/CatalogItemPage/Create.razor ================================================ @inject ILogger Logger @inject IJSRuntime JSRuntime @inject ICatalogItemService CatalogItemService @inherits BlazorAdmin.Helpers.BlazorComponent @namespace BlazorAdmin.Pages.CatalogItemPage @if (_showCreateModal) { } @code { [Parameter] public IEnumerable Brands { get; set; } [Parameter] public IEnumerable Types { get; set; } [Parameter] public EventCallback OnSaveClick { get; set; } private string LoadPicture => string.IsNullOrEmpty(_item.PictureBase64) ? string.Empty : $"data:image/png;base64, {_item.PictureBase64}"; private bool HasPicture => !string.IsNullOrEmpty(_item.PictureBase64); private string _badFileMessage = string.Empty; private string _modalDisplay = "none;"; private string _modalClass = ""; private bool _showCreateModal = false; private CreateCatalogItemRequest _item = new CreateCatalogItemRequest(); private async Task CreateClick() { var result = await CatalogItemService.Create(_item); if (result != null) { await OnSaveClick.InvokeAsync(null); await Close(); } } public async Task Open() { Logger.LogInformation("Now loading... /Catalog/Create"); await new Css(JSRuntime).HideBodyOverflow(); _item = new CreateCatalogItemRequest { CatalogTypeId = Types.First().Id, CatalogBrandId = Brands.First().Id }; _modalDisplay = "block;"; _modalClass = "Show"; _showCreateModal = true; StateHasChanged(); } private async Task Close() { await new Css(JSRuntime).ShowBodyOverflow(); _modalDisplay = "none"; _modalClass = ""; _showCreateModal = false; } } ================================================ FILE: src/BlazorAdmin/Pages/CatalogItemPage/Delete.razor ================================================ @inject ILogger Logger @inject IJSRuntime JSRuntime @inject ICatalogItemService CatalogItemService @inherits BlazorAdmin.Helpers.BlazorComponent @namespace BlazorAdmin.Pages.CatalogItemPage @if (_showDeleteModal) { } @code { [Parameter] public IEnumerable Brands { get; set; } [Parameter] public IEnumerable Types { get; set; } [Parameter] public EventCallback OnSaveClick { get; set; } private bool HasPicture => !string.IsNullOrEmpty(_item.PictureUri); private string _modalDisplay = "none;"; private string _modalClass = ""; private bool _showDeleteModal = false; private CatalogItem _item = new CatalogItem(); private async Task DeleteClick(int id) { // TODO: Add some kind of "are you sure" check before this await CatalogItemService.Delete(id); await OnSaveClick.InvokeAsync(null); await Close(); } public async Task Open(int id) { Logger.LogInformation("Now loading... /Catalog/Delete/{Id}", id); await new Css(JSRuntime).HideBodyOverflow(); _item = await CatalogItemService.GetById(id); _modalDisplay = "block;"; _modalClass = "Show"; _showDeleteModal = true; StateHasChanged(); } private async Task Close() { await new Css(JSRuntime).ShowBodyOverflow(); _modalDisplay = "none"; _modalClass = ""; _showDeleteModal = false; } } ================================================ FILE: src/BlazorAdmin/Pages/CatalogItemPage/Details.razor ================================================ @inject ILogger
Logger @inject IJSRuntime JSRuntime @inject ICatalogItemService CatalogItemService @inherits BlazorAdmin.Helpers.BlazorComponent @namespace BlazorAdmin.Pages.CatalogItemPage @if (_showDetailsModal) { } @code { [Parameter] public IEnumerable Brands { get; set; } [Parameter] public IEnumerable Types { get; set; } [Parameter] public EventCallback OnEditClick { get; set; } private bool HasPicture => !string.IsNullOrEmpty(_item.PictureUri); private string _modalDisplay = "none;"; private string _modalClass = ""; private bool _showDetailsModal = false; private CatalogItem _item = new CatalogItem(); public async Task EditClick() { await OnEditClick.InvokeAsync(_item.Id); await Close(); } public async Task Open(int id) { Logger.LogInformation("Now loading... /Catalog/Details/{Id}", id); await new Css(JSRuntime).HideBodyOverflow(); _item = await CatalogItemService.GetById(id); _modalDisplay = "block;"; _modalClass = "Show"; _showDetailsModal = true; StateHasChanged(); } public async Task Close() { await new Css(JSRuntime).ShowBodyOverflow(); _modalDisplay = "none"; _modalClass = ""; _showDetailsModal = false; } } ================================================ FILE: src/BlazorAdmin/Pages/CatalogItemPage/Edit.razor ================================================ @inject ILogger Logger @inject IJSRuntime JSRuntime @inject ICatalogItemService CatalogItemService @inherits BlazorAdmin.Helpers.BlazorComponent @namespace BlazorAdmin.Pages.CatalogItemPage @if (_showEditModal) { } @code { [Parameter] public IEnumerable Brands { get; set; } [Parameter] public IEnumerable Types { get; set; } [Parameter] public EventCallback OnSaveClick { get; set; } private string LoadPicture => string.IsNullOrEmpty(_item.PictureBase64) ? string.IsNullOrEmpty(_item.PictureUri) ? string.Empty : $"{_item.PictureUri}" : $"data:image/png;base64, {_item.PictureBase64}"; private bool HasPicture => !(string.IsNullOrEmpty(_item.PictureBase64) && string.IsNullOrEmpty(_item.PictureUri)); private string _badFileMessage = string.Empty; private string _modalDisplay = "none;"; private string _modalClass = ""; private bool _showEditModal = false; private CatalogItem _item = new CatalogItem(); private async Task SaveClick() { await CatalogItemService.Edit(_item); await OnSaveClick.InvokeAsync(null); await Close(); } public async Task Open(int id) { Logger.LogInformation("Now loading... /Catalog/Edit/{Id}", id); await new Css(JSRuntime).HideBodyOverflow(); _item = await CatalogItemService.GetById(id); _modalDisplay = "block;"; _modalClass = "Show"; _showEditModal = true; StateHasChanged(); } private async Task Close() { await new Css(JSRuntime).ShowBodyOverflow(); _modalDisplay = "none"; _modalClass = ""; _showEditModal = false; } } ================================================ FILE: src/BlazorAdmin/Pages/CatalogItemPage/List.razor ================================================ @page "/admin" @attribute [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)] @inherits BlazorAdmin.Helpers.BlazorComponent @namespace BlazorAdmin.Pages.CatalogItemPage eShopOnWeb Admin: Manage Product Catalog

Manage Product Catalog

@if (catalogItems == null) { } else { @foreach (var item in catalogItems) { }
Item Type Brand Id Name @nameof(CatalogItem.Description) @nameof(CatalogItem.Price) Actions
@item.CatalogType @item.CatalogBrand @item.Id @item.Name @item.Description @item.Price
} ================================================ FILE: src/BlazorAdmin/Pages/CatalogItemPage/List.razor.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using BlazorAdmin.Helpers; using BlazorShared.Interfaces; using BlazorShared.Models; namespace BlazorAdmin.Pages.CatalogItemPage; public partial class List : BlazorComponent { [Microsoft.AspNetCore.Components.Inject] public ICatalogItemService CatalogItemService { get; set; } [Microsoft.AspNetCore.Components.Inject] public ICatalogLookupDataService CatalogBrandService { get; set; } [Microsoft.AspNetCore.Components.Inject] public ICatalogLookupDataService CatalogTypeService { get; set; } private List catalogItems = new List(); private List catalogTypes = new List(); private List catalogBrands = new List(); private Edit EditComponent { get; set; } private Delete DeleteComponent { get; set; } private Details DetailsComponent { get; set; } private Create CreateComponent { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { catalogItems = await CatalogItemService.List(); catalogTypes = await CatalogTypeService.List(); catalogBrands = await CatalogBrandService.List(); CallRequestRefresh(); } await base.OnAfterRenderAsync(firstRender); } private async void DetailsClick(int id) { await DetailsComponent.Open(id); } private async Task CreateClick() { await CreateComponent.Open(); } private async Task EditClick(int id) { await EditComponent.Open(id); } private async Task DeleteClick(int id) { await DeleteComponent.Open(id); } private async Task ReloadCatalogItems() { catalogItems = await CatalogItemService.List(); StateHasChanged(); } } ================================================ FILE: src/BlazorAdmin/Pages/Logout.razor ================================================ @page "/logout" @inject IJSRuntime JSRuntime @inject HttpClient HttpClient @inherits BlazorAdmin.Helpers.BlazorComponent @code { protected override async Task OnInitializedAsync() { await HttpClient.PostAsync("User/Logout", null); await new Route(JSRuntime).RouteOutside("/Identity/Account/Login"); } } ================================================ FILE: src/BlazorAdmin/Program.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using BlazorAdmin; using BlazorAdmin.Services; using Blazored.LocalStorage; using BlazorShared; using BlazorShared.Models; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#admin"); builder.RootComponents.Add("head::after"); var configSection = builder.Configuration.GetRequiredSection(BaseUrlConfiguration.CONFIG_NAME); builder.Services.Configure(configSection); builder.Services.AddScoped(sp => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddBlazoredLocalStorage(); builder.Services.AddAuthorizationCore(); builder.Services.AddScoped(); builder.Services.AddScoped(sp => (CustomAuthStateProvider)sp.GetRequiredService()); builder.Services.AddBlazorServices(); builder.Logging.AddConfiguration(builder.Configuration.GetRequiredSection("Logging")); await ClearLocalStorageCache(builder.Services); await builder.Build().RunAsync(); static async Task ClearLocalStorageCache(IServiceCollection services) { var sp = services.BuildServiceProvider(); var localStorageService = sp.GetRequiredService(); await localStorageService.RemoveItemAsync(typeof(CatalogBrand).Name); await localStorageService.RemoveItemAsync(typeof(CatalogType).Name); } ================================================ FILE: src/BlazorAdmin/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:58126", "sslPort": 44315 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "BlazorAdmin": { "commandName": "Project", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/BlazorAdmin/Services/CacheEntry.cs ================================================ using System; namespace BlazorAdmin.Services; public class CacheEntry { public CacheEntry(T item) { Value = item; } public CacheEntry() { } public T Value { get; set; } public DateTime DateCreated { get; set; } = DateTime.UtcNow; } ================================================ FILE: src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Blazored.LocalStorage; using BlazorShared.Interfaces; using BlazorShared.Models; using Microsoft.Extensions.Logging; namespace BlazorAdmin.Services; public class CachedCatalogItemServiceDecorator : ICatalogItemService { private readonly ILocalStorageService _localStorageService; private readonly CatalogItemService _catalogItemService; private ILogger _logger; public CachedCatalogItemServiceDecorator(ILocalStorageService localStorageService, CatalogItemService catalogItemService, ILogger logger) { _localStorageService = localStorageService; _catalogItemService = catalogItemService; _logger = logger; } public async Task> ListPaged(int pageSize) { string key = "items"; var cacheEntry = await _localStorageService.GetItemAsync>>(key); if (cacheEntry != null) { _logger.LogInformation("Loading items from local storage."); if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow) { return cacheEntry.Value; } else { _logger.LogInformation($"Loading {key} from local storage."); await _localStorageService.RemoveItemAsync(key); } } var items = await _catalogItemService.ListPaged(pageSize); var entry = new CacheEntry>(items); await _localStorageService.SetItemAsync(key, entry); return items; } public async Task> List() { string key = "items"; var cacheEntry = await _localStorageService.GetItemAsync>>(key); if (cacheEntry != null) { _logger.LogInformation("Loading items from local storage."); if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow) { return cacheEntry.Value; } else { _logger.LogInformation($"Loading {key} from local storage."); await _localStorageService.RemoveItemAsync(key); } } var items = await _catalogItemService.List(); var entry = new CacheEntry>(items); await _localStorageService.SetItemAsync(key, entry); return items; } public async Task GetById(int id) { return (await List()).FirstOrDefault(x => x.Id == id); } public async Task Create(CreateCatalogItemRequest catalogItem) { var result = await _catalogItemService.Create(catalogItem); await RefreshLocalStorageList(); return result; } public async Task Edit(CatalogItem catalogItem) { var result = await _catalogItemService.Edit(catalogItem); await RefreshLocalStorageList(); return result; } public async Task Delete(int id) { var result = await _catalogItemService.Delete(id); await RefreshLocalStorageList(); return result; } private async Task RefreshLocalStorageList() { string key = "items"; await _localStorageService.RemoveItemAsync(key); var items = await _catalogItemService.List(); var entry = new CacheEntry>(items); await _localStorageService.SetItemAsync(key, entry); } } ================================================ FILE: src/BlazorAdmin/Services/CachedCatalogLookupDataServiceDecorator .cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Blazored.LocalStorage; using BlazorShared.Interfaces; using BlazorShared.Models; using Microsoft.Extensions.Logging; namespace BlazorAdmin.Services; public class CachedCatalogLookupDataServiceDecorator : ICatalogLookupDataService where TLookupData : LookupData where TReponse : ILookupDataResponse { private readonly ILocalStorageService _localStorageService; private readonly CatalogLookupDataService _catalogTypeService; private ILogger> _logger; public CachedCatalogLookupDataServiceDecorator(ILocalStorageService localStorageService, CatalogLookupDataService catalogTypeService, ILogger> logger) { _localStorageService = localStorageService; _catalogTypeService = catalogTypeService; _logger = logger; } public async Task> List() { string key = typeof(TLookupData).Name; var cacheEntry = await _localStorageService.GetItemAsync>>(key); if (cacheEntry != null) { _logger.LogInformation($"Loading {key} from local storage."); if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow) { return cacheEntry.Value; } else { _logger.LogInformation($"Cache expired; removing {key} from local storage."); await _localStorageService.RemoveItemAsync(key); } } var types = await _catalogTypeService.List(); var entry = new CacheEntry>(types); await _localStorageService.SetItemAsync(key, entry); return types; } } ================================================ FILE: src/BlazorAdmin/Services/CatalogItemService.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BlazorShared.Interfaces; using BlazorShared.Models; using Microsoft.Extensions.Logging; namespace BlazorAdmin.Services; public class CatalogItemService : ICatalogItemService { private readonly ICatalogLookupDataService _brandService; private readonly ICatalogLookupDataService _typeService; private readonly HttpService _httpService; private readonly ILogger _logger; public CatalogItemService(ICatalogLookupDataService brandService, ICatalogLookupDataService typeService, HttpService httpService, ILogger logger) { _brandService = brandService; _typeService = typeService; _httpService = httpService; _logger = logger; } public async Task Create(CreateCatalogItemRequest catalogItem) { var response = await _httpService.HttpPost("catalog-items", catalogItem); return response?.CatalogItem; } public async Task Edit(CatalogItem catalogItem) { return (await _httpService.HttpPut("catalog-items", catalogItem)).CatalogItem; } public async Task Delete(int catalogItemId) { return (await _httpService.HttpDelete("catalog-items", catalogItemId)).Status; } public async Task GetById(int id) { var brandListTask = _brandService.List(); var typeListTask = _typeService.List(); var itemGetTask = _httpService.HttpGet($"catalog-items/{id}"); await Task.WhenAll(brandListTask, typeListTask, itemGetTask); var brands = brandListTask.Result; var types = typeListTask.Result; var catalogItem = itemGetTask.Result.CatalogItem; catalogItem.CatalogBrand = brands.FirstOrDefault(b => b.Id == catalogItem.CatalogBrandId)?.Name; catalogItem.CatalogType = types.FirstOrDefault(t => t.Id == catalogItem.CatalogTypeId)?.Name; return catalogItem; } public async Task> ListPaged(int pageSize) { _logger.LogInformation("Fetching catalog items from API."); var brandListTask = _brandService.List(); var typeListTask = _typeService.List(); var itemListTask = _httpService.HttpGet($"catalog-items?PageSize=10"); await Task.WhenAll(brandListTask, typeListTask, itemListTask); var brands = brandListTask.Result; var types = typeListTask.Result; var items = itemListTask.Result.CatalogItems; foreach (var item in items) { item.CatalogBrand = brands.FirstOrDefault(b => b.Id == item.CatalogBrandId)?.Name; item.CatalogType = types.FirstOrDefault(t => t.Id == item.CatalogTypeId)?.Name; } return items; } public async Task> List() { _logger.LogInformation("Fetching catalog items from API."); var brandListTask = _brandService.List(); var typeListTask = _typeService.List(); var itemListTask = _httpService.HttpGet($"catalog-items"); await Task.WhenAll(brandListTask, typeListTask, itemListTask); var brands = brandListTask.Result; var types = typeListTask.Result; var items = itemListTask.Result.CatalogItems; foreach (var item in items) { item.CatalogBrand = brands.FirstOrDefault(b => b.Id == item.CatalogBrandId)?.Name; item.CatalogType = types.FirstOrDefault(t => t.Id == item.CatalogTypeId)?.Name; } return items; } } ================================================ FILE: src/BlazorAdmin/Services/CatalogLookupDataService.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Reflection; using System.Threading.Tasks; using BlazorShared; using BlazorShared.Attributes; using BlazorShared.Interfaces; using BlazorShared.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BlazorAdmin.Services; public class CatalogLookupDataService : ICatalogLookupDataService where TLookupData : LookupData where TReponse : ILookupDataResponse { private readonly HttpClient _httpClient; private readonly ILogger> _logger; private readonly string _apiUrl; public CatalogLookupDataService(HttpClient httpClient, IOptions baseUrlConfiguration, ILogger> logger) { _httpClient = httpClient; _logger = logger; _apiUrl = baseUrlConfiguration.Value.ApiBase; } public async Task> List() { var endpointName = typeof(TLookupData).GetCustomAttribute().Name; _logger.LogInformation($"Fetching {typeof(TLookupData).Name} from API. Enpoint : {endpointName}"); var response = await _httpClient.GetFromJsonAsync($"{_apiUrl}{endpointName}"); return response.List; } } ================================================ FILE: src/BlazorAdmin/Services/HttpService.cs ================================================ using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using BlazorShared; using BlazorShared.Models; using Microsoft.Extensions.Options; namespace BlazorAdmin.Services; public class HttpService { private readonly HttpClient _httpClient; private readonly ToastService _toastService; private readonly string _apiUrl; public HttpService(HttpClient httpClient, IOptions baseUrlConfiguration, ToastService toastService) { _httpClient = httpClient; _toastService = toastService; _apiUrl = baseUrlConfiguration.Value.ApiBase; } public async Task HttpGet(string uri) where T : class { var result = await _httpClient.GetAsync($"{_apiUrl}{uri}"); if (!result.IsSuccessStatusCode) { return null; } return await FromHttpResponseMessage(result); } public async Task HttpDelete(string uri, int id) where T : class { var result = await _httpClient.DeleteAsync($"{_apiUrl}{uri}/{id}"); if (!result.IsSuccessStatusCode) { return null; } return await FromHttpResponseMessage(result); } public async Task HttpPost(string uri, object dataToSend) where T : class { var content = ToJson(dataToSend); var result = await _httpClient.PostAsync($"{_apiUrl}{uri}", content); if (!result.IsSuccessStatusCode) { var exception = JsonSerializer.Deserialize(await result.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); _toastService.ShowToast($"Error : {exception.Message}", ToastLevel.Error); return null; } return await FromHttpResponseMessage(result); } public async Task HttpPut(string uri, object dataToSend) where T : class { var content = ToJson(dataToSend); var result = await _httpClient.PutAsync($"{_apiUrl}{uri}", content); if (!result.IsSuccessStatusCode) { _toastService.ShowToast("Error", ToastLevel.Error); return null; } return await FromHttpResponseMessage(result); } private StringContent ToJson(object obj) { return new StringContent(JsonSerializer.Serialize(obj), Encoding.UTF8, "application/json"); } private async Task FromHttpResponseMessage(HttpResponseMessage result) { return JsonSerializer.Deserialize(await result.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } } ================================================ FILE: src/BlazorAdmin/Services/ToastService.cs ================================================ using System; using System.Timers; namespace BlazorAdmin.Services; public enum ToastLevel { Info, Success, Warning, Error } public class ToastService : IDisposable { public event Action OnShow; public event Action OnHide; private Timer Countdown; public void ShowToast(string message, ToastLevel level) { OnShow?.Invoke(message, level); StartCountdown(); } private void StartCountdown() { SetCountdown(); if (Countdown.Enabled) { Countdown.Stop(); Countdown.Start(); } else { Countdown.Start(); } } private void SetCountdown() { if (Countdown == null) { Countdown = new Timer(3000); Countdown.Elapsed += HideToast; Countdown.AutoReset = false; } } private void HideToast(object source, ElapsedEventArgs args) { OnHide?.Invoke(); } public void Dispose() { Countdown?.Dispose(); } } ================================================ FILE: src/BlazorAdmin/ServicesConfiguration.cs ================================================ using BlazorAdmin.Services; using BlazorShared.Interfaces; using BlazorShared.Models; using Microsoft.Extensions.DependencyInjection; namespace BlazorAdmin; public static class ServicesConfiguration { public static IServiceCollection AddBlazorServices(this IServiceCollection services) { services.AddScoped, CachedCatalogLookupDataServiceDecorator>(); services.AddScoped>(); services.AddScoped, CachedCatalogLookupDataServiceDecorator>(); services.AddScoped>(); services.AddScoped(); services.AddScoped(); return services; } } ================================================ FILE: src/BlazorAdmin/Shared/CustomInputSelect.cs ================================================ using Microsoft.AspNetCore.Components.Forms; namespace BlazorAdmin.Shared; /// /// This is needed until 5.0 ships with native support /// https://www.pragimtech.com/blog/blazor/inputselect-does-not-support-system.int32/ /// /// public class CustomInputSelect : InputSelect { protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { if (typeof(TValue) == typeof(int)) { if (int.TryParse(value, out var resultInt)) { result = (TValue)(object)resultInt; validationErrorMessage = null; return true; } else { result = default; validationErrorMessage = $"The selected value {value} is not a valid number."; return false; } } else { return base.TryParseValueFromString(value, out result, out validationErrorMessage); } } } ================================================ FILE: src/BlazorAdmin/Shared/MainLayout.razor ================================================ @inject AuthenticationStateProvider AuthStateProvider @inject IJSRuntime JSRuntime @inherits BlazorAdmin.Helpers.BlazorLayoutComponent @code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); if (authState.User == null) { await new Route(JSRuntime).RouteOutside("/Identity/Account/Login"); } CallRequestRefresh(); } await base.OnAfterRenderAsync(firstRender); } } ================================================ FILE: src/BlazorAdmin/Shared/NavMenu.razor ================================================ @inherits BlazorAdmin.Helpers.BlazorComponent
@code { private bool collapseNavMenu = true; private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } } ================================================ FILE: src/BlazorAdmin/Shared/RedirectToLogin.razor ================================================ @using System.Web; @inject NavigationManager Navigation @inject IJSRuntime JsRuntime @code { protected override void OnInitialized() { var returnUrl = HttpUtility.UrlEncode($"/{Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri))}"); JsRuntime.InvokeVoidAsync("location.replace", $"Identity/Account/Login?returnUrl={returnUrl}"); } } ================================================ FILE: src/BlazorAdmin/Shared/Spinner.razor ================================================  @inherits BlazorAdmin.Helpers.BlazorComponent @namespace BlazorAdmin.Shared
Loading...
================================================ FILE: src/BlazorAdmin/Shared/Toast.razor ================================================ @inherits BlazorAdmin.Helpers.ToastComponent @namespace BlazorAdmin.Shared
@Heading

@Message

================================================ FILE: src/BlazorAdmin/_Imports.razor ================================================ @using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Authorization; @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using Microsoft.Extensions.Logging @using BlazorAdmin @using BlazorAdmin.Shared @using BlazorAdmin.Services @using BlazorAdmin.JavaScript @using BlazorShared.Authorization @using BlazorShared.Interfaces @using BlazorInputFile @using BlazorShared.Models ================================================ FILE: src/BlazorAdmin/wwwroot/appsettings.Development.json ================================================ { "baseUrls": { "apiBase": "https://localhost:5099/api/", "webBase": "https://localhost:44315/" }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Information", "Microsoft": "Warning", "System": "Warning" } } } ================================================ FILE: src/BlazorAdmin/wwwroot/appsettings.Docker.json ================================================ { "baseUrls": { "apiBase": "http://localhost:5200/api/", "webBase": "http://host.docker.internal:5106/" } } ================================================ FILE: src/BlazorAdmin/wwwroot/appsettings.json ================================================ { "baseUrls": { "apiBase": "https://localhost:5099/api/", "webBase": "https://localhost:44315/" }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Information", "Microsoft": "Warning", "System": "Warning" } } } ================================================ FILE: src/BlazorAdmin/wwwroot/css/admin.css ================================================ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } a, .btn-link { color: #0366d6; } .btn-primary { color: #fff; background-color: #1b6ec2; border-color: #1861ac; } #admin { position: relative; display: flex; } .top-row { height: 3.5rem; display: flex; align-items: center; } .main { flex: 1; } .main .top-row { background-color: #f7f7f7; border-bottom: 1px solid #d6d5d5; justify-content: flex-end; } .main .top-row > a, .main .top-row .btn-link { white-space: nowrap; margin-left: 1.5rem; } .main .top-row a:first-child { overflow: hidden; text-overflow: ellipsis; } .sidebar { background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); } .sidebar .top-row { background-color: rgba(0,0,0,0.4); } .sidebar .navbar-brand { font-size: 1.1rem; } .sidebar .oi { width: 2rem; font-size: 1.1rem; vertical-align: text-top; top: -2px; } .sidebar .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; } .sidebar .nav-item:first-of-type { padding-top: 1rem; } .sidebar .nav-item:last-of-type { padding-bottom: 1rem; } .sidebar .nav-item a { color: #d7d7d7; border-radius: 4px; height: 3rem; display: flex; align-items: center; line-height: 3rem; } .sidebar .nav-item a.active { background-color: rgba(255,255,255,0.25); color: white; } .sidebar .nav-item a:hover { background-color: rgba(255,255,255,0.1); color: white; } .content { padding-top: 1.1rem; } .navbar-toggler { background-color: rgba(255, 255, 255, 0.1); } .valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } .invalid { outline: 1px solid red; } .validation-message { color: red; } #blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; } .cursor-pointer { cursor: pointer; } .img-thumbnail { max-width: 120px; height: auto; } .esh-picture { height: 100%; width: 100%; } .body-no-overflow { overflow: hidden !important; } .toast { display: none; padding: 1.5rem; color: #fff; z-index: 99999; position: absolute; width: 25rem; top: 2rem; border-radius: 1rem; left: 50%; } .toast-icon { display: flex; flex-direction: column; justify-content: center; padding: 01rem; font-size: 2.5rem; } .toast-body { display: flex; flex-direction: column; flex: 1; padding-left: 1rem; } .toast-body p { margin-bottom: 0; } .toast-visible { display: flex; flex-direction: row; animation: fadein 1.5s; } @keyframes fadein { from { opacity: 0; } to { opacity: 1; } } @media (max-width: 767.98px) { .main .top-row:not(.auth) { display: none; } .main .top-row.auth { justify-content: space-between; } .main .top-row a, .main .top-row .btn-link { margin-left: 0; } } @media (min-width: 768px) { .sidebar { width: 250px; height: 100vh; position: sticky; top: 0; } .main .top-row { position: sticky; top: 0; } .main > div { padding-left: 2rem !important; padding-right: 1.5rem !important; } .navbar-toggler { display: none; } .sidebar .collapse { /* Never collapse the sidebar for wide screens */ display: block; } } ================================================ FILE: src/BlazorAdmin/wwwroot/css/open-iconic/FONT-LICENSE ================================================ SIL OPEN FONT LICENSE Version 1.1 Copyright (c) 2014 Waybury PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: src/BlazorAdmin/wwwroot/css/open-iconic/ICON-LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Waybury Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: src/BlazorAdmin/wwwroot/css/open-iconic/README.md ================================================ [Open Iconic v1.1.1](http://useiconic.com/open) =========== ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) ## What's in Open Iconic? * 223 icons designed to be legible down to 8 pixels * Super-light SVG files - 61.8 for the entire set * SVG sprite—the modern replacement for icon fonts * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. ## Getting Started #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. ### General Usage #### Using Open Iconic's SVGs We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). ``` icon name ``` #### Using Open Iconic's SVG Sprite Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* ``` ``` Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. ``` .icon { width: 16px; height: 16px; } ``` Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. ``` .icon-account-login { fill: #f00; } ``` To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). #### Using Open Iconic's Icon Font... ##### …with Bootstrap You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` ``` ``` ``` ``` ##### …with Foundation You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` ``` ``` ``` ``` ##### …on its own You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` ``` ``` ``` ``` ## License ### Icons All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). ### Fonts All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). ================================================ FILE: src/BlazorShared/Attributes/EndpointAttribute.cs ================================================ using System; namespace BlazorShared.Attributes; public class EndpointAttribute : Attribute { public string Name { get; set; } } ================================================ FILE: src/BlazorShared/Authorization/ClaimValue.cs ================================================ namespace BlazorShared.Authorization; public class ClaimValue { public ClaimValue() { } public ClaimValue(string type, string value) { Type = type; Value = value; } public string Type { get; set; } public string Value { get; set; } } ================================================ FILE: src/BlazorShared/Authorization/Constants.cs ================================================ namespace BlazorShared.Authorization; public static class Constants { public static class Roles { public const string ADMINISTRATORS = "Administrators"; } } ================================================ FILE: src/BlazorShared/Authorization/UserInfo.cs ================================================ using System.Collections.Generic; namespace BlazorShared.Authorization; public class UserInfo { public static readonly UserInfo Anonymous = new UserInfo(); public bool IsAuthenticated { get; set; } public string NameClaimType { get; set; } public string RoleClaimType { get; set; } public string Token { get; set; } public IEnumerable Claims { get; set; } } ================================================ FILE: src/BlazorShared/BaseUrlConfiguration.cs ================================================ namespace BlazorShared; public class BaseUrlConfiguration { public const string CONFIG_NAME = "baseUrls"; public string ApiBase { get; set; } public string WebBase { get; set; } } ================================================ FILE: src/BlazorShared/BlazorShared.csproj ================================================  BlazorShared BlazorShared ================================================ FILE: src/BlazorShared/Interfaces/ICatalogItemService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using BlazorShared.Models; namespace BlazorShared.Interfaces; public interface ICatalogItemService { Task Create(CreateCatalogItemRequest catalogItem); Task Edit(CatalogItem catalogItem); Task Delete(int id); Task GetById(int id); Task> ListPaged(int pageSize); Task> List(); } ================================================ FILE: src/BlazorShared/Interfaces/ICatalogLookupDataService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using BlazorShared.Models; namespace BlazorShared.Interfaces; public interface ICatalogLookupDataService where TLookupData : LookupData { Task> List(); } ================================================ FILE: src/BlazorShared/Interfaces/ILookupDataResponse.cs ================================================ using System.Collections.Generic; using BlazorShared.Models; namespace BlazorShared.Interfaces; public interface ILookupDataResponse where TLookupData : LookupData { List List { get; set; } } ================================================ FILE: src/BlazorShared/Models/CatalogBrand.cs ================================================ using BlazorShared.Attributes; namespace BlazorShared.Models; [Endpoint(Name = "catalog-brands")] public class CatalogBrand : LookupData { } ================================================ FILE: src/BlazorShared/Models/CatalogBrandResponse.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; using BlazorShared.Interfaces; namespace BlazorShared.Models; public class CatalogBrandResponse : ILookupDataResponse { [JsonPropertyName("CatalogBrands")] public List List { get; set; } = new List(); } ================================================ FILE: src/BlazorShared/Models/CatalogItem.cs ================================================ using System; using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; using BlazorInputFile; namespace BlazorShared.Models; public class CatalogItem { public int Id { get; set; } public int CatalogTypeId { get; set; } public string CatalogType { get; set; } = "NotSet"; public int CatalogBrandId { get; set; } public string CatalogBrand { get; set; } = "NotSet"; [Required(ErrorMessage = "The Name field is required")] public string Name { get; set; } [Required(ErrorMessage = "The Description field is required")] public string Description { get; set; } // decimal(18,2) [RegularExpression(@"^\d+(\.\d{0,2})*$", ErrorMessage = "The field Price must be a positive number with maximum two decimals.")] [Range(0.01, 1000)] [DataType(DataType.Currency)] public decimal Price { get; set; } public string PictureUri { get; set; } public string PictureBase64 { get; set; } public string PictureName { get; set; } private const int ImageMaximumBytes = 512000; public static string IsValidImage(string pictureName, string pictureBase64) { if (string.IsNullOrEmpty(pictureBase64)) { return "File not found!"; } var fileData = Convert.FromBase64String(pictureBase64); if (fileData.Length <= 0) { return "File length is 0!"; } if (fileData.Length > ImageMaximumBytes) { return "Maximum length is 512KB"; } if (!IsExtensionValid(pictureName)) { return "File is not image"; } return null; } public static async Task DataToBase64(IFileListEntry fileItem) { using (var reader = new StreamReader(fileItem.Data)) { using (var memStream = new MemoryStream()) { await reader.BaseStream.CopyToAsync(memStream); var fileData = memStream.ToArray(); var encodedBase64 = Convert.ToBase64String(fileData); return encodedBase64; } } } private static bool IsExtensionValid(string fileName) { var extension = Path.GetExtension(fileName); return string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".gif", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase); } } ================================================ FILE: src/BlazorShared/Models/CatalogType.cs ================================================ using BlazorShared.Attributes; namespace BlazorShared.Models; [Endpoint(Name = "catalog-types")] public class CatalogType : LookupData { } ================================================ FILE: src/BlazorShared/Models/CatalogTypeResponse.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; using BlazorShared.Interfaces; namespace BlazorShared.Models; public class CatalogTypeResponse : ILookupDataResponse { [JsonPropertyName("CatalogTypes")] public List List { get; set; } = new List(); } ================================================ FILE: src/BlazorShared/Models/CreateCatalogItemRequest.cs ================================================ using System.ComponentModel.DataAnnotations; namespace BlazorShared.Models; public class CreateCatalogItemRequest { public int CatalogTypeId { get; set; } public int CatalogBrandId { get; set; } [Required(ErrorMessage = "The Name field is required")] public string Name { get; set; } = string.Empty; [Required(ErrorMessage = "The Description field is required")] public string Description { get; set; } = string.Empty; // decimal(18,2) [RegularExpression(@"^\d+(\.\d{0,2})*$", ErrorMessage = "The field Price must be a positive number with maximum two decimals.")] [Range(0.01, 1000)] [DataType(DataType.Currency)] public decimal Price { get; set; } = 0; public string PictureUri { get; set; } = string.Empty; public string PictureBase64 { get; set; } = string.Empty; public string PictureName { get; set; } = string.Empty; } ================================================ FILE: src/BlazorShared/Models/CreateCatalogItemResponse.cs ================================================ namespace BlazorShared.Models; public class CreateCatalogItemResponse { public CatalogItem CatalogItem { get; set; } = new CatalogItem(); } ================================================ FILE: src/BlazorShared/Models/DeleteCatalogItemResponse.cs ================================================ namespace BlazorShared.Models; public class DeleteCatalogItemResponse { public string Status { get; set; } = "Deleted"; } ================================================ FILE: src/BlazorShared/Models/EditCatalogItemResponse.cs ================================================ namespace BlazorShared.Models; public class EditCatalogItemResult { public CatalogItem CatalogItem { get; set; } = new CatalogItem(); } ================================================ FILE: src/BlazorShared/Models/ErrorDetails.cs ================================================ using System.Text.Json; namespace BlazorShared.Models; public class ErrorDetails { public int StatusCode { get; set; } public string Message { get; set; } public override string ToString() { return JsonSerializer.Serialize(this); } } ================================================ FILE: src/BlazorShared/Models/LookupData.cs ================================================ namespace BlazorShared.Models; public abstract class LookupData { public int Id { get; set; } public string Name { get; set; } } ================================================ FILE: src/BlazorShared/Models/PagedCatalogItemResponse.cs ================================================ using System.Collections.Generic; namespace BlazorShared.Models; public class PagedCatalogItemResponse { public List CatalogItems { get; set; } = new List(); public int PageCount { get; set; } = 0; } ================================================ FILE: src/Infrastructure/Data/CatalogContext.cs ================================================ using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.Infrastructure.Data; public class CatalogContext : DbContext { #pragma warning disable CS8618 // Required by Entity Framework public CatalogContext(DbContextOptions options) : base(options) {} public DbSet Baskets { get; set; } public DbSet CatalogItems { get; set; } public DbSet CatalogBrands { get; set; } public DbSet CatalogTypes { get; set; } public DbSet Orders { get; set; } public DbSet OrderItems { get; set; } public DbSet BasketItems { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } } ================================================ FILE: src/Infrastructure/Data/CatalogContextSeed.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.Infrastructure.Data; public class CatalogContextSeed { public static async Task SeedAsync(CatalogContext catalogContext, ILogger logger, int retry = 0) { var retryForAvailability = retry; try { if (catalogContext.Database.IsSqlServer()) { catalogContext.Database.Migrate(); } if (!await catalogContext.CatalogBrands.AnyAsync()) { await catalogContext.CatalogBrands.AddRangeAsync( GetPreconfiguredCatalogBrands()); await catalogContext.SaveChangesAsync(); } if (!await catalogContext.CatalogTypes.AnyAsync()) { await catalogContext.CatalogTypes.AddRangeAsync( GetPreconfiguredCatalogTypes()); await catalogContext.SaveChangesAsync(); } if (!await catalogContext.CatalogItems.AnyAsync()) { await catalogContext.CatalogItems.AddRangeAsync( GetPreconfiguredItems()); await catalogContext.SaveChangesAsync(); } } catch (Exception ex) { if (retryForAvailability >= 10) throw; retryForAvailability++; logger.LogError(ex.Message); await SeedAsync(catalogContext, logger, retryForAvailability); throw; } } static IEnumerable GetPreconfiguredCatalogBrands() { return new List { new("Azure"), new(".NET"), new("Visual Studio"), new("SQL Server"), new("Other") }; } static IEnumerable GetPreconfiguredCatalogTypes() { return new List { new("Mug"), new("T-Shirt"), new("Sheet"), new("USB Memory Stick") }; } static IEnumerable GetPreconfiguredItems() { return new List { new(2,2, ".NET Bot Black Sweatshirt", ".NET Bot Black Sweatshirt", 19.5M, "http://catalogbaseurltobereplaced/images/products/1.png"), new(1,2, ".NET Black & White Mug", ".NET Black & White Mug", 8.50M, "http://catalogbaseurltobereplaced/images/products/2.png"), new(2,5, "Prism White T-Shirt", "Prism White T-Shirt", 12, "http://catalogbaseurltobereplaced/images/products/3.png"), new(2,2, ".NET Foundation Sweatshirt", ".NET Foundation Sweatshirt", 12, "http://catalogbaseurltobereplaced/images/products/4.png"), new(3,5, "Roslyn Red Sheet", "Roslyn Red Sheet", 8.5M, "http://catalogbaseurltobereplaced/images/products/5.png"), new(2,2, ".NET Blue Sweatshirt", ".NET Blue Sweatshirt", 12, "http://catalogbaseurltobereplaced/images/products/6.png"), new(2,5, "Roslyn Red T-Shirt", "Roslyn Red T-Shirt", 12, "http://catalogbaseurltobereplaced/images/products/7.png"), new(2,5, "Kudu Purple Sweatshirt", "Kudu Purple Sweatshirt", 8.5M, "http://catalogbaseurltobereplaced/images/products/8.png"), new(1,5, "Cup White Mug", "Cup White Mug", 12, "http://catalogbaseurltobereplaced/images/products/9.png"), new(3,2, ".NET Foundation Sheet", ".NET Foundation Sheet", 12, "http://catalogbaseurltobereplaced/images/products/10.png"), new(3,2, "Cup Sheet", "Cup Sheet", 8.5M, "http://catalogbaseurltobereplaced/images/products/11.png"), new(2,5, "Prism White TShirt", "Prism White TShirt", 12, "http://catalogbaseurltobereplaced/images/products/12.png") }; } } ================================================ FILE: src/Infrastructure/Data/Config/BasketConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class BasketConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items)); navigation?.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Property(b => b.BuyerId) .IsRequired() .HasMaxLength(256); } } ================================================ FILE: src/Infrastructure/Data/Config/BasketItemConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class BasketItemConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.Property(bi => bi.UnitPrice) .IsRequired(true) .HasColumnType("decimal(18,2)"); } } ================================================ FILE: src/Infrastructure/Data/Config/CatalogBrandConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class CatalogBrandConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(ci => ci.Id); builder.Property(ci => ci.Id) .UseHiLo("catalog_brand_hilo") .IsRequired(); builder.Property(cb => cb.Brand) .IsRequired() .HasMaxLength(100); } } ================================================ FILE: src/Infrastructure/Data/Config/CatalogItemConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class CatalogItemConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Catalog"); builder.Property(ci => ci.Id) .UseHiLo("catalog_hilo") .IsRequired(); builder.Property(ci => ci.Name) .IsRequired(true) .HasMaxLength(50); builder.Property(ci => ci.Price) .IsRequired(true) .HasColumnType("decimal(18,2)"); builder.Property(ci => ci.PictureUri) .IsRequired(false); builder.HasOne(ci => ci.CatalogBrand) .WithMany() .HasForeignKey(ci => ci.CatalogBrandId); builder.HasOne(ci => ci.CatalogType) .WithMany() .HasForeignKey(ci => ci.CatalogTypeId); } } ================================================ FILE: src/Infrastructure/Data/Config/CatalogTypeConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class CatalogTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(ci => ci.Id); builder.Property(ci => ci.Id) .UseHiLo("catalog_type_hilo") .IsRequired(); builder.Property(cb => cb.Type) .IsRequired() .HasMaxLength(100); } } ================================================ FILE: src/Infrastructure/Data/Config/OrderConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems)); navigation?.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Property(b => b.BuyerId) .IsRequired() .HasMaxLength(256); builder.OwnsOne(o => o.ShipToAddress, a => { a.WithOwner(); a.Property(a => a.ZipCode) .HasMaxLength(18) .IsRequired(); a.Property(a => a.Street) .HasMaxLength(180) .IsRequired(); a.Property(a => a.State) .HasMaxLength(60); a.Property(a => a.Country) .HasMaxLength(90) .IsRequired(); a.Property(a => a.City) .HasMaxLength(100) .IsRequired(); }); builder.Navigation(x => x.ShipToAddress).IsRequired(); } } ================================================ FILE: src/Infrastructure/Data/Config/OrderItemConfiguration.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.Infrastructure.Data.Config; public class OrderItemConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.OwnsOne(i => i.ItemOrdered, io => { io.WithOwner(); io.Property(cio => cio.ProductName) .HasMaxLength(50) .IsRequired(); }); builder.Property(oi => oi.UnitPrice) .IsRequired(true) .HasColumnType("decimal(18,2)"); } } ================================================ FILE: src/Infrastructure/Data/EfRepository.cs ================================================ using Ardalis.Specification.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.Infrastructure.Data; public class EfRepository : RepositoryBase, IReadRepository, IRepository where T : class, IAggregateRoot { public EfRepository(CatalogContext dbContext) : base(dbContext) { } } ================================================ FILE: src/Infrastructure/Data/FileItem.cs ================================================ namespace Microsoft.eShopWeb.Infrastructure.Data; public class FileItem { public string? FileName { get; set; } public string? Url { get; set; } public long Size { get; set; } public string? Ext { get; set; } public string? Type { get; set; } public string? DataBase64 { get; set; } } ================================================ FILE: src/Infrastructure/Data/Migrations/20201202111507_InitialModel.Designer.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.eShopWeb.Infrastructure.Data; namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations { [DbContext(typeof(CatalogContext))] [Migration("20201202111507_InitialModel")] partial class InitialModel { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .UseIdentityColumns() .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("ProductVersion", "5.0.0"); modelBuilder.HasSequence("catalog_brand_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_type_hilo") .IncrementsBy(10); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("BuyerId") .IsRequired() .HasMaxLength(40) .HasColumnType("nvarchar(40)"); b.HasKey("Id"); b.ToTable("Baskets"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("BasketId") .HasColumnType("int"); b.Property("CatalogItemId") .HasColumnType("int"); b.Property("Quantity") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("BasketId"); b.ToTable("BasketItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseHiLo("catalog_brand_hilo"); b.Property("Brand") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogBrands"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseHiLo("catalog_hilo"); b.Property("CatalogBrandId") .HasColumnType("int"); b.Property("CatalogTypeId") .HasColumnType("int"); b.Property("Description") .HasColumnType("nvarchar(max)"); b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b.Property("PictureUri") .HasColumnType("nvarchar(max)"); b.Property("Price") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("CatalogBrandId"); b.HasIndex("CatalogTypeId"); b.ToTable("Catalog"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseHiLo("catalog_type_hilo"); b.Property("Type") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogTypes"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("BuyerId") .HasColumnType("nvarchar(max)"); b.Property("OrderDate") .HasColumnType("datetimeoffset"); b.HasKey("Id"); b.ToTable("Orders"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("OrderId") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.Property("Units") .HasColumnType("int"); b.HasKey("Id"); b.HasIndex("OrderId"); b.ToTable("OrderItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", null) .WithMany("Items") .HasForeignKey("BasketId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", "CatalogBrand") .WithMany() .HasForeignKey("CatalogBrandId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", "CatalogType") .WithMany() .HasForeignKey("CatalogTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("CatalogBrand"); b.Navigation("CatalogType"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Address", "ShipToAddress", b1 => { b1.Property("OrderId") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b1.Property("City") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b1.Property("Country") .IsRequired() .HasMaxLength(90) .HasColumnType("nvarchar(90)"); b1.Property("State") .HasMaxLength(60) .HasColumnType("nvarchar(60)"); b1.Property("Street") .IsRequired() .HasMaxLength(180) .HasColumnType("nvarchar(180)"); b1.Property("ZipCode") .IsRequired() .HasMaxLength(18) .HasColumnType("nvarchar(18)"); b1.HasKey("OrderId"); b1.ToTable("Orders"); b1.WithOwner() .HasForeignKey("OrderId"); }); b.Navigation("ShipToAddress"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", null) .WithMany("OrderItems") .HasForeignKey("OrderId"); b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.CatalogItemOrdered", "ItemOrdered", b1 => { b1.Property("OrderItemId") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b1.Property("CatalogItemId") .HasColumnType("int"); b1.Property("PictureUri") .HasColumnType("nvarchar(max)"); b1.Property("ProductName") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b1.HasKey("OrderItemId"); b1.ToTable("OrderItems"); b1.WithOwner() .HasForeignKey("OrderItemId"); }); b.Navigation("ItemOrdered"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Navigation("Items"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Navigation("OrderItems"); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Infrastructure/Data/Migrations/20201202111507_InitialModel.cs ================================================ using System; using Microsoft.EntityFrameworkCore.Migrations; namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations; public partial class InitialModel : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateSequence( name: "catalog_brand_hilo", incrementBy: 10); migrationBuilder.CreateSequence( name: "catalog_hilo", incrementBy: 10); migrationBuilder.CreateSequence( name: "catalog_type_hilo", incrementBy: 10); migrationBuilder.CreateTable( name: "Baskets", columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), BuyerId = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, constraints: table => { table.PrimaryKey("PK_Baskets", x => x.Id); }); migrationBuilder.CreateTable( name: "CatalogBrands", columns: table => new { Id = table.Column(type: "int", nullable: false), Brand = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false) }, constraints: table => { table.PrimaryKey("PK_CatalogBrands", x => x.Id); }); migrationBuilder.CreateTable( name: "CatalogTypes", columns: table => new { Id = table.Column(type: "int", nullable: false), Type = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false) }, constraints: table => { table.PrimaryKey("PK_CatalogTypes", x => x.Id); }); migrationBuilder.CreateTable( name: "Orders", columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), BuyerId = table.Column(type: "nvarchar(max)", nullable: true), OrderDate = table.Column(type: "datetimeoffset", nullable: false), ShipToAddress_Street = table.Column(type: "nvarchar(180)", maxLength: 180, nullable: true), ShipToAddress_City = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), ShipToAddress_State = table.Column(type: "nvarchar(60)", maxLength: 60, nullable: true), ShipToAddress_Country = table.Column(type: "nvarchar(90)", maxLength: 90, nullable: true), ShipToAddress_ZipCode = table.Column(type: "nvarchar(18)", maxLength: 18, nullable: true) }, constraints: table => { table.PrimaryKey("PK_Orders", x => x.Id); }); migrationBuilder.CreateTable( name: "BasketItems", columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), UnitPrice = table.Column(type: "decimal(18,2)", nullable: false), Quantity = table.Column(type: "int", nullable: false), CatalogItemId = table.Column(type: "int", nullable: false), BasketId = table.Column(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey("PK_BasketItems", x => x.Id); table.ForeignKey( name: "FK_BasketItems_Baskets_BasketId", column: x => x.BasketId, principalTable: "Baskets", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "Catalog", columns: table => new { Id = table.Column(type: "int", nullable: false), Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), Description = table.Column(type: "nvarchar(max)", nullable: true), Price = table.Column(type: "decimal(18,2)", nullable: false), PictureUri = table.Column(type: "nvarchar(max)", nullable: true), CatalogTypeId = table.Column(type: "int", nullable: false), CatalogBrandId = table.Column(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Catalog", x => x.Id); table.ForeignKey( name: "FK_Catalog_CatalogBrands_CatalogBrandId", column: x => x.CatalogBrandId, principalTable: "CatalogBrands", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Catalog_CatalogTypes_CatalogTypeId", column: x => x.CatalogTypeId, principalTable: "CatalogTypes", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "OrderItems", columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), ItemOrdered_CatalogItemId = table.Column(type: "int", nullable: true), ItemOrdered_ProductName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), ItemOrdered_PictureUri = table.Column(type: "nvarchar(max)", nullable: true), UnitPrice = table.Column(type: "decimal(18,2)", nullable: false), Units = table.Column(type: "int", nullable: false), OrderId = table.Column(type: "int", nullable: true) }, constraints: table => { table.PrimaryKey("PK_OrderItems", x => x.Id); table.ForeignKey( name: "FK_OrderItems_Orders_OrderId", column: x => x.OrderId, principalTable: "Orders", principalColumn: "Id", onDelete: ReferentialAction.Restrict); }); migrationBuilder.CreateIndex( name: "IX_BasketItems_BasketId", table: "BasketItems", column: "BasketId"); migrationBuilder.CreateIndex( name: "IX_Catalog_CatalogBrandId", table: "Catalog", column: "CatalogBrandId"); migrationBuilder.CreateIndex( name: "IX_Catalog_CatalogTypeId", table: "Catalog", column: "CatalogTypeId"); migrationBuilder.CreateIndex( name: "IX_OrderItems_OrderId", table: "OrderItems", column: "OrderId"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "BasketItems"); migrationBuilder.DropTable( name: "Catalog"); migrationBuilder.DropTable( name: "OrderItems"); migrationBuilder.DropTable( name: "Baskets"); migrationBuilder.DropTable( name: "CatalogBrands"); migrationBuilder.DropTable( name: "CatalogTypes"); migrationBuilder.DropTable( name: "Orders"); migrationBuilder.DropSequence( name: "catalog_brand_hilo"); migrationBuilder.DropSequence( name: "catalog_hilo"); migrationBuilder.DropSequence( name: "catalog_type_hilo"); } } ================================================ FILE: src/Infrastructure/Data/Migrations/20211026175614_FixBuyerId.Designer.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.eShopWeb.Infrastructure.Data; namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations { [DbContext(typeof(CatalogContext))] [Migration("20211026175614_FixBuyerId")] partial class FixBuyerId { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("ProductVersion", "5.0.11") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.HasSequence("catalog_brand_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_type_hilo") .IncrementsBy(10); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("BuyerId") .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.ToTable("Baskets"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("BasketId") .HasColumnType("int"); b.Property("CatalogItemId") .HasColumnType("int"); b.Property("Quantity") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("BasketId"); b.ToTable("BasketItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("Brand") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogBrands"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("CatalogBrandId") .HasColumnType("int"); b.Property("CatalogTypeId") .HasColumnType("int"); b.Property("Description") .HasColumnType("nvarchar(max)"); b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b.Property("PictureUri") .HasColumnType("nvarchar(max)"); b.Property("Price") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("CatalogBrandId"); b.HasIndex("CatalogTypeId"); b.ToTable("Catalog"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("Type") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogTypes"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("BuyerId") .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("OrderDate") .HasColumnType("datetimeoffset"); b.HasKey("Id"); b.ToTable("Orders"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("OrderId") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.Property("Units") .HasColumnType("int"); b.HasKey("Id"); b.HasIndex("OrderId"); b.ToTable("OrderItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", null) .WithMany("Items") .HasForeignKey("BasketId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", "CatalogBrand") .WithMany() .HasForeignKey("CatalogBrandId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", "CatalogType") .WithMany() .HasForeignKey("CatalogTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("CatalogBrand"); b.Navigation("CatalogType"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Address", "ShipToAddress", b1 => { b1.Property("OrderId") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b1.Property("City") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b1.Property("Country") .IsRequired() .HasMaxLength(90) .HasColumnType("nvarchar(90)"); b1.Property("State") .HasMaxLength(60) .HasColumnType("nvarchar(60)"); b1.Property("Street") .IsRequired() .HasMaxLength(180) .HasColumnType("nvarchar(180)"); b1.Property("ZipCode") .IsRequired() .HasMaxLength(18) .HasColumnType("nvarchar(18)"); b1.HasKey("OrderId"); b1.ToTable("Orders"); b1.WithOwner() .HasForeignKey("OrderId"); }); b.Navigation("ShipToAddress"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", null) .WithMany("OrderItems") .HasForeignKey("OrderId"); b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.CatalogItemOrdered", "ItemOrdered", b1 => { b1.Property("OrderItemId") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b1.Property("CatalogItemId") .HasColumnType("int"); b1.Property("PictureUri") .HasColumnType("nvarchar(max)"); b1.Property("ProductName") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b1.HasKey("OrderItemId"); b1.ToTable("OrderItems"); b1.WithOwner() .HasForeignKey("OrderItemId"); }); b.Navigation("ItemOrdered"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Navigation("Items"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Navigation("OrderItems"); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Infrastructure/Data/Migrations/20211026175614_FixBuyerId.cs ================================================ using Microsoft.EntityFrameworkCore.Migrations; namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations; public partial class FixBuyerId : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( name: "BuyerId", table: "Orders", type: "nvarchar(256)", maxLength: 256, nullable: false, defaultValue: "", oldClrType: typeof(string), oldType: "nvarchar(max)", oldNullable: true); migrationBuilder.AlterColumn( name: "BuyerId", table: "Baskets", type: "nvarchar(256)", maxLength: 256, nullable: false, oldClrType: typeof(string), oldType: "nvarchar(40)", oldMaxLength: 40); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( name: "BuyerId", table: "Orders", type: "nvarchar(max)", nullable: true, oldClrType: typeof(string), oldType: "nvarchar(256)", oldMaxLength: 256); migrationBuilder.AlterColumn( name: "BuyerId", table: "Baskets", type: "nvarchar(40)", maxLength: 40, nullable: false, oldClrType: typeof(string), oldType: "nvarchar(256)", oldMaxLength: 256); } } ================================================ FILE: src/Infrastructure/Data/Migrations/20211231093753_FixShipToAddress.Designer.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.eShopWeb.Infrastructure.Data; #nullable disable namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations { [DbContext(typeof(CatalogContext))] [Migration("20211231093753_FixShipToAddress")] partial class FixShipToAddress { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "6.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); modelBuilder.HasSequence("catalog_brand_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_type_hilo") .IncrementsBy(10); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("BuyerId") .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.ToTable("Baskets"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("BasketId") .HasColumnType("int"); b.Property("CatalogItemId") .HasColumnType("int"); b.Property("Quantity") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("BasketId"); b.ToTable("BasketItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "catalog_brand_hilo"); b.Property("Brand") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogBrands"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "catalog_hilo"); b.Property("CatalogBrandId") .HasColumnType("int"); b.Property("CatalogTypeId") .HasColumnType("int"); b.Property("Description") .HasColumnType("nvarchar(max)"); b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b.Property("PictureUri") .HasColumnType("nvarchar(max)"); b.Property("Price") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("CatalogBrandId"); b.HasIndex("CatalogTypeId"); b.ToTable("Catalog", (string)null); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "catalog_type_hilo"); b.Property("Type") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogTypes"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("BuyerId") .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("OrderDate") .HasColumnType("datetimeoffset"); b.HasKey("Id"); b.ToTable("Orders"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("OrderId") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.Property("Units") .HasColumnType("int"); b.HasKey("Id"); b.HasIndex("OrderId"); b.ToTable("OrderItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", null) .WithMany("Items") .HasForeignKey("BasketId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", "CatalogBrand") .WithMany() .HasForeignKey("CatalogBrandId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", "CatalogType") .WithMany() .HasForeignKey("CatalogTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("CatalogBrand"); b.Navigation("CatalogType"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Address", "ShipToAddress", b1 => { b1.Property("OrderId") .HasColumnType("int"); b1.Property("City") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b1.Property("Country") .IsRequired() .HasMaxLength(90) .HasColumnType("nvarchar(90)"); b1.Property("State") .HasMaxLength(60) .HasColumnType("nvarchar(60)"); b1.Property("Street") .IsRequired() .HasMaxLength(180) .HasColumnType("nvarchar(180)"); b1.Property("ZipCode") .IsRequired() .HasMaxLength(18) .HasColumnType("nvarchar(18)"); b1.HasKey("OrderId"); b1.ToTable("Orders"); b1.WithOwner() .HasForeignKey("OrderId"); }); b.Navigation("ShipToAddress") .IsRequired(); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", null) .WithMany("OrderItems") .HasForeignKey("OrderId"); b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.CatalogItemOrdered", "ItemOrdered", b1 => { b1.Property("OrderItemId") .HasColumnType("int"); b1.Property("CatalogItemId") .HasColumnType("int"); b1.Property("PictureUri") .HasColumnType("nvarchar(max)"); b1.Property("ProductName") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b1.HasKey("OrderItemId"); b1.ToTable("OrderItems"); b1.WithOwner() .HasForeignKey("OrderItemId"); }); b.Navigation("ItemOrdered"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Navigation("Items"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Navigation("OrderItems"); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Infrastructure/Data/Migrations/20211231093753_FixShipToAddress.cs ================================================ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations { public partial class FixShipToAddress : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( name: "ShipToAddress_ZipCode", table: "Orders", type: "nvarchar(18)", maxLength: 18, nullable: false, defaultValue: "", oldClrType: typeof(string), oldType: "nvarchar(18)", oldMaxLength: 18, oldNullable: true); migrationBuilder.AlterColumn( name: "ShipToAddress_Street", table: "Orders", type: "nvarchar(180)", maxLength: 180, nullable: false, defaultValue: "", oldClrType: typeof(string), oldType: "nvarchar(180)", oldMaxLength: 180, oldNullable: true); migrationBuilder.AlterColumn( name: "ShipToAddress_Country", table: "Orders", type: "nvarchar(90)", maxLength: 90, nullable: false, defaultValue: "", oldClrType: typeof(string), oldType: "nvarchar(90)", oldMaxLength: 90, oldNullable: true); migrationBuilder.AlterColumn( name: "ShipToAddress_City", table: "Orders", type: "nvarchar(100)", maxLength: 100, nullable: false, defaultValue: "", oldClrType: typeof(string), oldType: "nvarchar(100)", oldMaxLength: 100, oldNullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( name: "ShipToAddress_ZipCode", table: "Orders", type: "nvarchar(18)", maxLength: 18, nullable: true, oldClrType: typeof(string), oldType: "nvarchar(18)", oldMaxLength: 18); migrationBuilder.AlterColumn( name: "ShipToAddress_Street", table: "Orders", type: "nvarchar(180)", maxLength: 180, nullable: true, oldClrType: typeof(string), oldType: "nvarchar(180)", oldMaxLength: 180); migrationBuilder.AlterColumn( name: "ShipToAddress_Country", table: "Orders", type: "nvarchar(90)", maxLength: 90, nullable: true, oldClrType: typeof(string), oldType: "nvarchar(90)", oldMaxLength: 90); migrationBuilder.AlterColumn( name: "ShipToAddress_City", table: "Orders", type: "nvarchar(100)", maxLength: 100, nullable: true, oldClrType: typeof(string), oldType: "nvarchar(100)", oldMaxLength: 100); } } } ================================================ FILE: src/Infrastructure/Data/Migrations/CatalogContextModelSnapshot.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.eShopWeb.Infrastructure.Data; #nullable disable namespace Microsoft.eShopWeb.Infrastructure.Data.Migrations { [DbContext(typeof(CatalogContext))] partial class CatalogContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "6.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); modelBuilder.HasSequence("catalog_brand_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_hilo") .IncrementsBy(10); modelBuilder.HasSequence("catalog_type_hilo") .IncrementsBy(10); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("BuyerId") .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.ToTable("Baskets"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("BasketId") .HasColumnType("int"); b.Property("CatalogItemId") .HasColumnType("int"); b.Property("Quantity") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("BasketId"); b.ToTable("BasketItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "catalog_brand_hilo"); b.Property("Brand") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogBrands"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "catalog_hilo"); b.Property("CatalogBrandId") .HasColumnType("int"); b.Property("CatalogTypeId") .HasColumnType("int"); b.Property("Description") .HasColumnType("nvarchar(max)"); b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b.Property("PictureUri") .HasColumnType("nvarchar(max)"); b.Property("Price") .HasColumnType("decimal(18,2)"); b.HasKey("Id"); b.HasIndex("CatalogBrandId"); b.HasIndex("CatalogTypeId"); b.ToTable("Catalog", (string)null); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "catalog_type_hilo"); b.Property("Type") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b.HasKey("Id"); b.ToTable("CatalogTypes"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("BuyerId") .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("OrderDate") .HasColumnType("datetimeoffset"); b.HasKey("Id"); b.ToTable("Orders"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); b.Property("OrderId") .HasColumnType("int"); b.Property("UnitPrice") .HasColumnType("decimal(18,2)"); b.Property("Units") .HasColumnType("int"); b.HasKey("Id"); b.HasIndex("OrderId"); b.ToTable("OrderItems"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.BasketItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", null) .WithMany("Items") .HasForeignKey("BasketId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogBrand", "CatalogBrand") .WithMany() .HasForeignKey("CatalogBrandId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.CatalogType", "CatalogType") .WithMany() .HasForeignKey("CatalogTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("CatalogBrand"); b.Navigation("CatalogType"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Address", "ShipToAddress", b1 => { b1.Property("OrderId") .HasColumnType("int"); b1.Property("City") .IsRequired() .HasMaxLength(100) .HasColumnType("nvarchar(100)"); b1.Property("Country") .IsRequired() .HasMaxLength(90) .HasColumnType("nvarchar(90)"); b1.Property("State") .HasMaxLength(60) .HasColumnType("nvarchar(60)"); b1.Property("Street") .IsRequired() .HasMaxLength(180) .HasColumnType("nvarchar(180)"); b1.Property("ZipCode") .IsRequired() .HasMaxLength(18) .HasColumnType("nvarchar(18)"); b1.HasKey("OrderId"); b1.ToTable("Orders"); b1.WithOwner() .HasForeignKey("OrderId"); }); b.Navigation("ShipToAddress") .IsRequired(); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.OrderItem", b => { b.HasOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", null) .WithMany("OrderItems") .HasForeignKey("OrderId"); b.OwnsOne("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.CatalogItemOrdered", "ItemOrdered", b1 => { b1.Property("OrderItemId") .HasColumnType("int"); b1.Property("CatalogItemId") .HasColumnType("int"); b1.Property("PictureUri") .HasColumnType("nvarchar(max)"); b1.Property("ProductName") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); b1.HasKey("OrderItemId"); b1.ToTable("OrderItems"); b1.WithOwner() .HasForeignKey("OrderItemId"); }); b.Navigation("ItemOrdered"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate.Basket", b => { b.Navigation("Items"); }); modelBuilder.Entity("Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate.Order", b => { b.Navigation("OrderItems"); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Infrastructure/Data/Queries/BasketQueryService.cs ================================================ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.Infrastructure.Data.Queries; public class BasketQueryService : IBasketQueryService { private readonly CatalogContext _dbContext; public BasketQueryService(CatalogContext dbContext) { _dbContext = dbContext; } /// /// This method performs the sum on the database rather than in memory /// /// /// public async Task CountTotalBasketItems(string username) { var totalItems = await _dbContext.Baskets .Where(basket => basket.BuyerId == username) .SelectMany(item => item.Items) .SumAsync(sum => sum.Quantity); return totalItems; } } ================================================ FILE: src/Infrastructure/Dependencies.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.eShopWeb.Infrastructure; public static class Dependencies { public static void ConfigureServices(IConfiguration configuration, IServiceCollection services) { bool useOnlyInMemoryDatabase = false; if (configuration["UseOnlyInMemoryDatabase"] != null) { useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]!); } if (useOnlyInMemoryDatabase) { services.AddDbContext(c => c.UseInMemoryDatabase("Catalog")); services.AddDbContext(options => options.UseInMemoryDatabase("Identity")); } else { // use real database // Requires LocalDB which can be installed with SQL Server Express 2016 // https://www.microsoft.com/en-us/download/details.aspx?id=54284 services.AddDbContext(c => c.UseSqlServer(configuration.GetConnectionString("CatalogConnection"))); // Add Identity DbContext services.AddDbContext(options => options.UseSqlServer(configuration.GetConnectionString("IdentityConnection"))); } } } ================================================ FILE: src/Infrastructure/Identity/AppIdentityDbContext.cs ================================================ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Microsoft.eShopWeb.Infrastructure.Identity; public class AppIdentityDbContext : IdentityDbContext { public AppIdentityDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Customize the ASP.NET Identity model and override the defaults if needed. // For example, you can rename the ASP.NET Identity table names and more. // Add your customizations after calling base.OnModelCreating(builder); } } ================================================ FILE: src/Infrastructure/Identity/AppIdentityDbContextSeed.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Constants; namespace Microsoft.eShopWeb.Infrastructure.Identity; public class AppIdentityDbContextSeed { public static async Task SeedAsync(AppIdentityDbContext identityDbContext, UserManager userManager, RoleManager roleManager) { if (identityDbContext.Database.IsSqlServer()) { identityDbContext.Database.Migrate(); } await roleManager.CreateAsync(new IdentityRole(BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)); var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; await userManager.CreateAsync(defaultUser, AuthorizationConstants.DEFAULT_PASSWORD); string adminUserName = "admin@microsoft.com"; var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName }; await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD); adminUser = await userManager.FindByNameAsync(adminUserName); if (adminUser != null) { await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS); } } } ================================================ FILE: src/Infrastructure/Identity/ApplicationUser.cs ================================================ using Microsoft.AspNetCore.Identity; namespace Microsoft.eShopWeb.Infrastructure.Identity; public class ApplicationUser : IdentityUser { } ================================================ FILE: src/Infrastructure/Identity/IdentityTokenClaimService.cs ================================================ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.IdentityModel.Tokens; namespace Microsoft.eShopWeb.Infrastructure.Identity; public class IdentityTokenClaimService : ITokenClaimsService { private readonly UserManager _userManager; public IdentityTokenClaimService(UserManager userManager) { _userManager = userManager; } public async Task GetTokenAsync(string userName) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); var user = await _userManager.FindByNameAsync(userName); if (user == null) throw new UserNotFoundException(userName); var roles = await _userManager.GetRolesAsync(user); var claims = new List { new Claim(ClaimTypes.Name, userName) }; foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims.ToArray()), Expires = DateTime.UtcNow.AddDays(7), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } } ================================================ FILE: src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.Designer.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.eShopWeb.Infrastructure.Identity; namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations { [DbContext(typeof(AppIdentityDbContext))] [Migration("20201202111612_InitialIdentityModel")] partial class InitialIdentityModel { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .UseIdentityColumns() .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("ProductVersion", "5.0.0"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); b.Property("Name") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("NormalizedName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.HasIndex("NormalizedName") .IsUnique() .HasDatabaseName("RoleNameIndex") .HasFilter("[NormalizedName] IS NOT NULL"); b.ToTable("AspNetRoles"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("ClaimType") .HasColumnType("nvarchar(max)"); b.Property("ClaimValue") .HasColumnType("nvarchar(max)"); b.Property("RoleId") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("Id"); b.HasIndex("RoleId"); b.ToTable("AspNetRoleClaims"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("ClaimType") .HasColumnType("nvarchar(max)"); b.Property("ClaimValue") .HasColumnType("nvarchar(max)"); b.Property("UserId") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("Id"); b.HasIndex("UserId"); b.ToTable("AspNetUserClaims"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.Property("LoginProvider") .HasColumnType("nvarchar(450)"); b.Property("ProviderKey") .HasColumnType("nvarchar(450)"); b.Property("ProviderDisplayName") .HasColumnType("nvarchar(max)"); b.Property("UserId") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("LoginProvider", "ProviderKey"); b.HasIndex("UserId"); b.ToTable("AspNetUserLogins"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { b.Property("UserId") .HasColumnType("nvarchar(450)"); b.Property("RoleId") .HasColumnType("nvarchar(450)"); b.HasKey("UserId", "RoleId"); b.HasIndex("RoleId"); b.ToTable("AspNetUserRoles"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.Property("UserId") .HasColumnType("nvarchar(450)"); b.Property("LoginProvider") .HasColumnType("nvarchar(450)"); b.Property("Name") .HasColumnType("nvarchar(450)"); b.Property("Value") .HasColumnType("nvarchar(max)"); b.HasKey("UserId", "LoginProvider", "Name"); b.ToTable("AspNetUserTokens"); }); modelBuilder.Entity("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); b.Property("AccessFailedCount") .HasColumnType("int"); b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); b.Property("Email") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("EmailConfirmed") .HasColumnType("bit"); b.Property("LockoutEnabled") .HasColumnType("bit"); b.Property("LockoutEnd") .HasColumnType("datetimeoffset"); b.Property("NormalizedEmail") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("NormalizedUserName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("PasswordHash") .HasColumnType("nvarchar(max)"); b.Property("PhoneNumber") .HasColumnType("nvarchar(max)"); b.Property("PhoneNumberConfirmed") .HasColumnType("bit"); b.Property("SecurityStamp") .HasColumnType("nvarchar(max)"); b.Property("TwoFactorEnabled") .HasColumnType("bit"); b.Property("UserName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.HasIndex("NormalizedEmail") .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") .IsUnique() .HasDatabaseName("UserNameIndex") .HasFilter("[NormalizedUserName] IS NOT NULL"); b.ToTable("AspNetUsers"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Infrastructure/Identity/Migrations/20201202111612_InitialIdentityModel.cs ================================================ using System; using Microsoft.EntityFrameworkCore.Migrations; namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations; public partial class InitialIdentityModel : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "AspNetRoles", columns: table => new { Id = table.Column(type: "nvarchar(450)", nullable: false), Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { table.PrimaryKey("PK_AspNetRoles", x => x.Id); }); migrationBuilder.CreateTable( name: "AspNetUsers", columns: table => new { Id = table.Column(type: "nvarchar(450)", nullable: false), UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), EmailConfirmed = table.Column(type: "bit", nullable: false), PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), TwoFactorEnabled = table.Column(type: "bit", nullable: false), LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), LockoutEnabled = table.Column(type: "bit", nullable: false), AccessFailedCount = table.Column(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey("PK_AspNetUsers", x => x.Id); }); migrationBuilder.CreateTable( name: "AspNetRoleClaims", columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), RoleId = table.Column(type: "nvarchar(450)", nullable: false), ClaimType = table.Column(type: "nvarchar(max)", nullable: true), ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); table.ForeignKey( name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", column: x => x.RoleId, principalTable: "AspNetRoles", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "AspNetUserClaims", columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), UserId = table.Column(type: "nvarchar(450)", nullable: false), ClaimType = table.Column(type: "nvarchar(max)", nullable: true), ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); table.ForeignKey( name: "FK_AspNetUserClaims_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "AspNetUserLogins", columns: table => new { LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), UserId = table.Column(type: "nvarchar(450)", nullable: false) }, constraints: table => { table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); table.ForeignKey( name: "FK_AspNetUserLogins_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "AspNetUserRoles", columns: table => new { UserId = table.Column(type: "nvarchar(450)", nullable: false), RoleId = table.Column(type: "nvarchar(450)", nullable: false) }, constraints: table => { table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); table.ForeignKey( name: "FK_AspNetUserRoles_AspNetRoles_RoleId", column: x => x.RoleId, principalTable: "AspNetRoles", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_AspNetUserRoles_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "AspNetUserTokens", columns: table => new { UserId = table.Column(type: "nvarchar(450)", nullable: false), LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), Name = table.Column(type: "nvarchar(450)", nullable: false), Value = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); table.ForeignKey( name: "FK_AspNetUserTokens_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "IX_AspNetRoleClaims_RoleId", table: "AspNetRoleClaims", column: "RoleId"); migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "AspNetRoles", column: "NormalizedName", unique: true, filter: "[NormalizedName] IS NOT NULL"); migrationBuilder.CreateIndex( name: "IX_AspNetUserClaims_UserId", table: "AspNetUserClaims", column: "UserId"); migrationBuilder.CreateIndex( name: "IX_AspNetUserLogins_UserId", table: "AspNetUserLogins", column: "UserId"); migrationBuilder.CreateIndex( name: "IX_AspNetUserRoles_RoleId", table: "AspNetUserRoles", column: "RoleId"); migrationBuilder.CreateIndex( name: "EmailIndex", table: "AspNetUsers", column: "NormalizedEmail"); migrationBuilder.CreateIndex( name: "UserNameIndex", table: "AspNetUsers", column: "NormalizedUserName", unique: true, filter: "[NormalizedUserName] IS NOT NULL"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "AspNetRoleClaims"); migrationBuilder.DropTable( name: "AspNetUserClaims"); migrationBuilder.DropTable( name: "AspNetUserLogins"); migrationBuilder.DropTable( name: "AspNetUserRoles"); migrationBuilder.DropTable( name: "AspNetUserTokens"); migrationBuilder.DropTable( name: "AspNetRoles"); migrationBuilder.DropTable( name: "AspNetUsers"); } } ================================================ FILE: src/Infrastructure/Identity/Migrations/AppIdentityDbContextModelSnapshot.cs ================================================ // using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.eShopWeb.Infrastructure.Identity; namespace Microsoft.eShopWeb.Infrastructure.Identity.Migrations { [DbContext(typeof(AppIdentityDbContext))] partial class AppIdentityDbContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .UseIdentityColumns() .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("ProductVersion", "5.0.0"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); b.Property("Name") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("NormalizedName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.HasIndex("NormalizedName") .IsUnique() .HasDatabaseName("RoleNameIndex") .HasFilter("[NormalizedName] IS NOT NULL"); b.ToTable("AspNetRoles"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("ClaimType") .HasColumnType("nvarchar(max)"); b.Property("ClaimValue") .HasColumnType("nvarchar(max)"); b.Property("RoleId") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("Id"); b.HasIndex("RoleId"); b.ToTable("AspNetRoleClaims"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .UseIdentityColumn(); b.Property("ClaimType") .HasColumnType("nvarchar(max)"); b.Property("ClaimValue") .HasColumnType("nvarchar(max)"); b.Property("UserId") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("Id"); b.HasIndex("UserId"); b.ToTable("AspNetUserClaims"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.Property("LoginProvider") .HasColumnType("nvarchar(450)"); b.Property("ProviderKey") .HasColumnType("nvarchar(450)"); b.Property("ProviderDisplayName") .HasColumnType("nvarchar(max)"); b.Property("UserId") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("LoginProvider", "ProviderKey"); b.HasIndex("UserId"); b.ToTable("AspNetUserLogins"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { b.Property("UserId") .HasColumnType("nvarchar(450)"); b.Property("RoleId") .HasColumnType("nvarchar(450)"); b.HasKey("UserId", "RoleId"); b.HasIndex("RoleId"); b.ToTable("AspNetUserRoles"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.Property("UserId") .HasColumnType("nvarchar(450)"); b.Property("LoginProvider") .HasColumnType("nvarchar(450)"); b.Property("Name") .HasColumnType("nvarchar(450)"); b.Property("Value") .HasColumnType("nvarchar(max)"); b.HasKey("UserId", "LoginProvider", "Name"); b.ToTable("AspNetUserTokens"); }); modelBuilder.Entity("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); b.Property("AccessFailedCount") .HasColumnType("int"); b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); b.Property("Email") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("EmailConfirmed") .HasColumnType("bit"); b.Property("LockoutEnabled") .HasColumnType("bit"); b.Property("LockoutEnd") .HasColumnType("datetimeoffset"); b.Property("NormalizedEmail") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("NormalizedUserName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.Property("PasswordHash") .HasColumnType("nvarchar(max)"); b.Property("PhoneNumber") .HasColumnType("nvarchar(max)"); b.Property("PhoneNumberConfirmed") .HasColumnType("bit"); b.Property("SecurityStamp") .HasColumnType("nvarchar(max)"); b.Property("TwoFactorEnabled") .HasColumnType("bit"); b.Property("UserName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); b.HasKey("Id"); b.HasIndex("NormalizedEmail") .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") .IsUnique() .HasDatabaseName("UserNameIndex") .HasFilter("[NormalizedUserName] IS NOT NULL"); b.ToTable("AspNetUsers"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.HasOne("Microsoft.eShopWeb.Infrastructure.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Infrastructure/Identity/UserNotFoundException.cs ================================================ using System; namespace Microsoft.eShopWeb.Infrastructure.Identity; public class UserNotFoundException : Exception { public UserNotFoundException(string userName) : base($"No user found with username: {userName}") { } } ================================================ FILE: src/Infrastructure/Infrastructure.csproj ================================================  Microsoft.eShopWeb.Infrastructure enable ================================================ FILE: src/Infrastructure/Logging/LoggerAdapter.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.Infrastructure.Logging; public class LoggerAdapter : IAppLogger { private readonly ILogger _logger; public LoggerAdapter(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); } public void LogWarning(string message, params object[] args) { _logger.LogWarning(message, args); } public void LogInformation(string message, params object[] args) { _logger.LogInformation(message, args); } } ================================================ FILE: src/Infrastructure/Services/EmailSender.cs ================================================ using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.Infrastructure.Services; // This class is used by the application to send email for account confirmation and password reset. // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 public class EmailSender : IEmailSender { public Task SendEmailAsync(string email, string subject, string message) { // TODO: Wire this up to actual email sending logic via SendGrid, local SMTP, etc. return Task.CompletedTask; } } ================================================ FILE: src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; public class AuthenticateRequest : BaseRequest { public string Username { get; set; } public string Password { get; set; } } ================================================ FILE: src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; public class AuthenticateResponse : BaseResponse { public AuthenticateResponse(Guid correlationId) : base(correlationId) { } public AuthenticateResponse() { } public bool Result { get; set; } = false; public string Token { get; set; } = string.Empty; public string Username { get; set; } = string.Empty; public bool IsLockedOut { get; set; } = false; public bool IsNotAllowed { get; set; } = false; public bool RequiresTwoFactor { get; set; } = false; } ================================================ FILE: src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; public class ClaimValue { public ClaimValue() { } public ClaimValue(string type, string value) { Type = type; Value = value; } public string Type { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; } ================================================ FILE: src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs ================================================ using System.Collections.Generic; namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; public class UserInfo { public static readonly UserInfo Anonymous = new UserInfo(); public bool IsAuthenticated { get; set; } public string NameClaimType { get; set; } = string.Empty; public string RoleClaimType { get; set; } = string.Empty; public IEnumerable Claims { get; set; } = new List(); } ================================================ FILE: src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs ================================================ using System.Threading; using System.Threading.Tasks; using Ardalis.ApiEndpoints; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; using Swashbuckle.AspNetCore.Annotations; namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; /// /// Authenticates a user /// public class AuthenticateEndpoint : EndpointBaseAsync .WithRequest .WithActionResult { private readonly SignInManager _signInManager; private readonly ITokenClaimsService _tokenClaimsService; public AuthenticateEndpoint(SignInManager signInManager, ITokenClaimsService tokenClaimsService) { _signInManager = signInManager; _tokenClaimsService = tokenClaimsService; } [HttpPost("api/authenticate")] [SwaggerOperation( Summary = "Authenticates a user", Description = "Authenticates a user", OperationId = "auth.authenticate", Tags = new[] { "AuthEndpoints" }) ] public override async Task> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken = default) { var response = new AuthenticateResponse(request.CorrelationId()); // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true //var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); var result = await _signInManager.PasswordSignInAsync(request.Username, request.Password, false, true); response.Result = result.Succeeded; response.IsLockedOut = result.IsLockedOut; response.IsNotAllowed = result.IsNotAllowed; response.RequiresTwoFactor = result.RequiresTwoFactor; response.Username = request.Username; if (result.Succeeded) { response.Token = await _tokenClaimsService.GetTokenAsync(request.Username); } return response; } } ================================================ FILE: src/PublicApi/BaseMessage.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi; /// /// Base class used by API requests /// public abstract class BaseMessage { /// /// Unique Identifier used by logging /// protected Guid _correlationId = Guid.NewGuid(); public Guid CorrelationId() => _correlationId; } ================================================ FILE: src/PublicApi/BaseRequest.cs ================================================ namespace Microsoft.eShopWeb.PublicApi; /// /// Base class used by API requests /// public abstract class BaseRequest : BaseMessage { } ================================================ FILE: src/PublicApi/BaseResponse.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi; /// /// Base class used by API responses /// public abstract class BaseResponse : BaseMessage { public BaseResponse(Guid correlationId) : base() { base._correlationId = correlationId; } public BaseResponse() { } } ================================================ FILE: src/PublicApi/CatalogBrandEndpoints/CatalogBrandDto.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; public class CatalogBrandDto { public int Id { get; set; } public string Name { get; set; } } ================================================ FILE: src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.ListCatalogBrandsResponse.cs ================================================ using System; using System.Collections.Generic; namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; public class ListCatalogBrandsResponse : BaseResponse { public ListCatalogBrandsResponse(Guid correlationId) : base(correlationId) { } public ListCatalogBrandsResponse() { } public List CatalogBrands { get; set; } = new List(); } ================================================ FILE: src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs ================================================ using System.Linq; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; /// /// List Catalog Brands /// public class CatalogBrandListEndpoint : IEndpoint> { private readonly IMapper _mapper; public CatalogBrandListEndpoint(IMapper mapper) { _mapper = mapper; } public void AddRoute(IEndpointRouteBuilder app) { app.MapGet("api/catalog-brands", async (IRepository catalogBrandRepository) => { return await HandleAsync(catalogBrandRepository); }) .Produces() .WithTags("CatalogBrandEndpoints"); } public async Task HandleAsync(IRepository catalogBrandRepository) { var response = new ListCatalogBrandsResponse(); var items = await catalogBrandRepository.ListAsync(); response.CatalogBrands.AddRange(items.Select(_mapper.Map)); return Results.Ok(response); } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemDto.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class CatalogItemDto { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string PictureUri { get; set; } public int CatalogTypeId { get; set; } public int CatalogBrandId { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class GetByIdCatalogItemRequest : BaseRequest { public int CatalogItemId { get; init; } public GetByIdCatalogItemRequest(int catalogItemId) { CatalogItemId = catalogItemId; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemResponse.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class GetByIdCatalogItemResponse : BaseResponse { public GetByIdCatalogItemResponse(Guid correlationId) : base(correlationId) { } public GetByIdCatalogItemResponse() { } public CatalogItemDto CatalogItem { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Get a Catalog Item by Id /// public class CatalogItemGetByIdEndpoint : IEndpoint> { private readonly IUriComposer _uriComposer; public CatalogItemGetByIdEndpoint(IUriComposer uriComposer) { _uriComposer = uriComposer; } public void AddRoute(IEndpointRouteBuilder app) { app.MapGet("api/catalog-items/{catalogItemId}", async (int catalogItemId, IRepository itemRepository) => { return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId), itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } public async Task HandleAsync(GetByIdCatalogItemRequest request, IRepository itemRepository) { var response = new GetByIdCatalogItemResponse(request.CorrelationId()); var item = await itemRepository.GetByIdAsync(request.CatalogItemId); if (item is null) return Results.NotFound(); response.CatalogItem = new CatalogItemDto { Id = item.Id, CatalogBrandId = item.CatalogBrandId, CatalogTypeId = item.CatalogTypeId, Description = item.Description, Name = item.Name, PictureUri = _uriComposer.ComposePicUri(item.PictureUri), Price = item.Price }; return Results.Ok(response); } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class ListPagedCatalogItemRequest : BaseRequest { public int PageSize { get; init; } public int PageIndex { get; init; } public int? CatalogBrandId { get; init; } public int? CatalogTypeId { get; init; } public ListPagedCatalogItemRequest(int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId) { PageSize = pageSize ?? 0; PageIndex = pageIndex ?? 0; CatalogBrandId = catalogBrandId; CatalogTypeId = catalogTypeId; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemResponse.cs ================================================ using System; using System.Collections.Generic; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class ListPagedCatalogItemResponse : BaseResponse { public ListPagedCatalogItemResponse(Guid correlationId) : base(correlationId) { } public ListPagedCatalogItemResponse() { } public List CatalogItems { get; set; } = new List(); public int PageCount { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// List Catalog Items (paged) /// public class CatalogItemListPagedEndpoint : IEndpoint> { private readonly IUriComposer _uriComposer; private readonly IMapper _mapper; public CatalogItemListPagedEndpoint(IUriComposer uriComposer, IMapper mapper) { _uriComposer = uriComposer; _mapper = mapper; } public void AddRoute(IEndpointRouteBuilder app) { app.MapGet("api/catalog-items", async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository itemRepository) => { return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId), itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } public async Task HandleAsync(ListPagedCatalogItemRequest request, IRepository itemRepository) { await Task.Delay(1000); var response = new ListPagedCatalogItemResponse(request.CorrelationId()); var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId); int totalItems = await itemRepository.CountAsync(filterSpec); var pagedSpec = new CatalogFilterPaginatedSpecification( skip: request.PageIndex * request.PageSize, take: request.PageSize, brandId: request.CatalogBrandId, typeId: request.CatalogTypeId); var items = await itemRepository.ListAsync(pagedSpec); response.CatalogItems.AddRange(items.Select(_mapper.Map)); foreach (CatalogItemDto item in response.CatalogItems) { item.PictureUri = _uriComposer.ComposePicUri(item.PictureUri); } if (request.PageSize > 0) { response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString()); } else { response.PageCount = totalItems > 0 ? 1 : 0; } return Results.Ok(response); } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemRequest.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class CreateCatalogItemRequest : BaseRequest { public int CatalogBrandId { get; set; } public int CatalogTypeId { get; set; } public string Description { get; set; } public string Name { get; set; } public string PictureUri { get; set; } public string PictureBase64 { get; set; } public string PictureName { get; set; } public decimal Price { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemResponse.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class CreateCatalogItemResponse : BaseResponse { public CreateCatalogItemResponse(Guid correlationId) : base(correlationId) { } public CreateCatalogItemResponse() { } public CatalogItemDto CatalogItem { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Creates a new Catalog Item /// public class CreateCatalogItemEndpoint : IEndpoint> { private readonly IUriComposer _uriComposer; public CreateCatalogItemEndpoint(IUriComposer uriComposer) { _uriComposer = uriComposer; } public void AddRoute(IEndpointRouteBuilder app) { app.MapPost("api/catalog-items", [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async (CreateCatalogItemRequest request, IRepository itemRepository) => { return await HandleAsync(request, itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } public async Task HandleAsync(CreateCatalogItemRequest request, IRepository itemRepository) { var response = new CreateCatalogItemResponse(request.CorrelationId()); var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name); var existingCataloogItem = await itemRepository.CountAsync(catalogItemNameSpecification); if (existingCataloogItem > 0) { throw new DuplicateException($"A catalogItem with name {request.Name} already exists"); } var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri); newItem = await itemRepository.AddAsync(newItem); if (newItem.Id != 0) { //We disabled the upload functionality and added a default/placeholder image to this sample due to a potential security risk // pointed out by the community. More info in this issue: https://github.com/dotnet-architecture/eShopOnWeb/issues/537 // In production, we recommend uploading to a blob storage and deliver the image via CDN after a verification process. newItem.UpdatePictureUri("eCatalog-item-default.png"); await itemRepository.UpdateAsync(newItem); } var dto = new CatalogItemDto { Id = newItem.Id, CatalogBrandId = newItem.CatalogBrandId, CatalogTypeId = newItem.CatalogTypeId, Description = newItem.Description, Name = newItem.Name, PictureUri = _uriComposer.ComposePicUri(newItem.PictureUri), Price = newItem.Price }; response.CatalogItem = dto; return Results.Created($"api/catalog-items/{dto.Id}", response); } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class DeleteCatalogItemRequest : BaseRequest { public int CatalogItemId { get; init; } public DeleteCatalogItemRequest(int catalogItemId) { CatalogItemId = catalogItemId; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemResponse.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class DeleteCatalogItemResponse : BaseResponse { public DeleteCatalogItemResponse(Guid correlationId) : base(correlationId) { } public DeleteCatalogItemResponse() { } public string Status { get; set; } = "Deleted"; } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Deletes a Catalog Item /// public class DeleteCatalogItemEndpoint : IEndpoint> { public void AddRoute(IEndpointRouteBuilder app) { app.MapDelete("api/catalog-items/{catalogItemId}", [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async (int catalogItemId, IRepository itemRepository) => { return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId), itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } public async Task HandleAsync(DeleteCatalogItemRequest request, IRepository itemRepository) { var response = new DeleteCatalogItemResponse(request.CorrelationId()); var itemToDelete = await itemRepository.GetByIdAsync(request.CatalogItemId); if (itemToDelete is null) return Results.NotFound(); await itemRepository.DeleteAsync(itemToDelete); return Results.Ok(response); } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemRequest.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class UpdateCatalogItemRequest : BaseRequest { [Range(1, 10000)] public int Id { get; set; } [Range(1, 10000)] public int CatalogBrandId { get; set; } [Range(1, 10000)] public int CatalogTypeId { get; set; } [Required] public string Description { get; set; } [Required] public string Name { get; set; } public string PictureBase64 { get; set; } public string PictureUri { get; set; } public string PictureName { get; set; } [Range(0.01, 10000)] public decimal Price { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemResponse.cs ================================================ using System; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; public class UpdateCatalogItemResponse : BaseResponse { public UpdateCatalogItemResponse(Guid correlationId) : base(correlationId) { } public UpdateCatalogItemResponse() { } public CatalogItemDto CatalogItem { get; set; } } ================================================ FILE: src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Updates a Catalog Item /// public class UpdateCatalogItemEndpoint : IEndpoint> { private readonly IUriComposer _uriComposer; public UpdateCatalogItemEndpoint(IUriComposer uriComposer) { _uriComposer = uriComposer; } public void AddRoute(IEndpointRouteBuilder app) { app.MapPut("api/catalog-items", [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async (UpdateCatalogItemRequest request, IRepository itemRepository) => { return await HandleAsync(request, itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } public async Task HandleAsync(UpdateCatalogItemRequest request, IRepository itemRepository) { var response = new UpdateCatalogItemResponse(request.CorrelationId()); var existingItem = await itemRepository.GetByIdAsync(request.Id); if (existingItem == null) { return Results.NotFound(); } CatalogItem.CatalogItemDetails details = new(request.Name, request.Description, request.Price); existingItem.UpdateDetails(details); existingItem.UpdateBrand(request.CatalogBrandId); existingItem.UpdateType(request.CatalogTypeId); await itemRepository.UpdateAsync(existingItem); var dto = new CatalogItemDto { Id = existingItem.Id, CatalogBrandId = existingItem.CatalogBrandId, CatalogTypeId = existingItem.CatalogTypeId, Description = existingItem.Description, Name = existingItem.Name, PictureUri = _uriComposer.ComposePicUri(existingItem.PictureUri), Price = existingItem.Price }; response.CatalogItem = dto; return Results.Ok(response); } } ================================================ FILE: src/PublicApi/CatalogTypeEndpoints/CatalogTypeDto.cs ================================================ namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; public class CatalogTypeDto { public int Id { get; set; } public string Name { get; set; } } ================================================ FILE: src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.ListCatalogTypesResponse.cs ================================================ using System; using System.Collections.Generic; namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; public class ListCatalogTypesResponse : BaseResponse { public ListCatalogTypesResponse(Guid correlationId) : base(correlationId) { } public ListCatalogTypesResponse() { } public List CatalogTypes { get; set; } = new List(); } ================================================ FILE: src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs ================================================ using System.Linq; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; /// /// List Catalog Types /// public class CatalogTypeListEndpoint : IEndpoint> { private readonly IMapper _mapper; public CatalogTypeListEndpoint(IMapper mapper) { _mapper = mapper; } public void AddRoute(IEndpointRouteBuilder app) { app.MapGet("api/catalog-types", async (IRepository catalogTypeRepository) => { return await HandleAsync(catalogTypeRepository); }) .Produces() .WithTags("CatalogTypeEndpoints"); } public async Task HandleAsync(IRepository catalogTypeRepository) { var response = new ListCatalogTypesResponse(); var items = await catalogTypeRepository.ListAsync(); response.CatalogTypes.AddRange(items.Select(_mapper.Map)); return Results.Ok(response); } } ================================================ FILE: src/PublicApi/CustomSchemaFilters.cs ================================================ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; namespace Microsoft.eShopWeb.PublicApi; public class CustomSchemaFilters : ISchemaFilter { public void Apply(OpenApiSchema schema, SchemaFilterContext context) { var excludeProperties = new[] { "CorrelationId" }; foreach (var prop in excludeProperties) if (schema.Properties.ContainsKey(prop)) schema.Properties.Remove(prop); } } ================================================ FILE: src/PublicApi/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app COPY . . #COPY ["src/PublicApi/PublicApi.csproj", "./PublicApi/"] #RUN dotnet restore "./PublicApi/PublicApi.csproj" #COPY . . WORKDIR "/app/src/PublicApi" RUN dotnet restore RUN dotnet build "./PublicApi.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "./PublicApi.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "PublicApi.dll"] ================================================ FILE: src/PublicApi/ImageValidators.cs ================================================ using System; using System.IO; namespace Microsoft.eShopWeb.PublicApi; public static class ImageValidators { private const int ImageMaximumBytes = 512000; public static bool IsValidImage(this byte[] postedFile, string fileName) { return postedFile != null && postedFile.Length > 0 && postedFile.Length <= ImageMaximumBytes && IsExtensionValid(fileName); } private static bool IsExtensionValid(string fileName) { var extension = Path.GetExtension(fileName); return string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".gif", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase); } } ================================================ FILE: src/PublicApi/MappingProfile.cs ================================================ using AutoMapper; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; using Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; namespace Microsoft.eShopWeb.PublicApi; public class MappingProfile : Profile { public MappingProfile() { CreateMap(); CreateMap() .ForMember(dto => dto.Name, options => options.MapFrom(src => src.Type)); CreateMap() .ForMember(dto => dto.Name, options => options.MapFrom(src => src.Brand)); } } ================================================ FILE: src/PublicApi/Middleware/ExceptionMiddleware.cs ================================================ using System; using System.Net; using System.Threading.Tasks; using BlazorShared.Models; using Microsoft.AspNetCore.Http; using Microsoft.eShopWeb.ApplicationCore.Exceptions; namespace Microsoft.eShopWeb.PublicApi.Middleware; public class ExceptionMiddleware { private readonly RequestDelegate _next; public ExceptionMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext httpContext) { try { await _next(httpContext); } catch (Exception ex) { await HandleExceptionAsync(httpContext, ex); } } private async Task HandleExceptionAsync(HttpContext context, Exception exception) { context.Response.ContentType = "application/json"; if (exception is DuplicateException duplicationException) { context.Response.StatusCode = (int)HttpStatusCode.Conflict; await context.Response.WriteAsync(new ErrorDetails() { StatusCode = context.Response.StatusCode, Message = duplicationException.Message }.ToString()); } else { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await context.Response.WriteAsync(new ErrorDetails() { StatusCode = context.Response.StatusCode, Message = exception.Message }.ToString()); } } } ================================================ FILE: src/PublicApi/Program.cs ================================================ using System; using System.Collections.Generic; using System.Text; using BlazorShared; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.PublicApi; using Microsoft.eShopWeb.PublicApi.Middleware; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using MinimalApi.Endpoint.Configurations.Extensions; using MinimalApi.Endpoint.Extensions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpoints(); // Use to force loading of appsettings.json of test project builder.Configuration.AddConfigurationFile("appsettings.test.json"); builder.Logging.AddConsole(); Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); builder.Services.Configure(builder.Configuration); var catalogSettings = builder.Configuration.Get() ?? new CatalogSettings(); builder.Services.AddSingleton(new UriComposer(catalogSettings)); builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); builder.Services.AddScoped(); var configSection = builder.Configuration.GetRequiredSection(BaseUrlConfiguration.CONFIG_NAME); builder.Services.Configure(configSection); var baseUrlConfig = configSection.Get(); builder.Services.AddMemoryCache(); var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); builder.Services.AddAuthentication(config => { config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(config => { config.RequireHttpsMetadata = false; config.SaveToken = true; config.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false }; }); const string CORS_POLICY = "CorsPolicy"; builder.Services.AddCors(options => { options.AddPolicy(name: CORS_POLICY, corsPolicyBuilder => { corsPolicyBuilder.WithOrigins(baseUrlConfig!.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); corsPolicyBuilder.AllowAnyMethod(); corsPolicyBuilder.AllowAnyHeader(); }); }); builder.Services.AddControllers(); builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly); builder.Configuration.AddEnvironmentVariables(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); c.EnableAnnotations(); c.SchemaFilter(); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below. \r\n\r\nExample: 'Bearer 12345abcdef'", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, Scheme = "oauth2", Name = "Bearer", In = ParameterLocation.Header, }, new List() } }); }); var app = builder.Build(); app.Logger.LogInformation("PublicApi App created..."); app.Logger.LogInformation("Seeding Database..."); using (var scope = app.Services.CreateScope()) { var scopedProvider = scope.ServiceProvider; try { var catalogContext = scopedProvider.GetRequiredService(); await CatalogContextSeed.SeedAsync(catalogContext, app.Logger); var userManager = scopedProvider.GetRequiredService>(); var roleManager = scopedProvider.GetRequiredService>(); var identityContext = scopedProvider.GetRequiredService(); await AppIdentityDbContextSeed.SeedAsync(identityContext, userManager, roleManager); } catch (Exception ex) { app.Logger.LogError(ex, "An error occurred seeding the DB."); } } if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMiddleware(); app.UseHttpsRedirection(); app.UseRouting(); app.UseCors(CORS_POLICY); app.UseAuthorization(); // Enable middleware to serve generated Swagger as a JSON endpoint. app.UseSwagger(); // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); app.MapControllers(); app.MapEndpoints(); app.Logger.LogInformation("LAUNCHING PublicApi"); app.Run(); public partial class Program { } ================================================ FILE: src/PublicApi/Properties/launchSettings.json ================================================ { "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "PublicApi": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5099;http://localhost:5098" }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:5099/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:5099;http://localhost:5098" }, "distributionName": "" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "publishAllPorts": true, "useSSL": true } }, "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:52023", "sslPort": 44339 } } } ================================================ FILE: src/PublicApi/PublicApi.csproj ================================================  Microsoft.eShopWeb.PublicApi 5b662463-1efd-4bae-bde4-befe0be3e8ff Linux ..\.. enable all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/PublicApi/README.md ================================================ # API Endpoints 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. ================================================ FILE: src/PublicApi/appsettings.Development.json ================================================ { "baseUrls": { "apiBase": "https://localhost:5099/api/", "webBase": "https://localhost:5001/" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: src/PublicApi/appsettings.Docker.json ================================================ { "ConnectionStrings": { "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;", "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;" }, "baseUrls": { "apiBase": "http://localhost:5200/api/", "webBase": "http://host.docker.internal:5106/" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: src/PublicApi/appsettings.json ================================================ { "baseUrls": { "apiBase": "https://localhost:5099/api/", "webBase": "https://localhost:5001/" }, "ConnectionStrings": { "CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;", "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;" }, "CatalogBaseUrl": "", "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning", "Microsoft": "Warning", "System": "Warning" }, "AllowedHosts": "*" } } ================================================ FILE: src/Web/.config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "dotnet-ef": { "version": "8.0.0", "commands": [ "dotnet-ef" ] } } } ================================================ FILE: src/Web/Areas/Identity/IdentityHostingStartup.cs ================================================ using Microsoft.AspNetCore.Hosting; [assembly: HostingStartup(typeof(Microsoft.eShopWeb.Web.Areas.Identity.IdentityHostingStartup))] namespace Microsoft.eShopWeb.Web.Areas.Identity; public class IdentityHostingStartup : IHostingStartup { public void Configure(IWebHostBuilder builder) { builder.ConfigureServices((context, services) => { }); } } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml ================================================ @page @model ConfirmEmailModel @{ ViewData["Title"] = "Confirm email"; }

@ViewData["Title"]

Thank you for confirming your email.

================================================ FILE: src/Web/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.Infrastructure.Identity; namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account; [AllowAnonymous] public class ConfirmEmailModel : PageModel { private readonly UserManager _userManager; public ConfirmEmailModel(UserManager userManager) { _userManager = userManager; } public async Task OnGetAsync(string userId, string code) { if (userId == null || code == null) { return RedirectToPage("/Index"); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { return NotFound($"Unable to load user with ID '{userId}'."); } var result = await _userManager.ConfirmEmailAsync(user, code); if (!result.Succeeded) { throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':"); } return Page(); } } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/Login.cshtml ================================================ @page @model LoginModel @{ ViewData["Title"] = "Log in"; } @section Scripts { } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs ================================================ using System.ComponentModel.DataAnnotations; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account; [AllowAnonymous] public class LoginModel : PageModel { private readonly SignInManager _signInManager; private readonly ILogger _logger; private readonly IBasketService _basketService; public LoginModel(SignInManager signInManager, ILogger logger, IBasketService basketService) { _signInManager = signInManager; _logger = logger; _basketService = basketService; } [BindProperty] public required InputModel Input { get; set; } public IList? ExternalLogins { get; set; } public string? ReturnUrl { get; set; } [TempData] public string? ErrorMessage { get; set; } public class InputModel { [Required] [EmailAddress] public string? Email { get; set; } [Required] [DataType(DataType.Password)] public string? Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public async Task OnGetAsync(string? returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { ModelState.AddModelError(string.Empty, ErrorMessage); } returnUrl = returnUrl ?? Url.Content("~/"); // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ReturnUrl = returnUrl; } public async Task OnPostAsync(string? returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true //var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); var result = await _signInManager.PasswordSignInAsync(Input!.Email!, Input!.Password!, false, true); if (result.Succeeded) { _logger.LogInformation("User logged in."); await TransferAnonymousBasketToUserAsync(Input?.Email); return LocalRedirect(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input?.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning("User account locked out."); return RedirectToPage("./Lockout"); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); } } // If we got this far, something failed, redisplay form return Page(); } private async Task TransferAnonymousBasketToUserAsync(string? userName) { if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) { var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME]; if (Guid.TryParse(anonymousId, out var _)) { Guard.Against.NullOrEmpty(userName, nameof(userName)); await _basketService.TransferBasketAsync(anonymousId, userName); } Response.Cookies.Delete(Constants.BASKET_COOKIENAME); } } } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/Logout.cshtml ================================================ @page @model LogoutModel @{ ViewData["Title"] = "Log out"; }

@ViewData["Title"]

You have successfully logged out of the application.

================================================ FILE: src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs ================================================ using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Configuration; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account; //TODO : replace IMemoryCache by distributed cache if you are in multi-host scenario public class LogoutModel : PageModel { private readonly SignInManager _signInManager; private readonly ILogger _logger; private readonly IMemoryCache _cache; public LogoutModel(SignInManager signInManager, ILogger logger, IMemoryCache cache) { _signInManager = signInManager; _logger = logger; _cache = cache; } public void OnGet() { } public async Task OnPost(string? returnUrl = null) { await _signInManager.SignOutAsync(); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); var userId = _signInManager.Context.User.Claims.First(c => c.Type == ClaimTypes.Name); var identityKey = _signInManager.Context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName]; _cache.Set($"{userId.Value}:{identityKey}", identityKey, new MemoryCacheEntryOptions { AbsoluteExpiration = DateTime.Now.AddMinutes(ConfigureCookieSettings.ValidityMinutesPeriod) }); _logger.LogInformation("User logged out."); if (returnUrl != null) { return LocalRedirect(returnUrl); } else { return RedirectToPage("/Index"); } } } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/Register.cshtml ================================================ @page @model RegisterModel @{ ViewData["Title"] = "Register"; }

@ViewData["Title"]

Create a new account.


@section Scripts { } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.Encodings.Web; using System.Threading.Tasks; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account; [AllowAnonymous] public class RegisterModel : PageModel { private readonly SignInManager _signInManager; private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IEmailSender _emailSender; public RegisterModel( UserManager userManager, SignInManager signInManager, ILogger logger, IEmailSender emailSender) { _userManager = userManager; _signInManager = signInManager; _logger = logger; _emailSender = emailSender; } [BindProperty] public required InputModel Input { get; set; } public string? ReturnUrl { get; set; } public class InputModel { [Required] [EmailAddress] [Display(Name = "Email")] public string? Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string? Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string? ConfirmPassword { get; set; } } public void OnGet(string? returnUrl = null) { ReturnUrl = returnUrl; } public async Task OnPostAsync(string? returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); if (ModelState.IsValid) { var user = new ApplicationUser { UserName = Input?.Email, Email = Input?.Email }; var result = await _userManager.CreateAsync(user, Input?.Password!); if (result.Succeeded) { _logger.LogInformation("User created a new account with password."); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new { userId = user.Id, code = code }, protocol: Request.Scheme); Guard.Against.Null(callbackUrl, nameof(callbackUrl)); await _emailSender.SendEmailAsync(Input!.Email!, "Confirm your email", $"Please confirm your account by clicking here."); await _signInManager.SignInAsync(user, isPersistent: false); return LocalRedirect(returnUrl); } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } // If we got this far, something failed, redisplay form return Page(); } } ================================================ FILE: src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml ================================================ @using Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account ================================================ FILE: src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml ================================================  ================================================ FILE: src/Web/Areas/Identity/Pages/_ViewImports.cshtml ================================================ @using Microsoft.AspNetCore.Identity @using Microsoft.eShopWeb.Web.Areas.Identity @using Microsoft.eShopWeb.Infrastructure.Identity @namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers ================================================ FILE: src/Web/Areas/Identity/Pages/_ViewStart.cshtml ================================================ @{ Layout = "/Views/Shared/_Layout.cshtml"; } ================================================ FILE: src/Web/Configuration/ConfigureCookieSettings.cs ================================================ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.eShopWeb.Web.Configuration; public static class ConfigureCookieSettings { public const int ValidityMinutesPeriod = 60; public const string IdentifierCookieName = "EshopIdentifier"; public static IServiceCollection AddCookieSettings(this IServiceCollection services) { services.Configure(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. //TODO need to check that. //options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.Strict; }); services.ConfigureApplicationCookie(options => { options.EventsType = typeof(RevokeAuthenticationEvents); options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromMinutes(ValidityMinutesPeriod); options.LoginPath = "/Account/Login"; options.LogoutPath = "/Account/Logout"; options.Cookie = new CookieBuilder { Name = IdentifierCookieName, IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy }; }); services.AddScoped(); return services; } } ================================================ FILE: src/Web/Configuration/ConfigureCoreServices.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Data.Queries; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.Infrastructure.Services; namespace Microsoft.eShopWeb.Web.Configuration; public static class ConfigureCoreServices { public static IServiceCollection AddCoreServices(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); services.AddScoped(); services.AddScoped(); services.AddScoped(); var catalogSettings = configuration.Get() ?? new CatalogSettings(); services.AddSingleton(new UriComposer(catalogSettings)); services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); services.AddTransient(); return services; } } ================================================ FILE: src/Web/Configuration/ConfigureWebServices.cs ================================================ using MediatR; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Services; namespace Microsoft.eShopWeb.Web.Configuration; public static class ConfigureWebServices { public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration) { services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(BasketViewModelService).Assembly)); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.Configure(configuration); services.AddScoped(); return services; } } ================================================ FILE: src/Web/Configuration/RevokeAuthenticationEvents.cs ================================================ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.Web.Configuration; //TODO : replace IMemoryCache with a distributed cache if you are in multi-host scenario public class RevokeAuthenticationEvents : CookieAuthenticationEvents { private readonly IMemoryCache _cache; private readonly ILogger _logger; public RevokeAuthenticationEvents(IMemoryCache cache, ILogger logger) { _cache = cache; _logger = logger; } public override async Task ValidatePrincipal(CookieValidatePrincipalContext context) { var userId = context.Principal?.Claims.First(c => c.Type == ClaimTypes.Name); var identityKey = context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName]; if (_cache.TryGetValue($"{userId?.Value}:{identityKey}", out var revokeKeys)) { _logger.LogDebug($"Access has been revoked for: {userId?.Value}."); context.RejectPrincipal(); await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } } } ================================================ FILE: src/Web/Constants.cs ================================================ namespace Microsoft.eShopWeb.Web; public static class Constants { public const string BASKET_COOKIENAME = "eShop"; public const int ITEMS_PER_PAGE = 10; public const string DEFAULT_USERNAME = "Guest"; public const string BASKET_ID = "BasketId"; } ================================================ FILE: src/Web/Controllers/Api/BaseApiController.cs ================================================ using Microsoft.AspNetCore.Mvc; namespace Microsoft.eShopWeb.Web.Controllers.Api; // No longer used - shown for reference only if using full controllers instead of Endpoints for APIs [Route("api/[controller]/[action]")] [ApiController] public class BaseApiController : ControllerBase { } ================================================ FILE: src/Web/Controllers/ManageController.cs ================================================ using System.Text; using System.Text.Encodings.Web; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Services; using Microsoft.eShopWeb.Web.ViewModels.Manage; namespace Microsoft.eShopWeb.Web.Controllers; [ApiExplorerSettings(IgnoreApi = true)] [Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages [Route("[controller]/[action]")] public class ManageController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IEmailSender _emailSender; private readonly IAppLogger _logger; private readonly UrlEncoder _urlEncoder; private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; private const string RecoveryCodesKey = nameof(RecoveryCodesKey); public ManageController( UserManager userManager, SignInManager signInManager, IEmailSender emailSender, IAppLogger logger, UrlEncoder urlEncoder) { _userManager = userManager; _signInManager = signInManager; _emailSender = emailSender; _logger = logger; _urlEncoder = urlEncoder; } [TempData] public string? StatusMessage { get; set; } [HttpGet] public async Task MyAccount() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new IndexViewModel { Username = user.UserName, Email = user.Email, PhoneNumber = user.PhoneNumber, IsEmailConfirmed = user.EmailConfirmed, StatusMessage = StatusMessage }; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task MyAccount(IndexViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var email = user.Email; if (model.Email != email) { var setEmailResult = await _userManager.SetEmailAsync(user, model.Email); if (!setEmailResult.Succeeded) { throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); } } var phoneNumber = user.PhoneNumber; if (model.PhoneNumber != phoneNumber) { var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber); if (!setPhoneResult.Succeeded) { throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'."); } } StatusMessage = "Your profile has been updated"; return RedirectToAction(nameof(MyAccount)); } [HttpPost] [ValidateAntiForgeryToken] public async Task SendVerificationEmail(IndexViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); Guard.Against.Null(callbackUrl, nameof(callbackUrl)); var email = user.Email; if (email == null) { throw new ApplicationException($"No email associated with user {user.UserName}'."); } await _emailSender.SendEmailConfirmationAsync(email, callbackUrl); StatusMessage = "Verification email sent. Please check your email."; return RedirectToAction(nameof(MyAccount)); } [HttpGet] public async Task ChangePassword() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var hasPassword = await _userManager.HasPasswordAsync(user); if (!hasPassword) { return RedirectToAction(nameof(SetPassword)); } var model = new ChangePasswordViewModel { StatusMessage = StatusMessage }; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task ChangePassword(ChangePasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var changePasswordResult = await _userManager .ChangePasswordAsync(user, model.OldPassword!, model.NewPassword!); if (!changePasswordResult.Succeeded) { AddErrors(changePasswordResult); return View(model); } await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User changed their password successfully."); StatusMessage = "Your password has been changed."; return RedirectToAction(nameof(ChangePassword)); } [HttpGet] public async Task SetPassword() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var hasPassword = await _userManager.HasPasswordAsync(user); if (hasPassword) { return RedirectToAction(nameof(ChangePassword)); } var model = new SetPasswordViewModel { StatusMessage = StatusMessage }; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task SetPassword(SetPasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword!); if (!addPasswordResult.Succeeded) { AddErrors(addPasswordResult); return View(model); } await _signInManager.SignInAsync(user, isPersistent: false); StatusMessage = "Your password has been set."; return RedirectToAction(nameof(SetPassword)); } [HttpGet] public async Task ExternalLogins() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) }; model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) .Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) .ToList(); model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; model.StatusMessage = StatusMessage; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task LinkLogin(string provider) { // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); // Request a redirect to the external login provider to link a login for the current user var redirectUrl = Url.Action(nameof(LinkLoginCallback)); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); return new ChallengeResult(provider, properties); } [HttpGet] public async Task LinkLoginCallback() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); if (info == null) { throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); } var result = await _userManager.AddLoginAsync(user, info); if (!result.Succeeded) { throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); } // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); StatusMessage = "The external login was added."; return RedirectToAction(nameof(ExternalLogins)); } [HttpPost] [ValidateAntiForgeryToken] public async Task RemoveLogin(RemoveLoginViewModel model) { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!ModelState.IsValid) { return View(model); } var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); if (!result.Succeeded) { throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); } await _signInManager.SignInAsync(user, isPersistent: false); StatusMessage = "The external login was removed."; return RedirectToAction(nameof(ExternalLogins)); } [HttpGet] public async Task TwoFactorAuthentication() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new TwoFactorAuthenticationViewModel { HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, Is2faEnabled = user.TwoFactorEnabled, RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), }; return View(model); } [HttpGet] public async Task Disable2faWarning() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!user.TwoFactorEnabled) { throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); } return View(nameof(Disable2fa)); } [HttpPost] [ValidateAntiForgeryToken] public async Task Disable2fa() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); if (!disable2faResult.Succeeded) { throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); } _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); return RedirectToAction(nameof(TwoFactorAuthentication)); } [HttpGet] public async Task EnableAuthenticator() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new EnableAuthenticatorViewModel(); await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } [HttpGet] public IActionResult ShowRecoveryCodes() { var recoveryCodes = (string[]?)TempData[RecoveryCodesKey]; if (recoveryCodes == null) { return RedirectToAction(nameof(TwoFactorAuthentication)); } var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes }; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!ModelState.IsValid) { await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } // Strip spaces and hypens string verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty) ?? ""; var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); if (!is2faTokenValid) { ModelState.AddModelError("Code", "Verification code is invalid."); await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List(); TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); return RedirectToAction(nameof(ShowRecoveryCodes)); } [HttpGet] public IActionResult ResetAuthenticatorWarning() { return View(nameof(ResetAuthenticator)); } [HttpPost] [ValidateAntiForgeryToken] public async Task ResetAuthenticator() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } await _userManager.SetTwoFactorEnabledAsync(user, false); await _userManager.ResetAuthenticatorKeyAsync(user); _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); return RedirectToAction(nameof(EnableAuthenticator)); } [HttpPost] [ValidateAntiForgeryToken] public async Task GenerateRecoveryCodes() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!user.TwoFactorEnabled) { throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); } var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List(); _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; return View(nameof(ShowRecoveryCodes), model); } [HttpGet] public async Task GenerateRecoveryCodesWarning() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!user.TwoFactorEnabled) { throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' because they do not have 2FA enabled."); } return View(nameof(GenerateRecoveryCodesWarning)); } private void AddErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } private string FormatKey(string unformattedKey) { var result = new StringBuilder(); int currentPosition = 0; while (currentPosition + 4 < unformattedKey.Length) { result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); currentPosition += 4; } if (currentPosition < unformattedKey.Length) { result.Append(unformattedKey.Substring(currentPosition)); } return result.ToString().ToLowerInvariant(); } private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( AuthenticatorUriFormat, _urlEncoder.Encode("eShopOnWeb"), _urlEncoder.Encode(email), unformattedKey); } private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAuthenticatorViewModel model) { var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); if (string.IsNullOrEmpty(unformattedKey)) { await _userManager.ResetAuthenticatorKeyAsync(user); unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } model.SharedKey = FormatKey(unformattedKey!); model.AuthenticatorUri = GenerateQrCodeUri(user.Email!, unformattedKey!); } } ================================================ FILE: src/Web/Controllers/OrderController.cs ================================================ using Ardalis.GuardClauses; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.Web.Features.MyOrders; using Microsoft.eShopWeb.Web.Features.OrderDetails; namespace Microsoft.eShopWeb.Web.Controllers; [ApiExplorerSettings(IgnoreApi = true)] [Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages [Route("[controller]/[action]")] public class OrderController : Controller { private readonly IMediator _mediator; public OrderController(IMediator mediator) { _mediator = mediator; } [HttpGet] public async Task MyOrders() { Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name)); var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name)); return View(viewModel); } [HttpGet("{orderId}")] public async Task Detail(int orderId) { Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name)); var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId)); if (viewModel == null) { return BadRequest("No such order found for this user."); } return View(viewModel); } } ================================================ FILE: src/Web/Controllers/UserController.cs ================================================ using System.Security.Claims; using BlazorShared.Authorization; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Configuration; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.eShopWeb.Web.Controllers; [Route("[controller]")] [ApiController] public class UserController : ControllerBase { private readonly ITokenClaimsService _tokenClaimsService; private readonly SignInManager _signInManager; private readonly ILogger _logger; private readonly IMemoryCache _cache; public UserController(ITokenClaimsService tokenClaimsService, SignInManager signInManager, ILogger logger, IMemoryCache cache) { _tokenClaimsService = tokenClaimsService; _signInManager = signInManager; _logger = logger; _cache = cache; } [HttpGet] [Authorize] [AllowAnonymous] public async Task GetCurrentUser() => Ok(await CreateUserInfo(User)); [Route("Logout")] [HttpPost] [Authorize] [AllowAnonymous] public async Task Logout() { await _signInManager.SignOutAsync(); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); var userId = _signInManager.Context.User.Claims.First(c => c.Type == ClaimTypes.Name); var identityKey = _signInManager.Context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName]; _cache.Set($"{userId.Value}:{identityKey}", identityKey, new MemoryCacheEntryOptions { AbsoluteExpiration = DateTime.Now.AddMinutes(ConfigureCookieSettings.ValidityMinutesPeriod) }); _logger.LogInformation("User logged out."); return Ok(); } private async Task CreateUserInfo(ClaimsPrincipal claimsPrincipal) { if (claimsPrincipal.Identity == null || claimsPrincipal.Identity.Name == null || !claimsPrincipal.Identity.IsAuthenticated) { return UserInfo.Anonymous; } var userInfo = new UserInfo { IsAuthenticated = true }; if (claimsPrincipal.Identity is ClaimsIdentity claimsIdentity) { userInfo.NameClaimType = claimsIdentity.NameClaimType; userInfo.RoleClaimType = claimsIdentity.RoleClaimType; } else { userInfo.NameClaimType = "name"; userInfo.RoleClaimType = "role"; } if (claimsPrincipal.Claims.Any()) { var claims = new List(); var nameClaims = claimsPrincipal.FindAll(userInfo.NameClaimType); foreach (var claim in nameClaims) { claims.Add(new ClaimValue(userInfo.NameClaimType, claim.Value)); } foreach (var claim in claimsPrincipal.Claims.Except(nameClaims)) { claims.Add(new ClaimValue(claim.Type, claim.Value)); } userInfo.Claims = claims; } var token = await _tokenClaimsService.GetTokenAsync(claimsPrincipal.Identity.Name); userInfo.Token = token; return userInfo; } } ================================================ FILE: src/Web/Dockerfile ================================================ # RUN ALL CONTAINERS FROM ROOT (folder with .sln file): # docker-compose build # docker-compose up # # RUN JUST THIS CONTAINER FROM ROOT (folder with .sln file): # docker build --pull -t web -f src/Web/Dockerfile . # # RUN COMMAND # docker run --name eshopweb --rm -it -p 5106:5106 web FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app COPY *.sln . COPY . . WORKDIR /app/src/Web RUN dotnet restore RUN dotnet publish -c Release -o out FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app COPY --from=build /app/src/Web/out ./ # Optional: Set this here if not setting it from docker-compose.yml # ENV ASPNETCORE_ENVIRONMENT Development ENTRYPOINT ["dotnet", "Web.dll"] ================================================ FILE: src/Web/Extensions/CacheHelpers.cs ================================================ using System; namespace Microsoft.eShopWeb.Web.Extensions; public static class CacheHelpers { public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromSeconds(30); private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}"; public static string GenerateCatalogItemCacheKey(int pageIndex, int itemsPage, int? brandId, int? typeId) { return string.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandId, typeId); } public static string GenerateBrandsCacheKey() { return "brands"; } public static string GenerateTypesCacheKey() { return "types"; } } ================================================ FILE: src/Web/Extensions/EmailSenderExtensions.cs ================================================ using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.Web.Services; public static class EmailSenderExtensions { public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) { return emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking this link: link"); } } ================================================ FILE: src/Web/Extensions/UrlHelperExtensions.cs ================================================ namespace Microsoft.AspNetCore.Mvc; public static class UrlHelperExtensions { public static string? EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) { return urlHelper.Action( action: "GET", controller: "ConfirmEmail", values: new { userId, code }, protocol: scheme); } } ================================================ FILE: src/Web/Features/MyOrders/GetMyOrders.cs ================================================ using MediatR; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Features.MyOrders; public class GetMyOrders : IRequest> { public string UserName { get; set; } public GetMyOrders(string userName) { UserName = userName; } } ================================================ FILE: src/Web/Features/MyOrders/GetMyOrdersHandler.cs ================================================ using MediatR; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Features.MyOrders; public class GetMyOrdersHandler : IRequestHandler> { private readonly IReadRepository _orderRepository; public GetMyOrdersHandler(IReadRepository orderRepository) { _orderRepository = orderRepository; } public async Task> Handle(GetMyOrders request, CancellationToken cancellationToken) { var specification = new CustomerOrdersSpecification(request.UserName); var orders = await _orderRepository.ListAsync(specification, cancellationToken); return orders.Select(o => new OrderViewModel { OrderDate = o.OrderDate, OrderNumber = o.Id, ShippingAddress = o.ShipToAddress, Total = o.Total() }); } } ================================================ FILE: src/Web/Features/OrderDetails/GetOrderDetails.cs ================================================ using MediatR; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Features.OrderDetails; public class GetOrderDetails : IRequest { public string UserName { get; set; } public int OrderId { get; set; } public GetOrderDetails(string userName, int orderId) { UserName = userName; OrderId = orderId; } } ================================================ FILE: src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs ================================================ using MediatR; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Features.OrderDetails; public class GetOrderDetailsHandler : IRequestHandler { private readonly IReadRepository _orderRepository; public GetOrderDetailsHandler(IReadRepository orderRepository) { _orderRepository = orderRepository; } public async Task Handle(GetOrderDetails request, CancellationToken cancellationToken) { var spec = new OrderWithItemsByIdSpec(request.OrderId); var order = await _orderRepository.FirstOrDefaultAsync(spec, cancellationToken); if (order == null) { return null; } return new OrderDetailViewModel { OrderDate = order.OrderDate, OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel { PictureUrl = oi.ItemOrdered.PictureUri, ProductId = oi.ItemOrdered.CatalogItemId, ProductName = oi.ItemOrdered.ProductName, UnitPrice = oi.UnitPrice, Units = oi.Units }).ToList(), OrderNumber = order.Id, ShippingAddress = order.ShipToAddress, Total = order.Total() }; } } ================================================ FILE: src/Web/HealthChecks/ApiHealthCheck.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BlazorShared; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; namespace Microsoft.eShopWeb.Web.HealthChecks; public class ApiHealthCheck : IHealthCheck { private readonly BaseUrlConfiguration _baseUrlConfiguration; public ApiHealthCheck(IOptions baseUrlConfiguration) { _baseUrlConfiguration = baseUrlConfiguration.Value; } public async Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { string myUrl = _baseUrlConfiguration.ApiBase + "catalog-items"; var client = new HttpClient(); var response = await client.GetAsync(myUrl); var pageContents = await response.Content.ReadAsStringAsync(); if (pageContents.Contains(".NET Bot Black Sweatshirt")) { return HealthCheckResult.Healthy("The check indicates a healthy result."); } return HealthCheckResult.Unhealthy("The check indicates an unhealthy result."); } } ================================================ FILE: src/Web/HealthChecks/HomePageHealthCheck.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.eShopWeb.Web.HealthChecks; public class HomePageHealthCheck : IHealthCheck { private readonly IHttpContextAccessor _httpContextAccessor; public HomePageHealthCheck(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public async Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { var request = _httpContextAccessor.HttpContext?.Request; string myUrl = request?.Scheme + "://" + request?.Host.ToString(); var client = new HttpClient(); var response = await client.GetAsync(myUrl); var pageContents = await response.Content.ReadAsStringAsync(); if (pageContents.Contains(".NET Bot Black Sweatshirt")) { return HealthCheckResult.Healthy("The check indicates a healthy result."); } return HealthCheckResult.Unhealthy("The check indicates an unhealthy result."); } } ================================================ FILE: src/Web/Interfaces/IBasketViewModelService.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.Web.Pages.Basket; namespace Microsoft.eShopWeb.Web.Interfaces; public interface IBasketViewModelService { Task GetOrCreateBasketForUser(string userName); Task CountTotalBasketItems(string username); Task Map(Basket basket); } ================================================ FILE: src/Web/Interfaces/ICatalogItemViewModelService.cs ================================================ using System.Threading.Tasks; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Interfaces; public interface ICatalogItemViewModelService { Task UpdateCatalogItem(CatalogItemViewModel viewModel); } ================================================ FILE: src/Web/Interfaces/ICatalogViewModelService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Services; public interface ICatalogViewModelService { Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId); Task> GetBrands(); Task> GetTypes(); } ================================================ FILE: src/Web/Pages/Admin/EditCatalogItem.cshtml ================================================ @page @{ ViewData["Title"] = "Admin - Edit Catalog"; @model EditCatalogItemModel }
@section Scripts { } ================================================ FILE: src/Web/Pages/Admin/EditCatalogItem.cshtml.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Pages.Admin; [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)] public class EditCatalogItemModel : PageModel { private readonly ICatalogItemViewModelService _catalogItemViewModelService; public EditCatalogItemModel(ICatalogItemViewModelService catalogItemViewModelService) { _catalogItemViewModelService = catalogItemViewModelService; } [BindProperty] public CatalogItemViewModel CatalogModel { get; set; } = new CatalogItemViewModel(); public void OnGet(CatalogItemViewModel catalogModel) { CatalogModel = catalogModel; } public async Task OnPostAsync() { if (ModelState.IsValid) { await _catalogItemViewModelService.UpdateCatalogItem(CatalogModel); } return RedirectToPage("/Admin/Index"); } } ================================================ FILE: src/Web/Pages/Admin/Index.cshtml ================================================ @page @using BlazorAdmin @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ Layout = null; ViewData["Title"] = "Admin - Catalog"; } @ViewData["Title"] - Microsoft.eShopOnWeb
================================================ FILE: src/Web/Pages/Admin/Index.cshtml.cs ================================================ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.Web.Extensions; using Microsoft.eShopWeb.Web.Services; using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.eShopWeb.Web.Pages.Admin; [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)] public class IndexModel : PageModel { public IndexModel() { } } ================================================ FILE: src/Web/Pages/Basket/BasketItemViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.Pages.Basket; public class BasketItemViewModel { public int Id { get; set; } public int CatalogItemId { get; set; } public string? ProductName { get; set; } public decimal UnitPrice { get; set; } public decimal OldUnitPrice { get; set; } [Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")] public int Quantity { get; set; } public string? PictureUrl { get; set; } } ================================================ FILE: src/Web/Pages/Basket/BasketViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.Pages.Basket; public class BasketViewModel { public int Id { get; set; } public List Items { get; set; } = new List(); public string? BuyerId { get; set; } public decimal Total() { return Math.Round(Items.Sum(x => x.UnitPrice * x.Quantity), 2); } } ================================================ FILE: src/Web/Pages/Basket/Checkout.cshtml ================================================ @page @model CheckoutModel @{ ViewData["Title"] = "Checkout"; }

Review

@if (Model.BasketModel.Items.Any()) {

Product
Price
Quantity
Cost
@for (int i = 0; i < Model.BasketModel.Items.Count; i++) { var item = Model.BasketModel.Items[i];
@item.ProductName
$ @item.UnitPrice.ToString("N2")
@item.Quantity
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
}
Total
$ @Model.BasketModel.Total().ToString("N2")
} else {

Basket is empty.

[ Continue Shopping ]
}
================================================ FILE: src/Web/Pages/Basket/Checkout.cshtml.cs ================================================ using Ardalis.GuardClauses; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Interfaces; namespace Microsoft.eShopWeb.Web.Pages.Basket; [Authorize] public class CheckoutModel : PageModel { private readonly IBasketService _basketService; private readonly SignInManager _signInManager; private readonly IOrderService _orderService; private string? _username = null; private readonly IBasketViewModelService _basketViewModelService; private readonly IAppLogger _logger; public CheckoutModel(IBasketService basketService, IBasketViewModelService basketViewModelService, SignInManager signInManager, IOrderService orderService, IAppLogger logger) { _basketService = basketService; _signInManager = signInManager; _orderService = orderService; _basketViewModelService = basketViewModelService; _logger = logger; } public BasketViewModel BasketModel { get; set; } = new BasketViewModel(); public async Task OnGet() { await SetBasketModelAsync(); } public async Task OnPost(IEnumerable items) { try { await SetBasketModelAsync(); if (!ModelState.IsValid) { return BadRequest(); } var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity); await _basketService.SetQuantities(BasketModel.Id, updateModel); await _orderService.CreateOrderAsync(BasketModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); await _basketService.DeleteBasketAsync(BasketModel.Id); } catch (EmptyBasketOnCheckoutException emptyBasketOnCheckoutException) { //Redirect to Empty Basket page _logger.LogWarning(emptyBasketOnCheckoutException.Message); return RedirectToPage("/Basket/Index"); } return RedirectToPage("Success"); } private async Task SetBasketModelAsync() { Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name)); if (_signInManager.IsSignedIn(HttpContext.User)) { BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name); } else { GetOrSetBasketCookieAndUserName(); BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username!); } } private void GetOrSetBasketCookieAndUserName() { if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) { _username = Request.Cookies[Constants.BASKET_COOKIENAME]; } if (_username != null) return; _username = Guid.NewGuid().ToString(); var cookieOptions = new CookieOptions(); cookieOptions.Expires = DateTime.Today.AddYears(10); Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions); } } ================================================ FILE: src/Web/Pages/Basket/Index.cshtml ================================================ @page "{handler?}" @model IndexModel @{ ViewData["Title"] = "Basket"; }
@if (Model.BasketModel.Items.Any()) {

Product
Price
Quantity
Cost
@for (int i = 0; i < Model.BasketModel.Items.Count; i++) { var item = Model.BasketModel.Items[i];
@item.ProductName
$ @item.UnitPrice.ToString("N2")
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
}
Total
$ @Model.BasketModel.Total().ToString("N2")
} else {

Basket is empty.

[ Continue Shopping ]
}
================================================ FILE: src/Web/Pages/Basket/Index.cshtml.cs ================================================ using Ardalis.GuardClauses; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Pages.Basket; public class IndexModel : PageModel { private readonly IBasketService _basketService; private readonly IBasketViewModelService _basketViewModelService; private readonly IRepository _itemRepository; public IndexModel(IBasketService basketService, IBasketViewModelService basketViewModelService, IRepository itemRepository) { _basketService = basketService; _basketViewModelService = basketViewModelService; _itemRepository = itemRepository; } public BasketViewModel BasketModel { get; set; } = new BasketViewModel(); public async Task OnGet() { BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName()); } public async Task OnPost(CatalogItemViewModel productDetails) { if (productDetails?.Id == null) { return RedirectToPage("/Index"); } var item = await _itemRepository.GetByIdAsync(productDetails.Id); if (item == null) { return RedirectToPage("/Index"); } var username = GetOrSetBasketCookieAndUserName(); var basket = await _basketService.AddItemToBasket(username, productDetails.Id, item.Price); BasketModel = await _basketViewModelService.Map(basket); return RedirectToPage(); } public async Task OnPostUpdate(IEnumerable items) { if (!ModelState.IsValid) { return; } var basketView = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName()); var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity); var basket = await _basketService.SetQuantities(basketView.Id, updateModel); BasketModel = await _basketViewModelService.Map(basket); } private string GetOrSetBasketCookieAndUserName() { Guard.Against.Null(Request.HttpContext.User.Identity, nameof(Request.HttpContext.User.Identity)); string? userName = null; if (Request.HttpContext.User.Identity.IsAuthenticated) { Guard.Against.Null(Request.HttpContext.User.Identity.Name, nameof(Request.HttpContext.User.Identity.Name)); return Request.HttpContext.User.Identity.Name!; } if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) { userName = Request.Cookies[Constants.BASKET_COOKIENAME]; if (!Request.HttpContext.User.Identity.IsAuthenticated) { if (!Guid.TryParse(userName, out var _)) { userName = null; } } } if (userName != null) return userName; userName = Guid.NewGuid().ToString(); var cookieOptions = new CookieOptions { IsEssential = true }; cookieOptions.Expires = DateTime.Today.AddYears(10); Response.Cookies.Append(Constants.BASKET_COOKIENAME, userName, cookieOptions); return userName; } } ================================================ FILE: src/Web/Pages/Basket/Success.cshtml ================================================ @page @model SuccessModel @{ ViewData["Title"] = "Checkout Complete"; }

Thanks for your Order!

Continue Shopping...
================================================ FILE: src/Web/Pages/Basket/Success.cshtml.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.eShopWeb.Web.Pages.Basket; [Authorize] public class SuccessModel : PageModel { public void OnGet() { } } ================================================ FILE: src/Web/Pages/Error.cshtml ================================================ @page @model ErrorModel @{ ViewData["Title"] = "Error"; }

Error.

An error occurred while processing your request.

@if (Model.ShowRequestId) {

Request ID: @Model.RequestId

}

Development Mode

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

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

================================================ FILE: src/Web/Pages/Error.cshtml.cs ================================================ using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.eShopWeb.Web.Pages; [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class ErrorModel : PageModel { public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } ================================================ FILE: src/Web/Pages/FeatureDiagnostics.cshtml ================================================ @page "/feature-diagnostics" @using Microsoft.FeatureManagement @inject IFeatureManager FeatureManager @inject IConfiguration Configuration @{ ViewData["Title"] = "Feature Diagnostics"; var salesWeekendEnabled = await FeatureManager.IsEnabledAsync("SalesWeekend"); var useAppConfig = Configuration["UseAppConfig"]; var appConfigEndpoint = Configuration["AppConfigEndpoint"]; var featureFlagConfig = Configuration["FeatureManagement:SalesWeekend"]; }

Feature Flag Diagnostics

UseAppConfig @(useAppConfig ?? "(not set)")
AppConfigEndpoint @(string.IsNullOrEmpty(appConfigEndpoint) ? "(not set)" : appConfigEndpoint)
SalesWeekend Feature Flag Enabled @salesWeekendEnabled
FeatureManagement:SalesWeekend (from config) @(featureFlagConfig ?? "(not found in config)")
ASPNETCORE_ENVIRONMENT @(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "(not set)")

What to check:

  • If SalesWeekend Feature Flag Enabled is True, the banner should show.
  • If it's False, verify the feature flag in Azure App Configuration → Feature manager.
  • Ensure the feature flag name is exactly SalesWeekend (case-sensitive).
  • Ensure the feature flag has no label.
================================================ FILE: src/Web/Pages/Index.cshtml ================================================ @page @{ ViewData["Title"] = "Catalog"; @model IndexModel }
@if (Model.CatalogModel.CatalogItems.Any()) {
@foreach (var catalogItem in Model.CatalogModel.CatalogItems) {
}
} else {
@Model.SettingsModel.NoResultsMessage
}
================================================ FILE: src/Web/Pages/Index.cshtml.cs ================================================ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.Web.Services; using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.Extensions.Options; namespace Microsoft.eShopWeb.Web.Pages; public class IndexModel : PageModel { private readonly ICatalogViewModelService _catalogViewModelService; public SettingsViewModel SettingsModel { get; } public IndexModel(ICatalogViewModelService catalogViewModelService, IOptionsSnapshot options) { _catalogViewModelService = catalogViewModelService; SettingsModel = options.Value; } public required CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) { CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); } } ================================================ FILE: src/Web/Pages/Privacy.cshtml ================================================ @page @model PrivacyModel @{ ViewData["Title"] = "Privacy Policy"; }

@ViewData["Title"]

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

================================================ FILE: src/Web/Pages/Privacy.cshtml.cs ================================================ using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.eShopWeb.Web.Pages; public class PrivacyModel : PageModel { public void OnGet() { } } ================================================ FILE: src/Web/Pages/SettingsViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.Pages; public class SettingsViewModel { public string? NoResultsMessage { get; set; } } ================================================ FILE: src/Web/Pages/Shared/Components/BasketComponent/Basket.cs ================================================ using System; using System.Threading.Tasks; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent; public class Basket : ViewComponent { private readonly IBasketViewModelService _basketService; private readonly SignInManager _signInManager; public Basket(IBasketViewModelService basketService, SignInManager signInManager) { _basketService = basketService; _signInManager = signInManager; } public async Task InvokeAsync() { var vm = new BasketComponentViewModel { ItemsCount = await CountTotalBasketItems() }; return View(vm); } private async Task CountTotalBasketItems() { if (_signInManager.IsSignedIn(HttpContext.User)) { Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name)); return await _basketService.CountTotalBasketItems(User.Identity.Name); } string? anonymousId = GetAnnonymousIdFromCookie(); if (anonymousId == null) return 0; return await _basketService.CountTotalBasketItems(anonymousId); } private string? GetAnnonymousIdFromCookie() { if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) { var id = Request.Cookies[Constants.BASKET_COOKIENAME]; if (Guid.TryParse(id, out var _)) { return id; } } return null; } } ================================================ FILE: src/Web/Pages/Shared/Components/BasketComponent/Default.cshtml ================================================ @model BasketComponentViewModel @{ ViewData["Title"] = "My Basket"; }
@Model.ItemsCount
================================================ FILE: src/Web/Pages/Shared/_editCatalog.cshtml ================================================ @model CatalogItemViewModel
@Model.Name
================================================ FILE: src/Web/Pages/Shared/_pagination.cshtml ================================================ @model PaginationInfoViewModel @{ var prevRouteData = Context.Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); if (prevRouteData.ContainsKey("pageId")) prevRouteData.Remove("pageId"); prevRouteData.Add("pageId", (Model.ActualPage - 1).ToString()); var nextRouteData = Context.Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); if (nextRouteData.ContainsKey("pageId")) nextRouteData.Remove("pageId"); nextRouteData.Add("pageId", (Model.ActualPage + 1).ToString()); }
================================================ FILE: src/Web/Pages/Shared/_product.cshtml ================================================ @model CatalogItemViewModel
@Model.Name
@Model.Price.ToString("N2")
================================================ FILE: src/Web/Pages/_ViewImports.cshtml ================================================ @using Microsoft.eShopWeb.Web @using Microsoft.eShopWeb.Web.ViewModels @using Microsoft.eShopWeb.Web.ViewModels.Account @using Microsoft.eShopWeb.Web.ViewModels.Manage @using Microsoft.eShopWeb.Web.Pages @using Microsoft.AspNetCore.Identity @using Microsoft.eShopWeb.Infrastructure.Identity @namespace Microsoft.eShopWeb.Web.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.FeatureManagement.AspNetCore ================================================ FILE: src/Web/Pages/_ViewStart.cshtml ================================================ @{ Layout = "_Layout"; } ================================================ FILE: src/Web/Program.cs ================================================ using System.Net.Mime; using Ardalis.ListStartupServices; using Azure.Identity; using BlazorAdmin; using BlazorAdmin.Services; using Blazored.LocalStorage; using BlazorShared; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web; using Microsoft.eShopWeb.Web.Configuration; using Microsoft.eShopWeb.Web.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.eShopWeb.Web.Pages; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); builder.Logging.AddConsole(); builder.Configuration.AddEnvironmentVariables(); if (builder.Environment.IsDevelopment() || builder.Environment.EnvironmentName == "Docker") { // Configure SQL Server (local) Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); } else { // Configure SQL Server (prod) var credential = new ChainedTokenCredential(new AzureDeveloperCliCredential(), new DefaultAzureCredential()); builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration["AZURE_KEY_VAULT_ENDPOINT"] ?? ""), credential); builder.Services.AddDbContext(c => { var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_CATALOG_CONNECTION_STRING_KEY"] ?? ""]; c.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); }); builder.Services.AddDbContext(options => { var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY"] ?? ""]; options.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); }); } builder.Services.AddCookieSettings(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Lax; }); builder.Services.AddIdentity() .AddDefaultUI() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Services.AddScoped(); builder.Services.AddCoreServices(builder.Configuration); builder.Services.AddWebServices(builder.Configuration); // Add memory cache services builder.Services.AddMemoryCache(); builder.Services.AddRouting(options => { // Replace the type and the name used to refer to it with your own // IOutboundParameterTransformer implementation options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); }); builder.Services.AddMvc(options => { options.Conventions.Add(new RouteTokenTransformerConvention( new SlugifyParameterTransformer())); }); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(options => { options.Conventions.AuthorizePage("/Basket/Checkout"); }); builder.Services.AddHttpContextAccessor(); builder.Services .AddHealthChecks() .AddCheck("api_health_check", tags: new[] { "apiHealthCheck" }) .AddCheck("home_page_health_check", tags: new[] { "homePageHealthCheck" }); builder.Services.Configure(config => { config.Services = new List(builder.Services); config.Path = "/allservices"; }); // Initialize useAppConfig parameter var useAppConfig = false; Boolean.TryParse(builder.Configuration["UseAppConfig"], out useAppConfig); // Add Azure App Configuration middleware to the container of services. builder.Services.AddAzureAppConfiguration(); // Load configuration from Azure App Configuration if (useAppConfig) { builder.Configuration.AddAzureAppConfiguration(options => { var appConfigEndpoint = builder.Configuration["AppConfigEndpoint"]; if (String.IsNullOrEmpty(appConfigEndpoint)) { throw new Exception("AppConfigEndpoint is not set in the configuration. Please set AppConfigEndpoint in the configuration."); } options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) .ConfigureRefresh(refresh => { // Default cache expiration is 30 seconds refresh.Register("eShopWeb:Settings:NoResultsMessage").SetRefreshInterval(TimeSpan.FromSeconds(10)); }) .UseFeatureFlags(featureFlagOptions => { // Default cache expiration is 30 seconds featureFlagOptions.SetRefreshInterval(TimeSpan.FromSeconds(10)); }); }); } // Add Feature Management AFTER Azure App Configuration is loaded builder.Services.AddFeatureManagement(); // Bind configuration "eShopWeb:Settings" section to the Settings object // This must be AFTER Azure App Configuration is added so it picks up remote values builder.Services.Configure(builder.Configuration.GetSection("eShopWeb:Settings")); // blazor configuration var configSection = builder.Configuration.GetRequiredSection(BaseUrlConfiguration.CONFIG_NAME); builder.Services.Configure(configSection); var baseUrlConfig = configSection.Get(); // Blazor Admin Required Services for Prerendering builder.Services.AddScoped(s => new HttpClient { BaseAddress = new Uri(baseUrlConfig!.WebBase) }); // add blazor services builder.Services.AddBlazoredLocalStorage(); builder.Services.AddServerSideBlazor(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddBlazorServices(); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); var app = builder.Build(); if (useAppConfig) { // Use Azure App Configuration middleware for dynamic configuration refresh. app.UseAzureAppConfiguration(); } app.Logger.LogInformation("App created..."); app.Logger.LogInformation("Seeding Database..."); using (var scope = app.Services.CreateScope()) { var scopedProvider = scope.ServiceProvider; try { var catalogContext = scopedProvider.GetRequiredService(); await CatalogContextSeed.SeedAsync(catalogContext, app.Logger); var userManager = scopedProvider.GetRequiredService>(); var roleManager = scopedProvider.GetRequiredService>(); var identityContext = scopedProvider.GetRequiredService(); await AppIdentityDbContextSeed.SeedAsync(identityContext, userManager, roleManager); } catch (Exception ex) { app.Logger.LogError(ex, "An error occurred seeding the DB."); } } var catalogBaseUrl = builder.Configuration.GetValue(typeof(string), "CatalogBaseUrl") as string; if (!string.IsNullOrEmpty(catalogBaseUrl)) { app.Use((context, next) => { context.Request.PathBase = new PathString(catalogBaseUrl); return next(); }); } app.UseHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { var result = new { status = report.Status.ToString(), errors = report.Entries.Select(e => new { key = e.Key, value = Enum.GetName(typeof(HealthStatus), e.Value.Status) }) }.ToJson(); context.Response.ContentType = MediaTypeNames.Application.Json; await context.Response.WriteAsync(result); } }); if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Docker") { app.Logger.LogInformation("Adding Development middleware..."); app.UseDeveloperExceptionPage(); app.UseShowAllServicesMiddleware(); app.UseMigrationsEndPoint(); app.UseWebAssemblyDebugging(); } else { app.Logger.LogInformation("Adding non-Development middleware..."); app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}"); app.MapRazorPages(); app.MapHealthChecks("home_page_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("homePageHealthCheck") }); app.MapHealthChecks("api_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("apiHealthCheck") }); //endpoints.MapBlazorHub("/admin"); app.MapFallbackToFile("index.html"); app.Logger.LogInformation("LAUNCHING"); app.Run(); ================================================ FILE: src/Web/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:17469", "sslPort": 44315 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "Web": { "commandName": "Project", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "AZURE_TENANT_ID": "{azure-tenant-id}", "AZURE_CLIENT_ID": "{azure-client-id}", "AZURE_CLIENT_SECRET": "{azure-client-secret}" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "Web - PROD": { "commandName": "Project", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" } } } ================================================ FILE: src/Web/Services/BasketViewModelService.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Pages.Basket; namespace Microsoft.eShopWeb.Web.Services; public class BasketViewModelService : IBasketViewModelService { private readonly IRepository _basketRepository; private readonly IUriComposer _uriComposer; private readonly IBasketQueryService _basketQueryService; private readonly IRepository _itemRepository; public BasketViewModelService(IRepository basketRepository, IRepository itemRepository, IUriComposer uriComposer, IBasketQueryService basketQueryService) { _basketRepository = basketRepository; _uriComposer = uriComposer; _basketQueryService = basketQueryService; _itemRepository = itemRepository; } public async Task GetOrCreateBasketForUser(string userName) { var basketSpec = new BasketWithItemsSpecification(userName); var basket = (await _basketRepository.FirstOrDefaultAsync(basketSpec)); if (basket == null) { return await CreateBasketForUser(userName); } var viewModel = await Map(basket); return viewModel; } private async Task CreateBasketForUser(string userId) { var basket = new Basket(userId); await _basketRepository.AddAsync(basket); return new BasketViewModel() { BuyerId = basket.BuyerId, Id = basket.Id, }; } private async Task> GetBasketItems(IReadOnlyCollection basketItems) { var catalogItemsSpecification = new CatalogItemsSpecification(basketItems.Select(b => b.CatalogItemId).ToArray()); var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification); var items = basketItems.Select(basketItem => { var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId); var basketItemViewModel = new BasketItemViewModel { Id = basketItem.Id, UnitPrice = basketItem.UnitPrice, Quantity = basketItem.Quantity, CatalogItemId = basketItem.CatalogItemId, PictureUrl = _uriComposer.ComposePicUri(catalogItem.PictureUri), ProductName = catalogItem.Name }; return basketItemViewModel; }).ToList(); return items; } public async Task Map(Basket basket) { return new BasketViewModel() { BuyerId = basket.BuyerId, Id = basket.Id, Items = await GetBasketItems(basket.Items) }; } public async Task CountTotalBasketItems(string username) { var counter = await _basketQueryService.CountTotalBasketItems(username); return counter; } } ================================================ FILE: src/Web/Services/CachedCatalogViewModelService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.eShopWeb.Web.Extensions; using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.eShopWeb.Web.Services; public class CachedCatalogViewModelService : ICatalogViewModelService { private readonly IMemoryCache _cache; private readonly CatalogViewModelService _catalogViewModelService; public CachedCatalogViewModelService(IMemoryCache cache, CatalogViewModelService catalogViewModelService) { _cache = cache; _catalogViewModelService = catalogViewModelService; } public async Task> GetBrands() { return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetBrands(); })) ?? new List(); } public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) { var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId); return (await _cache.GetOrCreateAsync(cacheKey, async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId); })) ?? new CatalogIndexViewModel(); } public async Task> GetTypes() { return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetTypes(); })) ?? new List(); } } ================================================ FILE: src/Web/Services/CatalogItemViewModelService.cs ================================================ using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Services; public class CatalogItemViewModelService : ICatalogItemViewModelService { private readonly IRepository _catalogItemRepository; public CatalogItemViewModelService(IRepository catalogItemRepository) { _catalogItemRepository = catalogItemRepository; } public async Task UpdateCatalogItem(CatalogItemViewModel viewModel) { var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id); Guard.Against.Null(existingCatalogItem, nameof(existingCatalogItem)); CatalogItem.CatalogItemDetails details = new(viewModel.Name, existingCatalogItem.Description, viewModel.Price); existingCatalogItem.UpdateDetails(details); await _catalogItemRepository.UpdateAsync(existingCatalogItem); } } ================================================ FILE: src/Web/Services/CatalogViewModelService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.Web.Services; /// /// This is a UI-specific service so belongs in UI project. It does not contain any business logic and works /// with UI-specific types (view models and SelectListItem types). /// public class CatalogViewModelService : ICatalogViewModelService { private readonly ILogger _logger; private readonly IRepository _itemRepository; private readonly IRepository _brandRepository; private readonly IRepository _typeRepository; private readonly IUriComposer _uriComposer; public CatalogViewModelService( ILoggerFactory loggerFactory, IRepository itemRepository, IRepository brandRepository, IRepository typeRepository, IUriComposer uriComposer) { _logger = loggerFactory.CreateLogger(); _itemRepository = itemRepository; _brandRepository = brandRepository; _typeRepository = typeRepository; _uriComposer = uriComposer; } public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) { _logger.LogInformation("GetCatalogItems called."); var filterSpecification = new CatalogFilterSpecification(brandId, typeId); var filterPaginatedSpecification = new CatalogFilterPaginatedSpecification(itemsPage * pageIndex, itemsPage, brandId, typeId); // the implementation below using ForEach and Count. We need a List. var itemsOnPage = await _itemRepository.ListAsync(filterPaginatedSpecification); var totalItems = await _itemRepository.CountAsync(filterSpecification); var vm = new CatalogIndexViewModel() { CatalogItems = itemsOnPage.Select(i => new CatalogItemViewModel() { Id = i.Id, Name = i.Name, PictureUri = _uriComposer.ComposePicUri(i.PictureUri), Price = i.Price }).ToList(), Brands = (await GetBrands()).ToList(), Types = (await GetTypes()).ToList(), BrandFilterApplied = brandId ?? 0, TypesFilterApplied = typeId ?? 0, PaginationInfo = new PaginationInfoViewModel() { ActualPage = pageIndex, ItemsPerPage = itemsOnPage.Count, TotalItems = totalItems, TotalPages = int.Parse(Math.Ceiling(((decimal)totalItems / itemsPage)).ToString()) } }; vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : ""; vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : ""; return vm; } public async Task> GetBrands() { _logger.LogInformation("GetBrands called."); var brands = await _brandRepository.ListAsync(); var items = brands .Select(brand => new SelectListItem() { Value = brand.Id.ToString(), Text = brand.Brand }) .OrderBy(b => b.Text) .ToList(); var allItem = new SelectListItem() { Value = null, Text = "All", Selected = true }; items.Insert(0, allItem); return items; } public async Task> GetTypes() { _logger.LogInformation("GetTypes called."); var types = await _typeRepository.ListAsync(); var items = types .Select(type => new SelectListItem() { Value = type.Id.ToString(), Text = type.Type }) .OrderBy(t => t.Text) .ToList(); var allItem = new SelectListItem() { Value = null, Text = "All", Selected = true }; items.Insert(0, allItem); return items; } } ================================================ FILE: src/Web/SlugifyParameterTransformer.cs ================================================ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Routing; namespace Microsoft.eShopWeb.Web; public class SlugifyParameterTransformer : IOutboundParameterTransformer { public string? TransformOutbound(object? value) { if (value == null) { return null; } string? str = value.ToString(); if (string.IsNullOrEmpty(str)) { return null; } // Slugify value return Regex.Replace(str, "([a-z])([A-Z])", "$1-$2").ToLower(); } } ================================================ FILE: src/Web/ViewModels/Account/LoginViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Account; public class LoginViewModel { [Required] [EmailAddress] public string? Email { get; set; } [Required] [DataType(DataType.Password)] public string? Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } ================================================ FILE: src/Web/ViewModels/Account/LoginWith2faViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Account; public class LoginWith2faViewModel { [Required] [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Text)] [Display(Name = "Authenticator code")] public string? TwoFactorCode { get; set; } [Display(Name = "Remember this machine")] public bool RememberMachine { get; set; } public bool RememberMe { get; set; } } ================================================ FILE: src/Web/ViewModels/Account/RegisterViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Account; public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string? Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string? Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string? ConfirmPassword { get; set; } } ================================================ FILE: src/Web/ViewModels/Account/ResetPasswordViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Account; public class ResetPasswordViewModel { [Required] [EmailAddress] public string? Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] public string? Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string? ConfirmPassword { get; set; } public string? Code { get; set; } } ================================================ FILE: src/Web/ViewModels/BasketComponentViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels; public class BasketComponentViewModel { public int ItemsCount { get; set; } } ================================================ FILE: src/Web/ViewModels/CatalogIndexViewModel.cs ================================================ using Microsoft.AspNetCore.Mvc.Rendering; namespace Microsoft.eShopWeb.Web.ViewModels; public class CatalogIndexViewModel { public List CatalogItems { get; set; } = new List(); public List? Brands { get; set; } = new List(); public List? Types { get; set; } = new List(); public int? BrandFilterApplied { get; set; } public int? TypesFilterApplied { get; set; } public PaginationInfoViewModel? PaginationInfo { get; set; } } ================================================ FILE: src/Web/ViewModels/CatalogItemViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels; public class CatalogItemViewModel { public int Id { get; set; } public string? Name { get; set; } public string? PictureUri { get; set; } public decimal Price { get; set; } } ================================================ FILE: src/Web/ViewModels/File/FileViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels.File; public class FileViewModel { public string? FileName { get; set; } public string? Url { get; set; } public string? DataBase64 { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/ChangePasswordViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class ChangePasswordViewModel { [Required] [DataType(DataType.Password)] [Display(Name = "Current password")] public string? OldPassword { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "New password")] public string? NewPassword { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string? ConfirmPassword { get; set; } public string? StatusMessage { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/EnableAuthenticatorViewModel.cs ================================================ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class EnableAuthenticatorViewModel { [Required] [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Text)] [Display(Name = "Verification Code")] public string? Code { get; set; } [BindNever] public string? SharedKey { get; set; } [BindNever] public string? AuthenticatorUri { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/ExternalLoginsViewModel.cs ================================================ using System.Collections.Generic; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class ExternalLoginsViewModel { public IList? CurrentLogins { get; set; } public IList? OtherLogins { get; set; } public bool ShowRemoveButton { get; set; } public string? StatusMessage { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/IndexViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class IndexViewModel { public string? Username { get; set; } public bool IsEmailConfirmed { get; set; } [Required] [EmailAddress] public string? Email { get; set; } [Phone] [Display(Name = "Phone number")] public string? PhoneNumber { get; set; } public string? StatusMessage { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/RemoveLoginViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class RemoveLoginViewModel { [Required] public string LoginProvider { get; set; } = string.Empty; [Required] public string ProviderKey { get; set; } = string.Empty; } ================================================ FILE: src/Web/ViewModels/Manage/SetPasswordViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class SetPasswordViewModel { [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "New password")] public string? NewPassword { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string? ConfirmPassword { get; set; } public string? StatusMessage { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/ShowRecoveryCodesViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class ShowRecoveryCodesViewModel { public string[]? RecoveryCodes { get; set; } } ================================================ FILE: src/Web/ViewModels/Manage/TwoFactorAuthenticationViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class TwoFactorAuthenticationViewModel { public bool HasAuthenticator { get; set; } public int RecoveryCodesLeft { get; set; } public bool Is2faEnabled { get; set; } } ================================================ FILE: src/Web/ViewModels/OrderDetailViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels; public class OrderDetailViewModel : OrderViewModel { public List OrderItems { get; set; } = new(); } ================================================ FILE: src/Web/ViewModels/OrderItemViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels; public class OrderItemViewModel { public int ProductId { get; set; } public string? ProductName { get; set; } public decimal UnitPrice { get; set; } public decimal Discount => 0; public int Units { get; set; } public string? PictureUrl { get; set; } } ================================================ FILE: src/Web/ViewModels/OrderViewModel.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.Web.ViewModels; public class OrderViewModel { private const string DEFAULT_STATUS = "Pending"; public int OrderNumber { get; set; } public DateTimeOffset OrderDate { get; set; } public decimal Total { get; set; } public string Status => DEFAULT_STATUS; public Address? ShippingAddress { get; set; } } ================================================ FILE: src/Web/ViewModels/PaginationInfoViewModel.cs ================================================ namespace Microsoft.eShopWeb.Web.ViewModels; public class PaginationInfoViewModel { public int TotalItems { get; set; } public int ItemsPerPage { get; set; } public int ActualPage { get; set; } public int TotalPages { get; set; } public string? Previous { get; set; } public string? Next { get; set; } } ================================================ FILE: src/Web/Views/Account/Lockout.cshtml ================================================ @{ ViewData["Title"] = "Locked out"; }

@ViewData["Title"]

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

================================================ FILE: src/Web/Views/Account/LoginWith2fa.cshtml ================================================ @model LoginWith2faViewModel @{ ViewData["Title"] = "Two-factor authentication"; }

@ViewData["Title"]


Your login is protected with an authenticator app. Enter your authenticator code below.

Don't have access to your authenticator device? You can log in with a recovery code.

@section Scripts { } ================================================ FILE: src/Web/Views/Manage/ChangePassword.cshtml ================================================ @model ChangePasswordViewModel @{ ViewData["Title"] = "Change password"; ViewData.AddActivePage(ManageNavPages.ChangePassword); }

@ViewData["Title"]

@section Scripts { } ================================================ FILE: src/Web/Views/Manage/Disable2fa.cshtml ================================================ @{ ViewData["Title"] = "Disable two-factor authentication (2FA)"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); }

@ViewData["Title"]

================================================ FILE: src/Web/Views/Manage/EnableAuthenticator.cshtml ================================================ @model EnableAuthenticatorViewModel @{ ViewData["Title"] = "Enable authenticator"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); }

@ViewData["Title"]

To use an authenticator app go through the following steps:

  1. Download a two-factor authenticator app like Microsoft Authenticator for Windows Phone, Android and iOS or Google Authenticator for Android and iOS.

  2. Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    To enable QR code generation please read our documentation.
  3. Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below.

@section Scripts { } ================================================ FILE: src/Web/Views/Manage/ExternalLogins.cshtml ================================================ @model ExternalLoginsViewModel @{ ViewData["Title"] = "Manage your external logins"; ViewData.AddActivePage(ManageNavPages.ExternalLogins); } @if (Model.CurrentLogins?.Count > 0) {

Registered Logins

@foreach (var login in Model.CurrentLogins) { }
@login.LoginProvider @if (Model.ShowRemoveButton) {
} else { @:   }
} @if (Model.OtherLogins?.Count > 0) {

Add another service to log in.


@foreach (var provider in Model.OtherLogins) { }

} ================================================ FILE: src/Web/Views/Manage/GenerateRecoveryCodes.cshtml ================================================ @{ ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); }

@ViewData["Title"]

================================================ FILE: src/Web/Views/Manage/ManageNavPages.cs ================================================ using System; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Microsoft.eShopWeb.Web.Views.Manage; public static class ManageNavPages { public static string ActivePageKey => "ActivePage"; public static string Index => "Index"; public static string ChangePassword => "ChangePassword"; public static string ExternalLogins => "ExternalLogins"; public static string TwoFactorAuthentication => "TwoFactorAuthentication"; public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); public static string PageNavClass(ViewContext viewContext, string page) { var activePage = viewContext.ViewData["ActivePage"] as string; return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : string.Empty; } public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage; } ================================================ FILE: src/Web/Views/Manage/MyAccount.cshtml ================================================ @model IndexViewModel @{ ViewData["Title"] = "Profile"; ViewData.AddActivePage(ManageNavPages.Index); }

@ViewData["Title"]

@if (Model.IsEmailConfirmed) {
} else { }
@section Scripts { } ================================================ FILE: src/Web/Views/Manage/ResetAuthenticator.cshtml ================================================ @{ ViewData["Title"] = "Reset authenticator key"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); }

@ViewData["Title"]

================================================ FILE: src/Web/Views/Manage/SetPassword.cshtml ================================================ @model SetPasswordViewModel @{ ViewData["Title"] = "Set password"; ViewData.AddActivePage(ManageNavPages.ChangePassword); }

Set your password

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

@section Scripts { } ================================================ FILE: src/Web/Views/Manage/ShowRecoverCodes.cshtml ================================================ @model ShowRecoveryCodesViewModel @{ ViewData["Title"] = "Recovery codes"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); }

@ViewData["Title"]

@if (Model.RecoveryCodes != null) { @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) { @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
} }
© 2023 GitHub, Inc. ================================================ FILE: src/Web/Views/Manage/TwoFactorAuthentication.cshtml ================================================ @model TwoFactorAuthenticationViewModel @{ ViewData["Title"] = "Two-factor authentication"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); }

@ViewData["Title"]

@if (Model.Is2faEnabled) { if (Model.RecoveryCodesLeft == 0) {
You have no recovery codes left.

You must generate a new set of recovery codes before you can log in with a recovery code.

} else if (Model.RecoveryCodesLeft == 1) {
You have 1 recovery code left.

You can generate a new set of recovery codes.

} else if (Model.RecoveryCodesLeft <= 3) {
You have @Model.RecoveryCodesLeft recovery codes left.

You should generate a new set of recovery codes.

} Disable 2FA Reset recovery codes }
Authenticator app
@if (!Model.HasAuthenticator) { Add authenticator app } else { Configure authenticator app Reset authenticator key } @section Scripts { } ================================================ FILE: src/Web/Views/Manage/_Layout.cshtml ================================================ @{ Layout = "/Views/Shared/_Layout.cshtml"; }

Manage your account

Change your account settings


@RenderBody()
@section Scripts { @RenderSection("Scripts", required: false) } ================================================ FILE: src/Web/Views/Manage/_ManageNav.cshtml ================================================ @inject SignInManager SignInManager @{ var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); } ================================================ FILE: src/Web/Views/Manage/_StatusMessage.cshtml ================================================ @model string @if (!String.IsNullOrEmpty(Model)) { var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; } ================================================ FILE: src/Web/Views/Manage/_ViewImports.cshtml ================================================ @using Microsoft.eShopWeb.Web.Views.Manage ================================================ FILE: src/Web/Views/Order/Detail.cshtml ================================================ @model OrderDetailViewModel @{ ViewData["Title"] = "My Order History"; } @{ ViewData["Title"] = "Order Detail"; }
Order number
Date
Total
Status
@Model.OrderNumber
@Model.OrderDate
$@Model.Total.ToString("N2")
@Model.Status
Shipping Address
@Model.ShippingAddress?.Street
@Model.ShippingAddress?.City
@Model.ShippingAddress?.Country
ORDER DETAILS
@for (int i = 0; i < Model.OrderItems.Count; i++) { var item = Model.OrderItems[i];
@item.ProductName
$ @item.UnitPrice.ToString("N2")
@item.Units
$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")
}
TOTAL
$ @Model.Total.ToString("N2")
================================================ FILE: src/Web/Views/Order/MyOrders.cshtml ================================================ @model IEnumerable @{ ViewData["Title"] = "My Order History"; }

@ViewData["Title"]

Order number
Date
Total
Status
@if (Model != null && Model.Any()) { @foreach (var item in Model) {
@Html.DisplayFor(modelItem => item.OrderNumber)
@Html.DisplayFor(modelItem => item.OrderDate)
$ @Html.DisplayFor(modelItem => item.Total)
@Html.DisplayFor(modelItem => item.Status)
Detail
@if (item.Status.ToLower() == "submitted") { Cancel }
} }
================================================ FILE: src/Web/Views/Shared/Components/Basket/Default.cshtml ================================================ @model BasketComponentViewModel @{ ViewData["Title"] = "My Basket"; }
@Model.ItemsCount
================================================ FILE: src/Web/Views/Shared/Error.cshtml ================================================ @{ ViewData["Title"] = "Error"; }

Error.

An error occurred while processing your request.

Development Mode

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

Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application.

================================================ FILE: src/Web/Views/Shared/_CookieConsentPartial.cshtml ================================================ @using Microsoft.AspNetCore.Http.Features @{ var consentFeature = Context.Features.Get(); var showBanner = !consentFeature?.CanTrack ?? false; var cookieString = consentFeature?.CreateConsentCookie(); } @if (showBanner) { } ================================================ FILE: src/Web/Views/Shared/_Layout.cshtml ================================================  @ViewData["Title"] - Microsoft.eShopOnWeb
@RenderBody()
@RenderSection("scripts", required: false) ================================================ FILE: src/Web/Views/Shared/_LoginPartial.cshtml ================================================ @if (Context!.User!.Identity!.IsAuthenticated) {
@await Component.InvokeAsync("Basket")
} else {
@await Component.InvokeAsync("Basket")
} ================================================ FILE: src/Web/Views/Shared/_ValidationScriptsPartial.cshtml ================================================  ================================================ FILE: src/Web/Views/_ViewImports.cshtml ================================================ @using Microsoft.eShopWeb.Web @using Microsoft.eShopWeb.Web.ViewModels @using Microsoft.eShopWeb.Web.ViewModels.Account @using Microsoft.eShopWeb.Web.ViewModels.Manage @using Microsoft.eShopWeb.Web.Pages @using Microsoft.AspNetCore.Identity @using Microsoft.eShopWeb.Infrastructure.Identity @namespace Microsoft.eShopWeb.Web.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers ================================================ FILE: src/Web/Views/_ViewStart.cshtml ================================================ @{ Layout = "_Layout"; } ================================================ FILE: src/Web/Web.csproj ================================================  enable enable Microsoft.eShopWeb.Web aspnet-Web2-1FA3F72E-E7E3-4360-9E49-1CCCD7FE85F7 latest all runtime; build; native; contentfiles; analyzers; buildtransitive Always PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: src/Web/appsettings.Development.json ================================================ { "baseUrls": { "apiBase": "https://localhost:5099/api/", "webBase": "https://localhost:44315/" }, "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } } ================================================ FILE: src/Web/appsettings.Docker.json ================================================ { "ConnectionStrings": { "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;", "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;" }, "baseUrls": { "apiBase": "http://localhost:5200/api/", "webBase": "http://host.docker.internal:5106/" }, "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } } ================================================ FILE: src/Web/appsettings.json ================================================ { "baseUrls": { "apiBase": "https://localhost:5099/api/", "webBase": "https://localhost:44315/" }, "ConnectionStrings": { "CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;", "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;" }, "CatalogBaseUrl": "", "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning", "Microsoft": "Warning", "System": "Warning" }, "AllowedHosts": "*" }, "eShopWeb": { "Settings": { "NoResultsMessage": "THERE ARE NO RESULTS THAT MATCH YOUR SEARCH" } }, "UseAppConfig": false, "AppConfigEndpoint": "" } ================================================ FILE: src/Web/bundleconfig.json ================================================ [ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "wwwroot/css/app.css", "wwwroot/css/app.component.css", "wwwroot/css/shared/components/header/header.css", "wwwroot/css/shared/components/identity/identity.css", "wwwroot/css/shared/components/pager/pager.css", "wwwroot/css/basket/basket.component.css", "wwwroot/css/basket/basket-status/basket-status.component.css", "wwwroot/css/catalog/catalog.component.css", "wwwroot/css/orders/orders.component.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "wwwroot/js/site.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ] ================================================ FILE: src/Web/compilerconfig.json ================================================ [ { "outputFile": "wwwroot/css/shared/components/header/header.css", "inputFile": "wwwroot/css/shared/components/header/header.scss" }, { "outputFile": "wwwroot/css/orders/orders.component.css", "inputFile": "wwwroot/css/orders/orders.component.scss" }, { "outputFile": "wwwroot/css/catalog/catalog.component.css", "inputFile": "wwwroot/css/catalog/catalog.component.scss" }, { "outputFile": "wwwroot/css/basket/basket.component.css", "inputFile": "wwwroot/css/basket/basket.component.scss" }, { "outputFile": "wwwroot/css/basket/basket-status/basket-status.component.css", "inputFile": "wwwroot/css/basket/basket-status/basket-status.component.scss" }, { "outputFile": "wwwroot/css/app.component.css", "inputFile": "wwwroot/css/app.component.scss" }, { "outputFile": "wwwroot/css/_variables.css", "inputFile": "wwwroot/css/_variables.scss" }, { "outputFile": "wwwroot/css/shared/components/pager/pager.css", "inputFile": "wwwroot/css/shared/components/pager/pager.scss" } ] ================================================ FILE: src/Web/compilerconfig.json.defaults ================================================ { "compilers": { "less": { "autoPrefix": "", "cssComb": "none", "ieCompat": true, "strictMath": false, "strictUnits": false, "relativeUrls": true, "rootPath": "", "sourceMapRoot": "", "sourceMapBasePath": "", "sourceMap": false }, "sass": { "autoPrefix": "", "includePath": "", "indentType": "space", "indentWidth": 2, "outputStyle": "nested", "Precision": 5, "relativeUrls": true, "sourceMapRoot": "", "lineFeed": "", "sourceMap": false }, "stylus": { "sourceMap": false }, "babel": { "sourceMap": false }, "coffeescript": { "bare": false, "runtimeMode": "node", "sourceMap": false }, "handlebars": { "root": "", "noBOM": false, "name": "", "namespace": "", "knownHelpersOnly": false, "forcePartial": false, "knownHelpers": [], "commonjs": "", "amd": false, "sourceMap": false } }, "minifiers": { "css": { "enabled": true, "termSemicolons": true, "gzip": false }, "javascript": { "enabled": true, "termSemicolons": true, "gzip": false } } } ================================================ FILE: src/Web/key-768c1632-cf7b-41a9-bb7a-bff228ae8fba.xml ================================================  2021-12-01T14:37:52.0438755Z 2021-12-01T14:37:52.0246578Z 2022-03-01T14:37:52.0246578Z PF3GdfO7PnvHYvXyD5nxmoQ91pY9qfA0rjRsdXHdUQbE1Mg9Xok2gXLY2zn8XemsySH37UGrGknht8u/PlehWg== ================================================ FILE: src/Web/libman.json ================================================ { "version": "1.0", "defaultProvider": "cdnjs", "libraries": [ { "library": "jquery@3.6.3", "destination": "wwwroot/lib/jquery/" }, { "library": "twitter-bootstrap@3.4.1", "files": [ "css/bootstrap.css", "css/bootstrap.css.map", "css/bootstrap.min.css", "css/bootstrap.min.css.map", "js/bootstrap.js", "js/bootstrap.min.js" ], "destination": "wwwroot/lib/bootstrap/dist/" }, { "library": "jquery-validation-unobtrusive@4.0.0", "destination": "wwwroot/lib/jquery-validation-unobtrusive/" }, { "library": "jquery-validate@1.19.5", "destination": "wwwroot/lib/jquery-validate/", "files": [ "jquery.validate.min.js", "jquery.validate.js" ] }, { "library": "toastr.js@2.1.4", "destination": "wwwroot/lib/toastr/" }, { "library": "aspnet-signalr@1.0.27", "files": [ "signalr.js", "signalr.min.js" ], "destination": "wwwroot/lib/@aspnet/signalr/dist/browser/" } ] } ================================================ FILE: src/Web/wwwroot/css/_variables.css ================================================  ================================================ FILE: src/Web/wwwroot/css/_variables.scss ================================================ // Colors $color-brand: #00A69C; $color-brand-dark: darken($color-brand, 10%); $color-brand-darker: darken($color-brand, 20%); $color-brand-bright: lighten($color-brand, 10%); $color-brand-brighter: lighten($color-brand, 20%); $color-secondary: #83D01B; $color-secondary-dark: darken($color-secondary, 5%); $color-secondary-darker: darken($color-secondary, 20%); $color-secondary-bright: lighten($color-secondary, 10%); $color-secondary-brighter: lighten($color-secondary, 20%); $color-warning: #ff0000; $color-warning-dark: darken($color-warning, 5%); $color-warning-darker: darken($color-warning, 20%); $color-warning-bright: lighten($color-warning, 10%); $color-warning-brighter: lighten($color-warning, 20%); $color-background-dark: #333333; $color-background-darker: #000000; $color-background-bright: #EEEEFF; $color-background-brighter: #FFFFFF; $color-foreground-dark: #333333; $color-foreground-darker: #000000; $color-foreground-bright: #EEEEEE; $color-foreground-brighter: #FFFFFF; // Animations $animation-speed-default: .35s; $animation-speed-slow: .5s; $animation-speed-fast: .15s; // Fonts $font-weight-light: 200; $font-weight-semilight: 300; $font-weight-normal: 400; $font-weight-semibold: 600; $font-weight-bold: 700; $font-size-xs: .65rem; // 10.4px $font-size-s: .85rem; // 13.6px $font-size-m: 1rem; // 16px $font-size-l: 1.25rem; // 20px $font-size-xl: 1.5rem; // 24px // Medias $media-screen-xxs: 360px; $media-screen-xs: 640px; $media-screen-s: 768px; $media-screen-m: 1024px; $media-screen-l: 1280px; $media-screen-xl: 1440px; $media-screen-xxl: 1680px; $media-screen-xxxl: 1920px; // Borders $border-light: 1px; // Images $image_path: '../../images/'; $image-main_banner: '#{$image_path}main_banner.png'; $image-arrow_down: '#{$image_path}arrow-down.png'; ================================================ FILE: src/Web/wwwroot/css/app.component.css ================================================ .esh-app-footer { background-color: #000000; border-top: 1px solid #EEEEEE; 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; } ================================================ FILE: src/Web/wwwroot/css/app.component.scss ================================================ @import './_variables.scss'; .esh-app { &-footer { $margin: 2.5rem; $padding: 2.5rem; background-color: $color-background-darker; border-top: $border-light solid $color-foreground-bright; margin-top: $margin; padding-bottom: $padding; padding-top: $padding; width: 100%; bottom: 0; $height: 50px; &-brand { height: $height; width: 230px; } } &-header { margin: 15px; } &-wrapper { display: flex; min-height: 100vh; flex-direction: column; justify-content: space-between } } ================================================ FILE: src/Web/wwwroot/css/app.css ================================================ @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 0.35s; } a:hover, a:active { color: #75B918; transition: color 0.35s; } ================================================ FILE: src/Web/wwwroot/css/basket/basket-status/basket-status.component.css ================================================ .esh-basketstatus { cursor: pointer; display: inline-block; float: right; position: relative; transition: all 0.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: #FFFFFF; display: block; height: 1.5rem; left: 50%; position: absolute; text-align: center; top: 0; transform: translateX(-38%); transition: all 0.35s; width: 1.5rem; } .esh-basketstatus-badge-inoperative { background-color: #ff0000; border-radius: 50%; color: #FFFFFF; display: block; height: 1.5rem; left: 50%; position: absolute; text-align: center; top: 0; transform: translateX(-38%); transition: all 0.35s; width: 1.5rem; } .esh-basketstatus:hover .esh-basketstatus-badge { background-color: transparent; color: #75b918; transition: all 0.35s; } ================================================ FILE: src/Web/wwwroot/css/basket/basket-status/basket-status.component.scss ================================================ @import '../../_variables.scss'; .esh-basketstatus { cursor: pointer; display: inline-block; float: right; position: relative; transition: all $animation-speed-default; &.is-disabled { opacity: .5; pointer-events: none; } &-image { height: 36px; margin-top: .5rem; } &-badge { $size: 1.5rem; background-color: $color-secondary; border-radius: 50%; color: $color-foreground-brighter; display: block; height: $size; left: 50%; position: absolute; text-align: center; top: 0; transform: translateX(-38%); transition: all $animation-speed-default; width: $size; } &-badge-inoperative { $size: 1.5rem; background-color: $color-warning; border-radius: 50%; color: $color-foreground-brighter; display: block; height: $size; left: 50%; position: absolute; text-align: center; top: 0; transform: translateX(-38%); transition: all $animation-speed-default; width: $size; } &:hover &-badge { background-color: transparent; color: $color-secondary-dark; transition: all $animation-speed-default; } } ================================================ FILE: src/Web/wwwroot/css/basket/basket.component.css ================================================ .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 #EEEEEE; 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: #FFFFFF; display: inline-block; font-size: 1rem; font-weight: 400; margin-top: 1rem; padding: 1rem 1.5rem; text-align: center; text-transform: uppercase; transition: all 0.35s; } .esh-basket-checkout:hover { background-color: #4a760f; transition: all 0.35s; } .esh-basket-checkout:visited { color: #FFFFFF; } ================================================ FILE: src/Web/wwwroot/css/basket/basket.component.scss ================================================ @import '../variables.scss'; @mixin margin-left($distance) { margin-left: $distance; } .esh-basket { min-height: 80vh; &-titles { padding-bottom: 1rem; padding-top: 2rem; &--clean { padding-bottom: 0; padding-top: 0; } } &-title { text-transform: uppercase; } &-items { &--border { border-bottom: $border-light solid $color-foreground-bright; padding: .5rem 0; &:last-of-type { border-color: transparent; } } &-margin-left1 { @include margin-left(1px); } } $item-height: 8rem; &-item { font-size: $font-size-m; font-weight: $font-weight-semilight; &--middle { line-height: $item-height; @media screen and (max-width: $media-screen-m) { line-height: $font-size-m; } } &--mark { color: $color-brand; } } &-image { height: $item-height; } &-input { line-height: 1rem; width: 100%; } &-checkout { background-color: $color-secondary; border: 0; border-radius: 0; color: $color-foreground-brighter; display: inline-block; font-size: 1rem; font-weight: $font-weight-normal; margin-top: 1rem; padding: 1rem 1.5rem; text-align: center; text-transform: uppercase; transition: all $animation-speed-default; &:hover { background-color: $color-secondary-darker; transition: all $animation-speed-default; } &:visited { color: $color-foreground-brighter; } } } ================================================ FILE: src/Web/wwwroot/css/catalog/catalog.component.css ================================================ .esh-catalog-hero { background-image: url("../../images/main_banner.png"); background-size: cover; height: 260px; width: 100%; } .esh-catalog-title { position: relative; top: 74.28571px; } .esh-catalog-filters { background-color: #00A69C; height: 65px; } .esh-catalog-filter { -webkit-appearance: none; background-color: transparent; border-color: #00d9cc; color: #FFFFFF; cursor: pointer; margin-right: 1rem; margin-top: .5rem; min-width: 140px; outline-color: #83D01B; padding-bottom: 0; padding-left: 0.5rem; padding-right: 0.5rem; padding-top: 1.5rem; } .esh-catalog-filter option { background-color: #00A69C; } .esh-catalog-label { display: inline-block; position: relative; z-index: 0; } .esh-catalog-label::before { color: rgba(255, 255, 255, 0.5); content: attr(data-title); font-size: 0.65rem; margin-left: 0.5rem; margin-top: 0.65rem; position: absolute; text-transform: uppercase; z-index: 1; } .esh-catalog-label::after { background-image: url("../../images/arrow-down.png"); content: ''; height: 7px; position: absolute; right: 1.5rem; top: 2.5rem; width: 10px; z-index: 1; } .esh-catalog-send { background-color: #83D01B; color: #FFFFFF; cursor: pointer; font-size: 1rem; margin-top: -1.5rem; transition: all 0.35s; } .esh-catalog-send:hover { background-color: #4a760f; transition: all 0.35s; } .esh-catalog-items { margin-top: 1rem; } .esh-catalog-item { margin-bottom: 1.5rem; text-align: center; width: 33%; display: inline-block; float: none !important; } @media screen and (max-width: 1024px) { .esh-catalog-item { width: 50%; } } @media screen and (max-width: 768px) { .esh-catalog-item { width: 100%; } } .esh-catalog-thumbnail { max-width: 370px; width: 100%; } .esh-catalog-button { background-color: #83D01B; border: 0; color: #FFFFFF; cursor: pointer; font-size: 1rem; height: 3rem; margin-top: 1rem; transition: all 0.35s; width: 80%; } .esh-catalog-button.is-disabled { opacity: .5; pointer-events: none; } .esh-catalog-button:hover { background-color: #4a760f; transition: all 0.35s; } .esh-catalog-name { font-size: 1rem; font-weight: 300; margin-top: .5rem; text-align: center; text-transform: uppercase; } .esh-catalog-price { font-size: 28px; font-weight: 900; text-align: center; } .esh-catalog-price::before { content: '$'; } ================================================ FILE: src/Web/wwwroot/css/catalog/catalog.component.scss ================================================ @import '../_variables.scss'; .esh-catalog { $banner-height: 260px; &-hero { background-image: url($image-main_banner); background-size: cover; height: $banner-height; width: 100%; } &-title { position: relative; top: $banner-height / 3.5; } $filter-height: 65px; &-filters { background-color: $color-brand; height: $filter-height; } $filter-padding: .5rem; &-filter { -webkit-appearance: none; background-color: transparent; border-color: $color-brand-bright; color: $color-foreground-brighter; cursor: pointer; margin-right: 1rem; margin-top: .5rem; min-width: 140px; outline-color: $color-secondary; padding-bottom: 0; padding-left: $filter-padding; padding-right: $filter-padding; padding-top: $filter-padding * 3; option { background-color: $color-brand; } } &-label { display: inline-block; position: relative; z-index: 0; &::before { color: rgba($color-foreground-brighter, .5); content: attr(data-title); font-size: $font-size-xs; margin-left: $filter-padding; margin-top: $font-size-xs; position: absolute; text-transform: uppercase; z-index: 1; } &::after { background-image: url($image-arrow_down); content: ''; height: 7px; //png height position: absolute; right: $filter-padding * 3; top: $filter-padding * 5; width: 10px; //png width z-index: 1; } } &-send { background-color: $color-secondary; color: $color-foreground-brighter; cursor: pointer; font-size: $font-size-m; margin-top: -$filter-padding * 3; transition: all $animation-speed-default; &:hover { background-color: $color-secondary-darker; transition: all $animation-speed-default; } } &-items { margin-top: 1rem; } &-item { margin-bottom: 1.5rem; text-align: center; width: 33%; display: inline-block; float: none !important; @media screen and (max-width: $media-screen-m) { width: 50%; } @media screen and (max-width: $media-screen-s) { width: 100%; } } &-thumbnail { max-width: 370px; width: 100%; } &-button { background-color: $color-secondary; border: 0; color: $color-foreground-brighter; cursor: pointer; font-size: $font-size-m; height: 3rem; margin-top: 1rem; transition: all $animation-speed-default; width: 80%; &.is-disabled { opacity: .5; pointer-events: none; } &:hover { background-color: $color-secondary-darker; transition: all $animation-speed-default; } } &-name { font-size: $font-size-m; font-weight: $font-weight-semilight; margin-top: .5rem; text-align: center; text-transform: uppercase; } &-price { font-size: 28px; font-weight: 900; text-align: center; &::before { content: '$'; } } } ================================================ FILE: src/Web/wwwroot/css/catalog/pager.css ================================================ .esh-pager-wrapper { padding-top: 1rem; text-align: center; } .esh-pager-item-left { float: left; } .esh-pager-item-right { float: right; } .esh-pager-item--navigable { display: inline-block; cursor: pointer; } .esh-pager-item--navigable.is-disabled { opacity: 0; pointer-events: none; } .esh-pager-item--navigable:hover { color: #83D01B; } @media screen and (max-width: 1280px) { .esh-pager-item { font-size: 0.85rem; } } @media screen and (max-width: 1024px) { .esh-pager-item { margin: 0 4vw; } } ================================================ FILE: src/Web/wwwroot/css/orders/orders.component.css ================================================ .esh-orders { min-height: 80vh; overflow-x: hidden; } .esh-orders-header { background-color: #00A69C; height: 4rem; } .esh-orders-back { color: rgba(255, 255, 255, 0.4); line-height: 4rem; text-decoration: none; text-transform: uppercase; transition: color 0.35s; } .esh-orders-back:hover { color: #FFFFFF; transition: color 0.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: #EEEEFF; 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 0.35s; } .esh-orders-link:hover { color: #75b918; transition: color 0.35s; } .esh-orders-detail-section { padding-bottom: 30px; } .esh-orders-detail-title { font-size: 25px; } ================================================ FILE: src/Web/wwwroot/css/orders/orders.component.scss ================================================ @import '../variables.scss'; .esh-orders { min-height: 80vh; overflow-x: hidden; $header-height: 4rem; &-header { background-color: #00A69C; height: $header-height; } &-back { color: rgba($color-foreground-brighter, .4); line-height: $header-height; text-decoration: none; text-transform: uppercase; transition: color $animation-speed-default; &:hover { color: $color-foreground-brighter; transition: color $animation-speed-default; } } &-titles { padding-bottom: 1rem; padding-top: 2rem; } &-title { text-transform: uppercase; } &-items { $height: 2rem; height: $height; line-height: $height; position: relative; &:nth-of-type(2n + 1) { &:before { background-color: $color-background-bright; content: ''; height: 100%; left: 0; margin-left: -100vw; position: absolute; top: 0; width: 200vw; z-index: -1; } } } &-item { font-weight: $font-weight-semilight; &--hover { opacity: 0; pointer-events: none; } } &-items:hover &-item--hover { opacity: 1; pointer-events: all; } &-link { color: $color-secondary; text-decoration: none; transition: color $animation-speed-default; &:hover { color: $color-secondary-dark; transition: color $animation-speed-default; } } &-detail { &-section { padding-bottom: 30px; } &-title { font-size: 25px; } } } ================================================ FILE: src/Web/wwwroot/css/shared/components/header/header.css ================================================ .esh-header { background-color: #00A69C; height: 4rem; } .esh-header-back { color: rgba(255, 255, 255, 0.5); line-height: 4rem; text-decoration: none; text-transform: uppercase; transition: color 0.35s; } .esh-header-back:hover { color: #FFFFFF; transition: color 0.35s; } ================================================ FILE: src/Web/wwwroot/css/shared/components/header/header.scss ================================================ @import '../../../variables.scss'; .esh-header { $header-height: 4rem; background-color: $color-brand; height: $header-height; &-back { color: rgba($color-foreground-brighter, .5); line-height: $header-height; text-decoration: none; text-transform: uppercase; transition: color $animation-speed-default; &:hover { color: $color-foreground-brighter; transition: color $animation-speed-default; } } } ================================================ FILE: src/Web/wwwroot/css/shared/components/identity/identity.css ================================================ .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: 0.85rem; } } .esh-identity-image { display: inline-block; } .esh-identity-drop { background: #FFFFFF; height: 10px; width: 10rem; overflow: hidden; padding: .5rem; position: absolute; right: 0; top: 2.5rem; transition: height 0.35s; } .esh-identity:hover .esh-identity-drop { border: 1px solid #EEEEEE; height: 14rem; transition: height 0.35s; z-index: 10; } .esh-identity-item { cursor: pointer; transition: color 0.35s; } .esh-identity-item:hover { color: #75b918; transition: color 0.35s; } ================================================ FILE: src/Web/wwwroot/css/shared/components/identity/identity.scss ================================================ @import '../../../variables.scss'; .esh-identity { line-height: 3rem; position: relative; text-align: right; &-section { display: inline-block; width: 100%; } &-name { display: inline-block; &--upper { text-transform: uppercase; } @media screen and (max-width: $media-screen-s) { font-size: $font-size-s; } } &-image { display: inline-block; } &-drop { background: $color-background-brighter; height: 10px; width: 10rem; overflow: hidden; padding: .5rem; position: absolute; right: 0; top: 2.5rem; transition: height $animation-speed-default; } &:hover &-drop { border: $border-light solid $color-foreground-bright; height: 10rem; transition: height $animation-speed-default; } &-item { cursor: pointer; transition: color $animation-speed-default; &:hover { color: $color-secondary-dark; transition: color $animation-speed-default; } } } ================================================ FILE: src/Web/wwwroot/css/shared/components/pager/pager.css ================================================ .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: 0.85rem; } } @media screen and (max-width: 1024px) { .esh-pager-item { margin: 0 2.5vw; } } ================================================ FILE: src/Web/wwwroot/css/shared/components/pager/pager.scss ================================================ @import '../../../variables.scss'; .esh-pager { &-wrapper { padding-top: 1rem; text-align: center; } &-item { $margin: 5vw; margin: 0 $margin; &.is-disabled { opacity: .5; pointer-events: none; } &--navigable { cursor: pointer; display: inline-block; &:hover { color: $color-secondary; } } @media screen and (max-width: $media-screen-l) { font-size: $font-size-s; } @media screen and (max-width: $media-screen-m) { margin: 0 $margin / 2; } } } ================================================ FILE: src/Web/wwwroot/js/site.js ================================================ // Write your Javascript code. ================================================ FILE: tests/FunctionalTests/FunctionalTests.csproj ================================================  Microsoft.eShopWeb.FunctionalTests false enable enable all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: tests/FunctionalTests/PublicApi/ApiTestFixture.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.PublicApi.AuthEndpoints; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Microsoft.eShopWeb.FunctionalTests.PublicApi; public class TestApiApplication : WebApplicationFactory { private readonly string _environment = "Testing"; protected override IHost CreateHost(IHostBuilder builder) { builder.UseEnvironment(_environment); // Add mock/test services to the builder here builder.ConfigureServices(services => { services.AddScoped(sp => { // Replace SQLite with in-memory database for tests return new DbContextOptionsBuilder() .UseInMemoryDatabase("DbForPublicApi") .UseApplicationServiceProvider(sp) .Options; }); services.AddScoped(sp => { // Replace SQLite with in-memory database for tests return new DbContextOptionsBuilder() .UseInMemoryDatabase("IdentityDbForPublicApi") .UseApplicationServiceProvider(sp) .Options; }); }); return base.CreateHost(builder); } } ================================================ FILE: tests/FunctionalTests/PublicApi/ApiTokenHelper.cs ================================================ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.IdentityModel.Tokens; namespace Microsoft.eShopWeb.FunctionalTests.Web.Api; public class ApiTokenHelper { public static string GetAdminUserToken() { string userName = "admin@microsoft.com"; string[] roles = { "Administrators" }; return CreateToken(userName, roles); } public static string GetNormalUserToken() { string userName = "demouser@microsoft.com"; string[] roles = { }; return CreateToken(userName, roles); } private static string CreateToken(string userName, string[] roles) { var claims = new List { new Claim(ClaimTypes.Name, userName) }; foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims.ToArray()), Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } } ================================================ FILE: tests/FunctionalTests/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs ================================================ //using System.Net.Http; //using System.Text; //using System.Text.Json; //using System.Threading.Tasks; //using Microsoft.eShopWeb.ApplicationCore.Constants; //using Microsoft.eShopWeb.FunctionalTests.PublicApi; //using Microsoft.eShopWeb.PublicApi.AuthEndpoints; //using Xunit; //namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; //[Collection("Sequential")] //public class AuthenticateEndpoint : IClassFixture //{ // JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; // public AuthenticateEndpoint(TestApiApplication factory) // { // Client = factory.CreateClient(); // } // public HttpClient Client { get; } // [Theory] // [InlineData("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] // [InlineData("demouser@microsoft.com", "badpassword", false)] // [InlineData("baduser@microsoft.com", "badpassword", false)] // public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) // { // var request = new AuthenticateRequest() // { // Username = testUsername, // Password = testPassword // }; // var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); // var response = await Client.PostAsync("api/authenticate", jsonContent); // response.EnsureSuccessStatusCode(); // var stringResponse = await response.Content.ReadAsStringAsync(); // var model = stringResponse.FromJson(); // Assert.Equal(expectedResult, model.Result); // } //} ================================================ FILE: tests/FunctionalTests/Web/Controllers/AccountControllerSignIn.cs ================================================ using System.Net; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; [Collection("Sequential")] public class AccountControllerSignIn : IClassFixture { public AccountControllerSignIn(TestApplication factory) { Client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } public HttpClient Client { get; } [Fact] public async Task ReturnsSignInScreenOnGet() { var response = await Client.GetAsync("/identity/account/login"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); Assert.Contains("demouser@microsoft.com", stringResponse); } [Fact] public void RegexMatchesValidRequestVerificationToken() { // TODO: Move to a unit test // TODO: Move regex to a constant in test project var input = @""; string regexpression = @"name=""__RequestVerificationToken"" type=""hidden"" value=""([-A-Za-z0-9+=/\\_]+?)"""; var regex = new Regex(regexpression); var match = regex.Match(input); var group = match.Groups.Values.LastOrDefault(); Assert.NotNull(group); Assert.True(group.Value.Length > 50); } [Fact] public async Task ReturnsFormWithRequestVerificationToken() { var response = await Client.GetAsync("/identity/account/login"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); string token = WebPageHelpers.GetRequestVerificationToken(stringResponse); Assert.True(token.Length > 50); } [Fact] public async Task ReturnsSuccessfulSignInOnPostWithValidCredentials() { var getResponse = await Client.GetAsync("/identity/account/login"); getResponse.EnsureSuccessStatusCode(); var stringResponse1 = await getResponse.Content.ReadAsStringAsync(); var keyValues = new List> { new KeyValuePair("Email", "demouser@microsoft.com"), new KeyValuePair("Password", "Pass@word1"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(stringResponse1)) }; var formContent = new FormUrlEncodedContent(keyValues); var postResponse = await Client.PostAsync("/identity/account/login", formContent); Assert.Equal(HttpStatusCode.Redirect, postResponse.StatusCode); Assert.Equal(new System.Uri("/", UriKind.Relative), postResponse.Headers.Location); } [Fact] public async Task UpdatePhoneNumberProfile() { //Login var getResponse = await Client.GetAsync("/identity/account/login"); getResponse.EnsureSuccessStatusCode(); var stringResponse1 = await getResponse.Content.ReadAsStringAsync(); var keyValues = new List> { new KeyValuePair("Email", "demouser@microsoft.com"), new KeyValuePair("Password", "Pass@word1"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(stringResponse1)) }; var formContent = new FormUrlEncodedContent(keyValues); await Client.PostAsync("/identity/account/login", formContent); //Profile page var profileResponse = await Client.GetAsync("/manage/my-account"); profileResponse.EnsureSuccessStatusCode(); var stringProfileResponse = await profileResponse.Content.ReadAsStringAsync(); //Update phone number var updateProfileValues = new List> { new KeyValuePair("Email", "demouser@microsoft.com"), new KeyValuePair("PhoneNumber", "03656565"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(stringProfileResponse)) }; var updateProfileContent = new FormUrlEncodedContent(updateProfileValues); var postProfileResponse = await Client.PostAsync("/manage/my-account", updateProfileContent); Assert.Equal(HttpStatusCode.Redirect, postProfileResponse.StatusCode); var profileResponse2 = await Client.GetAsync("/manage/my-account"); var stringProfileResponse2 = await profileResponse2.Content.ReadAsStringAsync(); Assert.Contains("03656565", stringProfileResponse2); } } ================================================ FILE: tests/FunctionalTests/Web/Controllers/CatalogControllerIndex.cs ================================================ using System.Net.Http; using System.Threading.Tasks; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; [Collection("Sequential")] public class CatalogControllerIndex : IClassFixture { public CatalogControllerIndex(TestApplication factory) { Client = factory.CreateClient(); } public HttpClient Client { get; } [Fact] public async Task ReturnsHomePageWithProductListing() { // Arrange & Act var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); // Assert Assert.Contains(".NET Bot Black Sweatshirt", stringResponse); } } ================================================ FILE: tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs ================================================ using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; [Collection("Sequential")] public class OrderIndexOnGet : IClassFixture { public OrderIndexOnGet(TestApplication factory) { Client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } public HttpClient Client { get; } [Fact] public async Task ReturnsRedirectGivenAnonymousUser() { var response = await Client.GetAsync("/order/my-orders"); var redirectLocation = response!.Headers.Location!.OriginalString; Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Contains("/Account/Login", redirectLocation); } } ================================================ FILE: tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs ================================================ using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.Web.Pages.Basket; [Collection("Sequential")] public class BasketPageCheckout : IClassFixture { public BasketPageCheckout(TestApplication factory) { Client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = true }); } public HttpClient Client { get; } [Fact] public async Task RedirectsToLoginIfNotAuthenticated() { // Load Home Page var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var stringResponse1 = await response.Content.ReadAsStringAsync(); string token = WebPageHelpers.GetRequestVerificationToken(stringResponse1); // Add Item to Cart var keyValues = new List> { new KeyValuePair("id", "2"), new KeyValuePair("name", "shirt"), new KeyValuePair("price", "19.49"), new KeyValuePair("__RequestVerificationToken", token) }; var formContent = new FormUrlEncodedContent(keyValues); var postResponse = await Client.PostAsync("/basket/index", formContent); postResponse.EnsureSuccessStatusCode(); var stringResponse = await postResponse.Content.ReadAsStringAsync(); Assert.Contains(".NET Black & White Mug", stringResponse); keyValues.Clear(); formContent = new FormUrlEncodedContent(keyValues); var postResponse2 = await Client.PostAsync("/Basket/Checkout", formContent); Assert.Contains("/Identity/Account/Login", postResponse2!.RequestMessage!.RequestUri!.ToString()!); } } ================================================ FILE: tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs ================================================ using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.Web.Pages.Basket; [Collection("Sequential")] public class CheckoutTest : IClassFixture { public CheckoutTest(TestApplication factory) { Client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = true }); } public HttpClient Client { get; } [Fact] public async Task SucessfullyPay() { // Load Home Page var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); // Add Item to Cart var keyValues = new List> { new KeyValuePair("id", "2"), new KeyValuePair("name", "shirt"), new KeyValuePair("price", "19.49"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(stringResponse)) }; var formContent = new FormUrlEncodedContent(keyValues); var postResponse = await Client.PostAsync("/basket/index", formContent); postResponse.EnsureSuccessStatusCode(); var stringPostResponse = await postResponse.Content.ReadAsStringAsync(); Assert.Contains(".NET Black & White Mug", stringPostResponse); //Load login page var loginResponse = await Client.GetAsync("/Identity/Account/Login"); var longinKeyValues = new List> { new KeyValuePair("email", "demouser@microsoft.com"), new KeyValuePair("password", "Pass@word1"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(await loginResponse.Content.ReadAsStringAsync())) }; var loginFormContent = new FormUrlEncodedContent(longinKeyValues); var loginPostResponse = await Client.PostAsync("/Identity/Account/Login?ReturnUrl=%2FBasket%2FCheckout", loginFormContent); var loginStringResponse = await loginPostResponse.Content.ReadAsStringAsync(); //Basket checkout (Pay now) var checkOutKeyValues = new List> { new KeyValuePair("Items[0].Id", "2"), new KeyValuePair("Items[0].Quantity", "1"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(loginStringResponse)) }; var checkOutContent = new FormUrlEncodedContent(checkOutKeyValues); var checkOutResponse = await Client.PostAsync("/basket/checkout", checkOutContent); var stringCheckOutResponse = await checkOutResponse.Content.ReadAsStringAsync(); Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage!.RequestUri!.ToString()); Assert.Contains("Thanks for your Order!", stringCheckOutResponse); } } ================================================ FILE: tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs ================================================ using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.Web.Pages.Basket; [Collection("Sequential")] public class IndexTest : IClassFixture { public IndexTest(TestApplication factory) { Client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = true }); } public HttpClient Client { get; } [Fact] public async Task OnPostUpdateTo50Successfully() { // Load Home Page var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var stringResponse1 = await response.Content.ReadAsStringAsync(); string token = WebPageHelpers.GetRequestVerificationToken(stringResponse1); // Add Item to Cart var keyValues = new List> { new KeyValuePair("id", "2"), new KeyValuePair("name", "shirt"), new KeyValuePair("__RequestVerificationToken", token) }; var formContent = new FormUrlEncodedContent(keyValues); var postResponse = await Client.PostAsync("/basket/index", formContent); postResponse.EnsureSuccessStatusCode(); var stringResponse = await postResponse.Content.ReadAsStringAsync(); Assert.Contains(".NET Black & White Mug", stringResponse); //Update var updateKeyValues = new List> { new KeyValuePair("Items[0].Id", WebPageHelpers.GetId(stringResponse)), new KeyValuePair("Items[0].Quantity", "49"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(stringResponse)) }; var updateContent = new FormUrlEncodedContent(updateKeyValues); var updateResponse = await Client.PostAsync("/basket/update", updateContent); var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync(); Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!); decimal expectedTotalAmount = 416.50M; Assert.Contains(expectedTotalAmount.ToString("N2"), stringUpdateResponse); } [Fact] public async Task OnPostUpdateTo0EmptyBasket() { // Load Home Page var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var stringResponse1 = await response.Content.ReadAsStringAsync(); string token = WebPageHelpers.GetRequestVerificationToken(stringResponse1); // Add Item to Cart var keyValues = new List> { new KeyValuePair("id", "2"), new KeyValuePair("name", "shirt"), new KeyValuePair("__RequestVerificationToken", token) }; var formContent = new FormUrlEncodedContent(keyValues); var postResponse = await Client.PostAsync("/basket/index", formContent); postResponse.EnsureSuccessStatusCode(); var stringResponse = await postResponse.Content.ReadAsStringAsync(); Assert.Contains(".NET Black & White Mug", stringResponse); //Update var updateKeyValues = new List> { new KeyValuePair("Items[0].Id", WebPageHelpers.GetId(stringResponse)), new KeyValuePair("Items[0].Quantity", "0"), new KeyValuePair(WebPageHelpers.TokenTag, WebPageHelpers.GetRequestVerificationToken(stringResponse)) }; var updateContent = new FormUrlEncodedContent(updateKeyValues); var updateResponse = await Client.PostAsync("/basket/update", updateContent); var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync(); Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!); Assert.Contains("Basket is empty", stringUpdateResponse); } } ================================================ FILE: tests/FunctionalTests/Web/Pages/HomePageOnGet.cs ================================================ using System.Net.Http; using System.Threading.Tasks; using Microsoft.eShopWeb.FunctionalTests.Web; using Xunit; namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages; [Collection("Sequential")] public class HomePageOnGet : IClassFixture { public HomePageOnGet(TestApplication factory) { Client = factory.CreateClient(); } public HttpClient Client { get; } [Fact] public async Task ReturnsHomePageWithProductListing() { // Arrange & Act var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); // Assert Assert.Contains(".NET Bot Black Sweatshirt", stringResponse); } } ================================================ FILE: tests/FunctionalTests/Web/WebPageHelpers.cs ================================================ using System.Text.RegularExpressions; namespace Microsoft.eShopWeb.FunctionalTests.Web; public static class WebPageHelpers { public static string TokenTag = "__RequestVerificationToken"; public static string GetRequestVerificationToken(string input) { string regexpression = @"name=""__RequestVerificationToken"" type=""hidden"" value=""([-A-Za-z0-9+=/\\_]+?)"""; return RegexSearch(regexpression, input); } public static string GetId(string input) { string regexpression = @"name=""Items\[0\].Id"" value=""(\d)"""; return RegexSearch(regexpression, input); } private static string RegexSearch(string regexpression, string input) { var regex = new Regex(regexpression); var match = regex.Match(input); return match!.Groups!.Values!.LastOrDefault()!.Value; } } ================================================ FILE: tests/FunctionalTests/Web/WebTestFixture.cs ================================================ using System; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.FunctionalTests.Web; public class TestApplication : WebApplicationFactory { private readonly string _environment = "Development"; protected override IHost CreateHost(IHostBuilder builder) { builder.UseEnvironment(_environment); // Add mock/test services to the builder here builder.ConfigureServices(services => { var descriptors = services.Where(d => d.ServiceType == typeof(DbContextOptions) || d.ServiceType == typeof(DbContextOptions)) .ToList(); foreach (var descriptor in descriptors) { services.Remove(descriptor); } services.AddScoped(sp => { // Replace SQLite with in-memory database for tests return new DbContextOptionsBuilder() .UseInMemoryDatabase("InMemoryDbForTesting") .UseApplicationServiceProvider(sp) .Options; }); services.AddScoped(sp => { // Replace SQLite with in-memory database for tests return new DbContextOptionsBuilder() .UseInMemoryDatabase("Identity") .UseApplicationServiceProvider(sp) .Options; }); }); return base.CreateHost(builder); } } ================================================ FILE: tests/IntegrationTests/IntegrationTests.csproj ================================================  Microsoft.eShopWeb.IntegrationTests false all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers ================================================ FILE: tests/IntegrationTests/Repositories/BasketRepositoryTests/SetQuantities.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.UnitTests.Builders; using Xunit; namespace Microsoft.eShopWeb.IntegrationTests.Repositories.BasketRepositoryTests; public class SetQuantities { private readonly CatalogContext _catalogContext; private readonly EfRepository _basketRepository; private readonly BasketBuilder BasketBuilder = new BasketBuilder(); public SetQuantities() { var dbOptions = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: "TestCatalog") .Options; _catalogContext = new CatalogContext(dbOptions); _basketRepository = new EfRepository(_catalogContext); } [Fact] public async Task RemoveEmptyQuantities() { var basket = BasketBuilder.WithOneBasketItem(); var basketService = new BasketService(_basketRepository, null); await _basketRepository.AddAsync(basket); _catalogContext.SaveChanges(); await basketService.SetQuantities(BasketBuilder.BasketId, new Dictionary() { { BasketBuilder.BasketId.ToString(), 0 } }); Assert.Empty(basket.Items); } } ================================================ FILE: tests/IntegrationTests/Repositories/OrderRepositoryTests/GetById.cs ================================================ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.UnitTests.Builders; using Xunit; using Xunit.Abstractions; namespace Microsoft.eShopWeb.IntegrationTests.Repositories.OrderRepositoryTests; public class GetById { private readonly CatalogContext _catalogContext; private readonly EfRepository _orderRepository; private OrderBuilder OrderBuilder { get; } = new OrderBuilder(); private readonly ITestOutputHelper _output; public GetById(ITestOutputHelper output) { _output = output; var dbOptions = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: "TestCatalog") .Options; _catalogContext = new CatalogContext(dbOptions); _orderRepository = new EfRepository(_catalogContext); } [Fact] public async Task GetsExistingOrder() { var existingOrder = OrderBuilder.WithDefaultValues(); _catalogContext.Orders.Add(existingOrder); _catalogContext.SaveChanges(); int orderId = existingOrder.Id; _output.WriteLine($"OrderId: {orderId}"); var orderFromRepo = await _orderRepository.GetByIdAsync(orderId); Assert.Equal(OrderBuilder.TestBuyerId, orderFromRepo.BuyerId); // Note: Using InMemoryDatabase OrderItems is available. Will be null if using SQL DB. // Use the OrderWithItemsByIdSpec instead of just GetById to get the full aggregate var firstItem = orderFromRepo.OrderItems.FirstOrDefault(); Assert.Equal(OrderBuilder.TestUnits, firstItem.Units); } } ================================================ FILE: tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.UnitTests.Builders; using Xunit; namespace Microsoft.eShopWeb.IntegrationTests.Repositories.OrderRepositoryTests; public class GetByIdWithItemsAsync { private readonly CatalogContext _catalogContext; private readonly EfRepository _orderRepository; private OrderBuilder OrderBuilder { get; } = new OrderBuilder(); public GetByIdWithItemsAsync() { var dbOptions = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: "TestCatalog") .Options; _catalogContext = new CatalogContext(dbOptions); _orderRepository = new EfRepository(_catalogContext); } [Fact] public async Task GetOrderAndItemsByOrderIdWhenMultipleOrdersPresent() { //Arrange var itemOneUnitPrice = 5.50m; var itemOneUnits = 2; var itemTwoUnitPrice = 7.50m; var itemTwoUnits = 5; var firstOrder = OrderBuilder.WithDefaultValues(); _catalogContext.Orders.Add(firstOrder); int firstOrderId = firstOrder.Id; var secondOrderItems = new List(); secondOrderItems.Add(new OrderItem(OrderBuilder.TestCatalogItemOrdered, itemOneUnitPrice, itemOneUnits)); secondOrderItems.Add(new OrderItem(OrderBuilder.TestCatalogItemOrdered, itemTwoUnitPrice, itemTwoUnits)); var secondOrder = OrderBuilder.WithItems(secondOrderItems); _catalogContext.Orders.Add(secondOrder); int secondOrderId = secondOrder.Id; _catalogContext.SaveChanges(); //Act var spec = new OrderWithItemsByIdSpec(secondOrderId); var orderFromRepo = await _orderRepository.FirstOrDefaultAsync(spec); //Assert Assert.Equal(secondOrderId, orderFromRepo.Id); Assert.Equal(secondOrder.OrderItems.Count, orderFromRepo.OrderItems.Count); Assert.Equal(1, orderFromRepo.OrderItems.Count(x => x.UnitPrice == itemOneUnitPrice)); Assert.Equal(1, orderFromRepo.OrderItems.Count(x => x.UnitPrice == itemTwoUnitPrice)); Assert.Equal(itemOneUnits, orderFromRepo.OrderItems.SingleOrDefault(x => x.UnitPrice == itemOneUnitPrice).Units); Assert.Equal(itemTwoUnits, orderFromRepo.OrderItems.SingleOrDefault(x => x.UnitPrice == itemTwoUnitPrice).Units); } } ================================================ FILE: tests/PublicApiIntegrationTests/ApiTokenHelper.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace PublicApiIntegrationTests { public class ApiTokenHelper { public static string GetAdminUserToken() { string userName = "admin@microsoft.com"; string[] roles = { "Administrators" }; return CreateToken(userName, roles); } public static string GetNormalUserToken() { string userName = "demouser@microsoft.com"; string[] roles = { }; return CreateToken(userName, roles); } private static string CreateToken(string userName, string[] roles) { var claims = new List { new Claim(ClaimTypes.Name, userName) }; foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims.ToArray()), Expires = DateTime.UtcNow.AddHours(1), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } } } ================================================ FILE: tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs ================================================ using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.PublicApi.AuthEndpoints; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace PublicApiIntegrationTests.AuthEndpoints; [TestClass] public class AuthenticateEndpoint { [TestMethod] [DataRow("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] [DataRow("demouser@microsoft.com", "badpassword", false)] [DataRow("baduser@microsoft.com", "badpassword", false)] public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) { var request = new AuthenticateRequest() { Username = testUsername, Password = testPassword }; var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); var response = await ProgramTest.NewClient.PostAsync("api/authenticate", jsonContent); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); var model = stringResponse.FromJson(); Assert.AreEqual(expectedResult, model!.Result); } } ================================================ FILE: tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs ================================================ using Microsoft.eShopWeb; using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Threading.Tasks; namespace PublicApiIntegrationTests.CatalogItemEndpoints; [TestClass] public class CatalogItemGetByIdEndpointTest { [TestMethod] public async Task ReturnsItemGivenValidId() { var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); var model = stringResponse.FromJson(); Assert.AreEqual(5, model!.CatalogItem.Id); Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name); } [TestMethod] public async Task ReturnsNotFoundGivenInvalidId() { var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0"); Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } } ================================================ FILE: tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs ================================================ using Microsoft.eShopWeb; using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net; using System.Threading.Tasks; namespace PublicApiIntegrationTests.CatalogItemEndpoints; [TestClass] public class CatalogItemListPagedEndpoint { [TestMethod] public async Task ReturnsFirst10CatalogItems() { var client = ProgramTest.NewClient; var response = await client.GetAsync("/api/catalog-items?pageSize=10"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); var model = stringResponse.FromJson(); Assert.AreEqual(10, model!.CatalogItems.Count()); } [TestMethod] public async Task ReturnsCorrectCatalogItemsGivenPageIndex1() { var pageSize = 10; var pageIndex = 1; var client = ProgramTest.NewClient; var response = await client.GetAsync($"/api/catalog-items"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); var model = stringResponse.FromJson(); var totalItem = model!.CatalogItems.Count(); var response2 = await client.GetAsync($"/api/catalog-items?pageSize={pageSize}&pageIndex={pageIndex}"); response.EnsureSuccessStatusCode(); var stringResponse2 = await response2.Content.ReadAsStringAsync(); var model2 = stringResponse2.FromJson(); var totalExpected = totalItem - (pageSize * pageIndex); Assert.AreEqual(totalExpected, model2!.CatalogItems.Count()); } [DataTestMethod] [DataRow("catalog-items")] [DataRow("catalog-brands")] [DataRow("catalog-types")] [DataRow("catalog-items/1")] public async Task SuccessFullMutipleParallelCall(string endpointName) { var client = ProgramTest.NewClient; var tasks = new List>(); for (int i = 0; i < 100; i++) { var task = client.GetAsync($"/api/{endpointName}"); tasks.Add(task); } await Task.WhenAll(tasks.ToList()); var totalKO = tasks.Count(t => t.Result.StatusCode != HttpStatusCode.OK); Assert.AreEqual(0, totalKO); } } ================================================ FILE: tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs ================================================ using BlazorShared.Models; using Microsoft.eShopWeb; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace PublicApiIntegrationTests.AuthEndpoints; [TestClass] public class CreateCatalogItemEndpointTest { private int _testBrandId = 1; private int _testTypeId = 2; private string _testDescription = "test description"; private string _testName = "test name"; private decimal _testPrice = 1.23m; [TestMethod] public async Task ReturnsNotAuthorizedGivenNormalUserToken() { var jsonContent = GetValidNewItemJson(); var token = ApiTokenHelper.GetNormalUserToken(); var client = ProgramTest.NewClient; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await client.PostAsync("api/catalog-items", jsonContent); Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); } [TestMethod] public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken() { var jsonContent = GetValidNewItemJson(); var adminToken = ApiTokenHelper.GetAdminUserToken(); var client = ProgramTest.NewClient; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var response = await client.PostAsync("api/catalog-items", jsonContent); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); var model = stringResponse.FromJson(); Assert.AreEqual(_testBrandId, model!.CatalogItem.CatalogBrandId); Assert.AreEqual(_testTypeId, model.CatalogItem.CatalogTypeId); Assert.AreEqual(_testDescription, model.CatalogItem.Description); Assert.AreEqual(_testName, model.CatalogItem.Name); Assert.AreEqual(_testPrice, model.CatalogItem.Price); } private StringContent GetValidNewItemJson() { var request = new CreateCatalogItemRequest() { CatalogBrandId = _testBrandId, CatalogTypeId = _testTypeId, Description = _testDescription, Name = _testName, Price = _testPrice }; var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); return jsonContent; } } ================================================ FILE: tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs ================================================ using BlazorShared.Models; using Microsoft.eShopWeb; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Net.Http.Headers; using System.Threading.Tasks; namespace PublicApiIntegrationTests.CatalogItemEndpoints; [TestClass] public class DeleteCatalogItemEndpointTest { [TestMethod] public async Task ReturnsSuccessGivenValidIdAndAdminUserToken() { var adminToken = ApiTokenHelper.GetAdminUserToken(); var client = ProgramTest.NewClient; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var response = await client.DeleteAsync("api/catalog-items/12"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); var model = stringResponse.FromJson(); Assert.AreEqual("Deleted", model!.Status); } [TestMethod] public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken() { var adminToken = ApiTokenHelper.GetAdminUserToken(); var client = ProgramTest.NewClient; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var response = await client.DeleteAsync("api/catalog-items/0"); Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } } ================================================ FILE: tests/PublicApiIntegrationTests/ProgramTest.cs ================================================ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net.Http; namespace PublicApiIntegrationTests; [TestClass] public class ProgramTest { private static WebApplicationFactory _application = new(); public static HttpClient NewClient { get { return _application.CreateClient(); } } [AssemblyInitialize] public static void AssemblyInitialize(TestContext _) { _application = new WebApplicationFactory(); } } ================================================ FILE: tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj ================================================ enable false Always true PreserveNewest all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: tests/PublicApiIntegrationTests/appsettings.test.json ================================================ { "UseOnlyInMemoryDatabase": true } ================================================ FILE: tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketAddItem.cs ================================================ using System; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests; public class BasketAddItem { private readonly int _testCatalogItemId = 123; private readonly decimal _testUnitPrice = 1.23m; private readonly int _testQuantity = 2; private readonly string _buyerId = "Test buyerId"; [Fact] public void AddsBasketItemIfNotPresent() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); var firstItem = basket.Items.Single(); Assert.Equal(_testCatalogItemId, firstItem.CatalogItemId); Assert.Equal(_testUnitPrice, firstItem.UnitPrice); Assert.Equal(_testQuantity, firstItem.Quantity); } [Fact] public void IncrementsQuantityOfItemIfPresent() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); var firstItem = basket.Items.Single(); Assert.Equal(_testQuantity * 2, firstItem.Quantity); } [Fact] public void KeepsOriginalUnitPriceIfMoreItemsAdded() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); basket.AddItem(_testCatalogItemId, _testUnitPrice * 2, _testQuantity); var firstItem = basket.Items.Single(); Assert.Equal(_testUnitPrice, firstItem.UnitPrice); } [Fact] public void DefaultsToQuantityOfOne() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice); var firstItem = basket.Items.Single(); Assert.Equal(1, firstItem.Quantity); } [Fact] public void CantAddItemWithNegativeQuantity() { var basket = new Basket(_buyerId); Assert.Throws(() => basket.AddItem(_testCatalogItemId, _testUnitPrice, -1)); } [Fact] public void CantModifyQuantityToNegativeNumber() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice); Assert.Throws(() => basket.AddItem(_testCatalogItemId, _testUnitPrice, -2)); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketRemoveEmptyItems.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests; public class BasketRemoveEmptyItems { private readonly int _testCatalogItemId = 123; private readonly decimal _testUnitPrice = 1.23m; private readonly string _buyerId = "Test buyerId"; [Fact] public void RemovesEmptyBasketItems() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice, 0); basket.RemoveEmptyItems(); Assert.Empty(basket.Items); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketTotalItems.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests; public class BasketTotalItems { private readonly int _testCatalogItemId = 123; private readonly decimal _testUnitPrice = 1.23m; private readonly int _testQuantity = 2; private readonly string _buyerId = "Test buyerId"; [Fact] public void ReturnsTotalQuantityWithOneItem() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); var result = basket.TotalItems; Assert.Equal(_testQuantity, result); } [Fact] public void ReturnsTotalQuantityWithMultipleItems() { var basket = new Basket(_buyerId); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity*2); var result = basket.TotalItems; Assert.Equal(_testQuantity*3, result); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Entities/OrderTests/OrderTotal.cs ================================================ using System.Collections.Generic; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.UnitTests.Builders; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.OrderTests; public class OrderTotal { private decimal _testUnitPrice = 42m; [Fact] public void IsZeroForNewOrder() { var order = new OrderBuilder().WithNoItems(); Assert.Equal(0, order.Total()); } [Fact] public void IsCorrectGiven1Item() { var builder = new OrderBuilder(); var items = new List { new OrderItem(builder.TestCatalogItemOrdered, _testUnitPrice, 1) }; var order = new OrderBuilder().WithItems(items); Assert.Equal(_testUnitPrice, order.Total()); } [Fact] public void IsCorrectGiven3Items() { var builder = new OrderBuilder(); var order = builder.WithDefaultValues(); Assert.Equal(builder.TestUnitPrice * builder.TestUnits, order.Total()); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Extensions/JsonExtensions.cs ================================================ using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; public class JsonExtensions { [Fact] public void CorrectlySerializesAndDeserializesObject() { var testParent = new TestParent { Id = 7, Name = "Test name", Children = new[] { new TestChild(), new TestChild(), new TestChild() } }; var json = testParent.ToJson(); var result = json.FromJson(); Assert.Equal(testParent, result); } [ Theory, InlineData("{ \"id\": 9, \"name\": \"Another test\" }", 9, "Another test"), InlineData("{ \"id\": 3124, \"name\": \"Test Value 1\" }", 3124, "Test Value 1"), ] public void CorrectlyDeserializesJson(string json, int expectedId, string expectedName) => Assert.Equal(new TestParent { Id = expectedId, Name = expectedName }, json.FromJson()); } ================================================ FILE: tests/UnitTests/ApplicationCore/Extensions/TestChild.cs ================================================ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; [DebuggerDisplay("Id={Id}, Date={Date}")] public class TestChild : IEquatable { public Guid Id { get; set; } = Guid.NewGuid(); public DateTime Date { get; set; } = DateTime.UtcNow; public bool Equals([AllowNull] TestChild other) => other?.Date == Date && other?.Id == Id; } ================================================ FILE: tests/UnitTests/ApplicationCore/Extensions/TestParent.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; public class TestParent : IEquatable { public int Id { get; set; } public string? Name { get; set; } public IEnumerable? Children { get; set; } public bool Equals([AllowNull] TestParent other) { if (other?.Id == Id && other?.Name == Name) { if (Children is null) { return other?.Children is null; } return other?.Children?.Zip(Children).All(t => t.First?.Equals(t.Second) ?? false) ?? false; } return false; } } ================================================ FILE: tests/UnitTests/ApplicationCore/Services/BasketServiceTests/AddItemToBasket.cs ================================================ using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.ApplicationCore.Specifications; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; public class AddItemToBasket { private readonly string _buyerId = "Test buyerId"; private readonly IRepository _mockBasketRepo = Substitute.For>(); private readonly IAppLogger _mockLogger = Substitute.For>(); [Fact] public async Task InvokesBasketRepositoryGetBySpecAsyncOnce() { var basket = new Basket(_buyerId); basket.AddItem(1, 1.5m); _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(basket); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m); await _mockBasketRepo.Received().FirstOrDefaultAsync(Arg.Any(), default); } [Fact] public async Task InvokesBasketRepositoryUpdateAsyncOnce() { var basket = new Basket(_buyerId); basket.AddItem(1, 1.1m, 1); _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(basket); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m); await _mockBasketRepo.Received().UpdateAsync(basket, default); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs ================================================ using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; //using Moq; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; public class DeleteBasket { private readonly string _buyerId = "Test buyerId"; private readonly IRepository _mockBasketRepo = Substitute.For>(); private readonly IAppLogger _mockLogger = Substitute.For>(); [Fact] public async Task ShouldInvokeBasketRepositoryDeleteAsyncOnce() { var basket = new Basket(_buyerId); basket.AddItem(1, 1.1m, 1); basket.AddItem(2, 1.1m, 1); _mockBasketRepo.GetByIdAsync(Arg.Any(), default) .Returns(basket); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.DeleteBasketAsync(1); await _mockBasketRepo.Received().DeleteAsync(Arg.Any(), default); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs ================================================ using System; using System.Threading.Tasks; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.ApplicationCore.Specifications; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; public class TransferBasket { private readonly string _nonexistentAnonymousBasketBuyerId = "nonexistent-anonymous-basket-buyer-id"; private readonly string _existentAnonymousBasketBuyerId = "existent-anonymous-basket-buyer-id"; private readonly string _nonexistentUserBasketBuyerId = "newuser@microsoft.com"; private readonly string _existentUserBasketBuyerId = "testuser@microsoft.com"; private readonly IRepository _mockBasketRepo = Substitute.For>(); private readonly IAppLogger _mockLogger = Substitute.For>(); public class Results { private readonly Queue> values = new Queue>(); public Results(T result) { values.Enqueue(() => result); } public Results Then(T value) { return Then(() => value); } public Results Then(Func value) { values.Enqueue(value); return this; } public T Next() { return values.Dequeue()(); } } [Fact] public async Task InvokesBasketRepositoryFirstOrDefaultAsyncOnceIfAnonymousBasketNotExists() { var anonymousBasket = null as Basket; var userBasket = new Basket(_existentUserBasketBuyerId); var results = new Results(anonymousBasket) .Then(userBasket); _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(x => results.Next()); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); await _mockBasketRepo.Received().FirstOrDefaultAsync(Arg.Any(), default); } [Fact] public async Task TransferAnonymousBasketItemsWhilePreservingExistingUserBasketItems() { var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); anonymousBasket.AddItem(1, 10, 1); anonymousBasket.AddItem(3, 55, 7); var userBasket = new Basket(_existentUserBasketBuyerId); userBasket.AddItem(1, 10, 4); userBasket.AddItem(2, 99, 3); var results = new Results(anonymousBasket) .Then(userBasket); _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(x => results.Next()); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); await _mockBasketRepo.Received().UpdateAsync(userBasket, default); Assert.Equal(3, userBasket.Items.Count); Assert.Contains(userBasket.Items, x => x.CatalogItemId == 1 && x.UnitPrice == 10 && x.Quantity == 5); Assert.Contains(userBasket.Items, x => x.CatalogItemId == 2 && x.UnitPrice == 99 && x.Quantity == 3); Assert.Contains(userBasket.Items, x => x.CatalogItemId == 3 && x.UnitPrice == 55 && x.Quantity == 7); } [Fact] public async Task RemovesAnonymousBasketAfterUpdatingUserBasket() { var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); var userBasket = new Basket(_existentUserBasketBuyerId); var results = new Results(anonymousBasket) .Then(userBasket); _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(x => results.Next()); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); await _mockBasketRepo.Received().UpdateAsync(userBasket, default); await _mockBasketRepo.Received().DeleteAsync(anonymousBasket, default); } [Fact] public async Task CreatesNewUserBasketIfNotExists() { var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); var userBasket = null as Basket; var results = new Results(anonymousBasket) .Then(userBasket); _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(x => results.Next()); var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.TransferBasketAsync(_existentAnonymousBasketBuyerId, _nonexistentUserBasketBuyerId); await _mockBasketRepo.Received().AddAsync(Arg.Is(x => x.BuyerId == _nonexistentUserBasketBuyerId), default); } } ================================================ FILE: tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs ================================================ using System.Collections.Generic; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Specifications; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; public class BasketWithItems { private readonly int _testBasketId = 123; private readonly string _buyerId = "Test buyerId"; [Fact] public void MatchesBasketWithGivenBasketId() { var spec = new BasketWithItemsSpecification(_testBasketId); var result = spec.Evaluate(GetTestBasketCollection()).FirstOrDefault(); Assert.NotNull(result); Assert.Equal(_testBasketId, result.Id); } [Fact] public void MatchesNoBasketsIfBasketIdNotPresent() { int badBasketId = -1; var spec = new BasketWithItemsSpecification(badBasketId); var result = spec.Evaluate(GetTestBasketCollection()).Any(); Assert.False(result); } [Fact] public void MatchesBasketWithGivenBuyerId() { var spec = new BasketWithItemsSpecification(_buyerId); var result = spec.Evaluate(GetTestBasketCollection()).FirstOrDefault(); Assert.NotNull(result); Assert.Equal(_buyerId, result.BuyerId); } [Fact] public void MatchesNoBasketsIfBuyerIdNotPresent() { string badBuyerId = "badBuyerId"; var spec = new BasketWithItemsSpecification(badBuyerId); var result = spec.Evaluate(GetTestBasketCollection()).Any(); Assert.False(result); } public List GetTestBasketCollection() { var basket1Mock = Substitute.For(_buyerId); basket1Mock.Id.Returns(1); var basket2Mock = Substitute.For(_buyerId); basket2Mock.Id.Returns(2); var basket3Mock = Substitute.For(_buyerId); basket3Mock.Id.Returns(_testBasketId); return new List() { basket1Mock, basket2Mock, basket3Mock }; } } ================================================ FILE: tests/UnitTests/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; public class CatalogFilterPaginatedSpecification { [Fact] public void ReturnsAllCatalogItems() { var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterPaginatedSpecification(0, 10, null, null); var result = spec.Evaluate(GetTestCollection()); Assert.NotNull(result); Assert.Equal(4, result.ToList().Count); } [Fact] public void Returns2CatalogItemsWithSameBrandAndTypeId() { var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterPaginatedSpecification(0, 10, 1, 1); var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Equal(2, result.ToList().Count); } private List GetTestCollection() { var catalogItemList = new List(); catalogItemList.Add(new CatalogItem(1, 1, "Item 1", "Item 1", 1.00m, "TestUri1")); catalogItemList.Add(new CatalogItem(1, 1, "Item 1.5", "Item 1.5", 1.50m, "TestUri1")); catalogItemList.Add(new CatalogItem(2, 2, "Item 2", "Item 2", 2.00m, "TestUri2")); catalogItemList.Add(new CatalogItem(3, 3, "Item 3", "Item 3", 3.00m, "TestUri3")); return catalogItemList; } } ================================================ FILE: tests/UnitTests/ApplicationCore/Specifications/CatalogFilterSpecification.cs ================================================ using System.Collections.Generic; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; public class CatalogFilterSpecification { [Theory] [InlineData(null, null, 5)] [InlineData(1, null, 3)] [InlineData(2, null, 2)] [InlineData(null, 1, 2)] [InlineData(null, 3, 1)] [InlineData(1, 3, 1)] [InlineData(2, 3, 0)] public void MatchesExpectedNumberOfItems(int? brandId, int? typeId, int expectedCount) { var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterSpecification(brandId, typeId); var result = spec.Evaluate(GetTestItemCollection()).ToList(); Assert.Equal(expectedCount, result.Count()); } public List GetTestItemCollection() { return new List() { new CatalogItem(1, 1, "Description", "Name", 0, "FakePath"), new CatalogItem(2, 1, "Description", "Name", 0, "FakePath"), new CatalogItem(3, 1, "Description", "Name", 0, "FakePath"), new CatalogItem(1, 2, "Description", "Name", 0, "FakePath"), new CatalogItem(2, 2, "Description", "Name", 0, "FakePath"), }; } } ================================================ FILE: tests/UnitTests/ApplicationCore/Specifications/CatalogItemsSpecification.cs ================================================ using System.Collections.Generic; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; public class CatalogItemsSpecification { [Fact] public void MatchesSpecificCatalogItem() { var catalogItemIds = new int[] { 1 }; var spec = new eShopWeb.ApplicationCore.Specifications.CatalogItemsSpecification(catalogItemIds); var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Single(result.ToList()); } [Fact] public void MatchesAllCatalogItems() { var catalogItemIds = new int[] { 1, 3 }; var spec = new eShopWeb.ApplicationCore.Specifications.CatalogItemsSpecification(catalogItemIds); var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Equal(2, result.ToList().Count); } private List GetTestCollection() { var catalogItems = new List(); var mockCatalogItem1 = Substitute.For(1, 1, "Item 1 description", "Item 1", 1.5m, "Item1Uri"); mockCatalogItem1.Id.Returns(1); var mockCatalogItem3 = Substitute.For(3, 3, "Item 3 description", "Item 3", 3.5m, "Item3Uri"); mockCatalogItem3.Id.Returns(3); catalogItems.Add(mockCatalogItem1); catalogItems.Add(mockCatalogItem3); return catalogItems; } } ================================================ FILE: tests/UnitTests/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs ================================================ using System.Collections.Generic; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; public class CustomerOrdersWithItemsSpecification { private readonly string _buyerId = "TestBuyerId"; private Address _shipToAddress = new Address("Street", "City", "OH", "US", "11111"); [Fact] public void ReturnsOrderWithOrderedItem() { var spec = new eShopWeb.ApplicationCore.Specifications.CustomerOrdersWithItemsSpecification(_buyerId); var result = spec.Evaluate(GetTestCollection()).FirstOrDefault(); Assert.NotNull(result); Assert.NotNull(result.OrderItems); Assert.Single(result.OrderItems); Assert.NotNull(result.OrderItems.FirstOrDefault()?.ItemOrdered); } [Fact] public void ReturnsAllOrderWithAllOrderedItem() { var spec = new eShopWeb.ApplicationCore.Specifications.CustomerOrdersWithItemsSpecification(_buyerId); var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Equal(2, result.Count); Assert.Single(result[0].OrderItems); Assert.NotNull(result[0].OrderItems.FirstOrDefault()?.ItemOrdered); Assert.Equal(2, result[1].OrderItems.Count); Assert.NotNull(result[1].OrderItems.ToList()[0].ItemOrdered); Assert.NotNull(result[1].OrderItems.ToList()[1].ItemOrdered); } public List GetTestCollection() { var ordersList = new List(); ordersList.Add(new Order(_buyerId, _shipToAddress, new List { new OrderItem(new CatalogItemOrdered(1, "Product1", "testurl"), 10.50m, 1) })); ordersList.Add(new Order(_buyerId, _shipToAddress, new List { new OrderItem(new CatalogItemOrdered(2, "Product2", "testurl"), 15.50m, 2), new OrderItem(new CatalogItemOrdered(2, "Product3", "testurl"), 20.50m, 1) })); return ordersList; } } ================================================ FILE: tests/UnitTests/Builders/AddressBuilder.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.UnitTests.Builders; public class AddressBuilder { private Address _address; public string TestStreet => "123 Main St."; public string TestCity => "Kent"; public string TestState => "OH"; public string TestCountry => "USA"; public string TestZipCode => "44240"; public AddressBuilder() { _address = WithDefaultValues(); } public Address Build() { return _address; } public Address WithDefaultValues() { _address = new Address(TestStreet, TestCity, TestState, TestCountry, TestZipCode); return _address; } } ================================================ FILE: tests/UnitTests/Builders/BasketBuilder.cs ================================================ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using NSubstitute; namespace Microsoft.eShopWeb.UnitTests.Builders; public class BasketBuilder { private Basket _basket; public string BasketBuyerId => "testbuyerId@test.com"; public int BasketId => 1; public BasketBuilder() { _basket = WithNoItems(); } public Basket Build() { return _basket; } public Basket WithNoItems() { var basketMock = Substitute.For(BasketBuyerId); basketMock.Id.Returns(BasketId); _basket = basketMock; return _basket; } public Basket WithOneBasketItem() { var basketMock = Substitute.For(BasketBuyerId); _basket = basketMock; _basket.AddItem(2, 3.40m, 4); return _basket; } } ================================================ FILE: tests/UnitTests/Builders/OrderBuilder.cs ================================================ using System.Collections.Generic; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.UnitTests.Builders; public class OrderBuilder { private Order _order; public string TestBuyerId => "12345"; public int TestCatalogItemId => 234; public string TestProductName => "Test Product Name"; public string TestPictureUri => "http://test.com/image.jpg"; public decimal TestUnitPrice = 1.23m; public int TestUnits = 3; public CatalogItemOrdered TestCatalogItemOrdered { get; } public OrderBuilder() { TestCatalogItemOrdered = new CatalogItemOrdered(TestCatalogItemId, TestProductName, TestPictureUri); _order = WithDefaultValues(); } public Order Build() { return _order; } public Order WithDefaultValues() { var orderItem = new OrderItem(TestCatalogItemOrdered, TestUnitPrice, TestUnits); var itemList = new List() { orderItem }; _order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), itemList); return _order; } public Order WithNoItems() { _order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), new List()); return _order; } public Order WithItems(List items) { _order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), items); return _order; } } ================================================ FILE: tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Web.Features.MyOrders; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests; public class GetMyOrders { private readonly IReadRepository _mockOrderRepository = Substitute.For>(); public GetMyOrders() { var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); var address = new Address("", "", "", "", ""); Order order = new Order("buyerId", address, new List { item }); _mockOrderRepository.ListAsync(Arg.Any>(), default).Returns(new List { order }); } [Fact] public async Task NotReturnNullIfOrdersArePresIent() { var request = new eShopWeb.Web.Features.MyOrders.GetMyOrders("SomeUserName"); var handler = new GetMyOrdersHandler(_mockOrderRepository); var result = await handler.Handle(request, CancellationToken.None); Assert.NotNull(result); } } ================================================ FILE: tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Ardalis.Specification; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.Features.OrderDetails; using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests; public class GetOrderDetails { private readonly IReadRepository _mockOrderRepository = Substitute.For>(); public GetOrderDetails() { var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); var address = new Address("", "", "", "", ""); Order order = new Order("buyerId", address, new List { item }); _mockOrderRepository.FirstOrDefaultAsync(Arg.Any(), default) .Returns(order); } [Fact] public async Task NotBeNullIfOrderExists() { var request = new eShopWeb.Web.Features.OrderDetails.GetOrderDetails("SomeUserName", 0); var handler = new GetOrderDetailsHandler(_mockOrderRepository); var result = await handler.Handle(request, CancellationToken.None); Assert.NotNull(result); } } ================================================ FILE: tests/UnitTests/UnitTests.csproj ================================================  enable Microsoft.eShopWeb.UnitTests false latest enable all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateBrandsCacheKey.cs ================================================ using Microsoft.eShopWeb.Web.Extensions; using Xunit; namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests; public class GenerateBrandsCacheKey { [Fact] public void ReturnsBrandsCacheKey() { var result = CacheHelpers.GenerateBrandsCacheKey(); Assert.Equal("brands", result); } } ================================================ FILE: tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateCatalogItemCacheKey.cs ================================================ using Microsoft.eShopWeb.Web; using Microsoft.eShopWeb.Web.Extensions; using Xunit; namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests; public class GenerateCatalogItemCacheKey { [Fact] public void ReturnsCatalogItemCacheKey() { var pageIndex = 0; int? brandId = null; int? typeId = null; var result = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId); Assert.Equal("items-0-10--", result); } } ================================================ FILE: tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateTypesCacheKey.cs ================================================ using Microsoft.eShopWeb.Web.Extensions; using Xunit; namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests; public class GenerateTypesCacheKey { [Fact] public void ReturnsTypesCacheKey() { var result = CacheHelpers.GenerateTypesCacheKey(); Assert.Equal("types", result); } }