From c656d9b46595b02020ee60c5ecfc28d46dd69938 Mon Sep 17 00:00:00 2001 From: Luiz Date: Mon, 29 Jan 2024 15:37:40 -0600 Subject: [PATCH] Updating dotnet to 8.0 and sync project --- .devcontainer/Dockerfile | 81 ---------- .devcontainer/devcontainer.json | 66 ++++---- .editorconfig | 8 +- .gitattributes | 3 + .github/dependabot.yml | 6 + .github/workflows/dotnetcore.yml | 23 +++ .github/workflows/eshoponweb-cicd.yml | 2 +- .github/workflows/richnav.yml | 24 +++ .gitignore | 3 + .vscode/extensions.json | 3 +- .vscode/launch.json | 2 +- Directory.Packages.props | 72 +++++++++ README.md | 64 +++++++- azure.yaml | 6 + docker-compose.override.yml | 8 +- eShopOnWeb.sln | 1 + global.json | 2 +- infra/abbreviations.json | 135 ++++++++++++++++ infra/core/database/sqlserver/sqlserver.bicep | 129 ++++++++++++++++ infra/core/host/appservice.bicep | 101 ++++++++++++ infra/core/host/appserviceplan.bicep | 20 +++ infra/core/security/keyvault-access.bicep | 21 +++ infra/core/security/keyvault.bicep | 25 +++ infra/main.bicep | 144 ++++++++++++++++++ infra/main.parameters.json | 21 +++ src/ApplicationCore/ApplicationCore.csproj | 15 +- src/ApplicationCore/CatalogSettings.cs | 2 +- .../Entities/BasketAggregate/Basket.cs | 3 +- .../Entities/BuyerAggregate/Buyer.cs | 6 +- .../Entities/BuyerAggregate/PaymentMethod.cs | 6 +- src/ApplicationCore/Entities/CatalogItem.cs | 32 ++-- .../Entities/OrderAggregate/Address.cs | 1 + .../OrderAggregate/CatalogItemOrdered.cs | 6 +- .../Entities/OrderAggregate/Order.cs | 8 +- .../Entities/OrderAggregate/OrderItem.cs | 6 +- .../Extensions/GuardExtensions.cs | 6 - .../Extensions/JsonExtensions.cs | 2 +- .../Interfaces/IBasketService.cs | 3 +- src/ApplicationCore/Services/BasketService.cs | 17 +-- src/ApplicationCore/Services/OrderService.cs | 2 +- .../BasketWithItemsSpecification.cs | 2 +- .../CustomerOrdersSpecification.cs | 13 ++ .../Specifications/OrderWithItemsByIdSpec.cs | 2 +- src/BlazorAdmin/BlazorAdmin.csproj | 26 ++-- src/BlazorAdmin/CustomAuthStateProvider.cs | 2 +- .../Pages/CatalogItemPage/Delete.razor | 4 +- .../Pages/CatalogItemPage/Details.razor | 4 +- src/BlazorAdmin/Pages/Logout.razor | 2 +- src/BlazorAdmin/Shared/RedirectToLogin.razor | 11 +- src/BlazorShared/BlazorShared.csproj | 8 +- src/Infrastructure/Data/CatalogContext.cs | 5 +- .../Data/Config/BasketConfiguration.cs | 2 +- .../Data/Config/OrderConfiguration.cs | 2 +- src/Infrastructure/Data/FileItem.cs | 10 +- src/Infrastructure/Dependencies.cs | 4 +- .../Identity/AppIdentityDbContextSeed.cs | 5 +- .../Identity/IdentityTokenClaimService.cs | 1 + .../Identity/UserNotFoundException.cs | 10 ++ src/Infrastructure/Infrastructure.csproj | 15 +- .../AuthenticateEndpoint.ClaimValue.cs | 11 +- .../AuthenticateEndpoint.UserInfo.cs | 11 +- .../AuthEndpoints/AuthenticateEndpoint.cs | 3 +- .../CatalogBrandListEndpoint.cs | 10 +- .../CatalogItemGetByIdEndpoint.cs | 10 +- ...gedEndpoint.ListPagedCatalogItemRequest.cs | 4 +- .../CatalogItemListPagedEndpoint.cs | 21 ++- .../CreateCatalogItemEndpoint.cs | 16 +- .../DeleteCatalogItemEndpoint.cs | 13 +- .../UpdateCatalogItemEndpoint.cs | 21 +-- .../CatalogTypeListEndpoint.cs | 14 +- src/PublicApi/Dockerfile | 4 +- .../Middleware/ExceptionMiddleware.cs | 14 +- src/PublicApi/Program.cs | 21 ++- src/PublicApi/Properties/launchSettings.json | 35 +++-- src/PublicApi/PublicApi.csproj | 38 +++-- src/PublicApi/appsettings.Docker.json | 4 +- src/Web/.config/dotnet-tools.json | 2 +- .../Identity/Pages/Account/Login.cshtml.cs | 27 ++-- .../Identity/Pages/Account/Logout.cshtml.cs | 8 +- .../Identity/Pages/Account/Register.cshtml.cs | 22 +-- .../Configuration/ConfigureCoreServices.cs | 7 +- src/Web/Configuration/ConfigureWebServices.cs | 3 +- .../RevokeAuthenticationEvents.cs | 6 +- src/Web/Controllers/ManageController.cs | 30 ++-- src/Web/Controllers/OrderController.cs | 7 +- src/Web/Controllers/UserController.cs | 43 +++++- src/Web/Dockerfile | 4 +- src/Web/Extensions/UrlHelperExtensions.cs | 2 +- src/Web/Features/MyOrders/GetMyOrders.cs | 3 +- .../Features/MyOrders/GetMyOrdersHandler.cs | 16 +- .../Features/OrderDetails/GetOrderDetails.cs | 2 +- .../OrderDetails/GetOrderDetailsHandler.cs | 13 +- src/Web/HealthChecks/HomePageHealthCheck.cs | 4 +- src/Web/Pages/Basket/BasketItemViewModel.cs | 5 +- src/Web/Pages/Basket/BasketViewModel.cs | 2 +- src/Web/Pages/Basket/Checkout.cshtml.cs | 8 +- src/Web/Pages/Basket/Index.cshtml.cs | 9 +- src/Web/Pages/Error.cshtml.cs | 2 +- src/Web/Pages/Index.cshtml | 6 +- src/Web/Pages/Index.cshtml.cs | 7 +- .../Components/BasketComponent/Basket.cs | 6 +- src/Web/Pages/_ViewImports.cshtml | 1 - src/Web/Program.cs | 64 +++----- src/Web/Properties/launchSettings.json | 5 +- src/Web/Services/BasketViewModelService.cs | 2 +- .../Services/CachedCatalogViewModelService.cs | 12 +- .../Services/CatalogItemViewModelService.cs | 8 +- src/Web/SlugifyParameterTransformer.cs | 6 +- src/Web/ViewModels/Account/LoginViewModel.cs | 4 +- .../Account/LoginWith2faViewModel.cs | 2 +- .../ViewModels/Account/RegisterViewModel.cs | 6 +- .../Account/ResetPasswordViewModel.cs | 8 +- src/Web/ViewModels/CatalogIndexViewModel.cs | 11 +- src/Web/ViewModels/CatalogItemViewModel.cs | 4 +- src/Web/ViewModels/File/FileViewModel.cs | 6 +- .../Manage/ChangePasswordViewModel.cs | 8 +- .../Manage/EnableAuthenticatorViewModel.cs | 6 +- .../Manage/ExternalLoginsViewModel.cs | 6 +- src/Web/ViewModels/Manage/IndexViewModel.cs | 8 +- .../ViewModels/Manage/RemoveLoginViewModel.cs | 10 +- .../ViewModels/Manage/SetPasswordViewModel.cs | 6 +- .../Manage/ShowRecoveryCodesViewModel.cs | 2 +- src/Web/ViewModels/OrderDetailViewModel.cs | 6 + src/Web/ViewModels/OrderItemViewModel.cs | 4 +- src/Web/ViewModels/OrderViewModel.cs | 7 +- src/Web/ViewModels/PaginationInfoViewModel.cs | 4 +- src/Web/Views/Manage/ManageNavPages.cs | 2 +- src/Web/Views/Manage/ShowRecoverCodes.cshtml | 9 +- src/Web/Views/Order/Detail.cshtml | 8 +- src/Web/Views/Shared/_LoginPartial.cshtml | 2 +- src/Web/Web.csproj | 44 +++--- src/Web/appsettings.Docker.json | 4 +- src/Web/appsettings.json | 12 +- src/Web/libman.json | 12 +- tests/FunctionalTests/FunctionalTests.csproj | 18 +-- .../Web/Controllers/OrderControllerIndex.cs | 2 +- .../Web/Pages/Basket/BasketPageCheckout.cs | 2 +- .../Web/Pages/Basket/CheckoutTest.cs | 2 +- .../Web/Pages/Basket/IndexTest.cs | 4 +- tests/FunctionalTests/Web/WebPageHelpers.cs | 2 +- tests/FunctionalTests/Web/WebTestFixture.cs | 10 ++ .../IntegrationTests/IntegrationTests.csproj | 17 ++- .../GetByIdWithItemsAsync.cs | 2 +- .../AuthEndpoints/AuthenticateEndpointTest.cs | 39 +++-- .../CatalogItemGetByIdEndpointTest.cs | 37 +++-- .../CatalogItemListPagedEndpoint.cs | 83 ++++++---- .../CreateCatalogItemEndpointTest.cs | 95 ++++++------ .../DeleteCatalogItemEndpointTest.cs | 47 +++--- .../PublicApiIntegrationTests/ProgramTest.cs | 29 ++-- .../PublicApiIntegrationTests.csproj | 16 +- .../ApplicationCore/Extensions/TestParent.cs | 27 ++-- .../BasketServiceTests/AddItemToBasket.cs | 23 +-- .../BasketServiceTests/DeleteBasket.cs | 20 +-- .../BasketServiceTests/TransferBasket.cs | 84 +++++----- .../BasketWithItemsSpecification.cs | 20 +-- .../CatalogFilterPaginatedSpecification.cs | 12 +- .../CatalogFilterSpecification.cs | 4 +- .../CatalogItemsSpecification.cs | 22 ++- .../CustomerOrdersWithItemsSpecification.cs | 13 +- tests/UnitTests/Builders/BasketBuilder.cs | 12 +- .../OrdersTests/GetMyOrders.cs | 13 +- .../OrdersTests/GetOrderDetails.cs | 17 +-- tests/UnitTests/UnitTests.csproj | 22 ++- 163 files changed, 1732 insertions(+), 968 deletions(-) delete mode 100644 .devcontainer/Dockerfile create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dotnetcore.yml create mode 100644 .github/workflows/richnav.yml create mode 100644 Directory.Packages.props create mode 100644 azure.yaml create mode 100644 infra/abbreviations.json create mode 100644 infra/core/database/sqlserver/sqlserver.bicep create mode 100644 infra/core/host/appservice.bicep create mode 100644 infra/core/host/appserviceplan.bicep create mode 100644 infra/core/security/keyvault-access.bicep create mode 100644 infra/core/security/keyvault.bicep create mode 100644 infra/main.bicep create mode 100644 infra/main.parameters.json create mode 100644 src/ApplicationCore/Specifications/CustomerOrdersSpecification.cs create mode 100644 src/Infrastructure/Identity/UserNotFoundException.cs create mode 100644 src/Web/ViewModels/OrderDetailViewModel.cs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index d188b1d4..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ - -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- -FROM mcr.microsoft.com/dotnet/sdk:5.0 - -# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" -# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs -# will be updated to match your local UID/GID (when using the dockerFile property). -# See https://aka.ms/vscode-remote/containers/non-root-user for details. -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -# [Optional] Version of Node.js to install. -ARG INSTALL_NODE="false" -ARG NODE_VERSION="lts/*" -ENV NVM_DIR=/usr/local/share/nvm - -# [Optional] Install the Azure CLI -ARG INSTALL_AZURE_CLI="false" - -# Configure apt and install packages -RUN apt-get update \ - && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ - # - # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed - && apt-get -y install git openssh-client less iproute2 procps apt-transport-https gnupg2 curl lsb-release \ - # - # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. - && groupadd --gid $USER_GID $USERNAME \ - && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ - # [Optional] Add sudo support for the non-root user - && apt-get install -y sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ - && chmod 0440 /etc/sudoers.d/$USERNAME \ - # - # [Optional] Install Node.js for ASP.NET Core Web Applicationss - && if [ "$INSTALL_NODE" = "true" ]; then \ - # - # Install nvm and Node - mkdir -p ${NVM_DIR} \ - && curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 2>&1 \ - && chown -R ${USER_UID}:${USER_GID} ${NVM_DIR} \ - && /bin/bash -c "source $NVM_DIR/nvm.sh \ - && nvm alias default ${NODE_VERSION}" 2>&1 \ - && echo '[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \\. "$NVM_DIR/bash_completion"' \ - | tee -a /home/${USERNAME}/.bashrc /home/${USERNAME}/.zshrc >> /root/.zshrc \ - && echo "if [ \"\$(stat -c '%U' ${NVM_DIR})\" != \"${USERNAME}\" ]; then sudo chown -R ${USER_UID}:root ${NVM_DIR}; fi" \ - | tee -a /root/.bashrc /root/.zshrc /home/${USERNAME}/.bashrc >> /home/${USERNAME}/.zshrc \ - && chown ${USER_UID}:${USER_GID} /home/${USERNAME}/.bashrc /home/${USERNAME}/.zshrc \ - && chown -R ${USER_UID}:root ${NVM_DIR} \ - # - # Install yarn - && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ - && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ - && apt-get update \ - && apt-get -y install --no-install-recommends yarn; \ - fi \ - # - # [Optional] Install the Azure CLI - && if [ "$INSTALL_AZURE_CLI" = "true" ]; then \ - echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list \ - && curl -sL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - 2>/dev/null \ - && apt-get update \ - && apt-get install -y azure-cli; \ - fi \ - # - # Install EF Core dotnet tool - && dotnet tool install dotnet-ef --tool-path /home/$USERNAME/.dotnet/tools \ - && chown -R $USERNAME /home/$USERNAME/.dotnet \ - # - # Clean up - && apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* - -# Set PATH for dotnet tools -ENV PATH "$PATH:/home/$USERNAME/.dotnet/tools" \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6264a06d..a20ab945 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,39 +1,30 @@ -// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.112.0/containers/dotnetcore-3.1 +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet { "name": "eShopOnWeb", - "build": { - "dockerfile": "Dockerfile", - "args": { - "USERNAME": "vscode", - "INSTALL_NODE": "false", - "NODE_VERSION": "lts/*", - "INSTALL_AZURE_CLI": "false" - } - }, - - // Comment out to connect as root user. See https://aka.ms/vscode-remote/containers/non-root. - // make sure this is the same as USERNAME above - "remoteUser": "vscode", - - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash" + "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", + + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + "formulahendry.dotnet-test-explorer", + "ms-vscode.vscode-node-azure-pack", + "ms-kubernetes-tools.vscode-kubernetes-tools", + "redhat.vscode-yaml" + ] + } }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-dotnettools.csharp", - "formulahendry.dotnet-test-explorer", - "ms-vscode.vscode-node-azure-pack", - "ms-kubernetes-tools.vscode-kubernetes-tools", - "redhat.vscode-yaml" - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [5000, 5001], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "dotnet dev-certs https --trust" + // [Optional] To reuse of your local HTTPS dev cert, first export it locally using this command: // * Windows PowerShell: // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" @@ -43,14 +34,11 @@ // Next, after running the command above, uncomment lines in the 'mounts' and 'remoteEnv' lines below, // and open / rebuild the container so the settings take effect. // - "mounts": [ - // "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" - ], - "remoteEnv": { - // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", - // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", - } - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "dotnet restore" + // "mounts": [ + // // "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" + // ], + // "remoteEnv": { + // // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", + // // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", + // }, } diff --git a/.editorconfig b/.editorconfig index 88b30b08..459d7521 100644 --- a/.editorconfig +++ b/.editorconfig @@ -141,4 +141,10 @@ csharp_preserve_single_line_blocks = true ############################### [*.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 \ No newline at end of file +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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5dc46e6b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..bc18f008 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml new file mode 100644 index 00000000..92a83271 --- /dev/null +++ b/.github/workflows/dotnetcore.yml @@ -0,0 +1,23 @@ +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@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.x' + include-prerelease: true + + - name: Build with dotnet + run: dotnet build ./eShopOnWeb.sln --configuration Release + + - name: Test with dotnet + run: dotnet test ./eShopOnWeb.sln --configuration Release diff --git a/.github/workflows/eshoponweb-cicd.yml b/.github/workflows/eshoponweb-cicd.yml index 41e8f28b..961a7bf7 100644 --- a/.github/workflows/eshoponweb-cicd.yml +++ b/.github/workflows/eshoponweb-cicd.yml @@ -23,7 +23,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' include-prerelease: true #Build/Test/Publish the .net project - name: Build with dotnet diff --git a/.github/workflows/richnav.yml b/.github/workflows/richnav.yml new file mode 100644 index 00000000..1202a15a --- /dev/null +++ b/.github/workflows/richnav.yml @@ -0,0 +1,24 @@ +name: eShopOnWeb - Code Index + +on: workflow_dispatch + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + 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' diff --git a/.gitignore b/.gitignore index 1148ecd7..91a682b0 100644 --- a/.gitignore +++ b/.gitignore @@ -257,3 +257,6 @@ pub/ #Ignore marker-file used to know which docker files we have. .eshopdocker_* +.devcontainer + +.azure diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d0663c2c..680470c7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "formulahendry.dotnet-test-explorer", "ms-vscode.vscode-node-azure-pack", "ms-kubernetes-tools.vscode-kubernetes-tools", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "ms-azuretools.azure-dev" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index d375caeb..378eecf6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/Web/bin/Debug/net5.0/Web.dll", + "program": "${workspaceFolder}/src/Web/bin/Debug/net8.0/Web.dll", "args": [], "cwd": "${workspaceFolder}/src/Web", "stopAtEntry": false, diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..0aa8cd4b --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,72 @@ + + + true + net8.0 + 8.0.0 + 8.0.0 + 8.0.0 + 8.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + diff --git a/README.md b/README.md index 29c0a765..18ef6735 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A list of Frequently Asked Questions about this repository can be found [here](h ## 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 7.0**. [Also available in ePub/mobi formats](https://dotnet.microsoft.com/learn/web/aspnet-architecture). +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/ @@ -24,7 +24,7 @@ The **eShopOnWeb** sample is related to the [eShopOnContainers](https://github.c 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 7.0. +> #### The `main` branch is currently running ASP.NET Core 8.0. > #### Older versions are tagged. ## Topics (eBook TOC) @@ -41,13 +41,58 @@ The goal for this sample is to demonstrate some of the principles and patterns d - Development Process for Azure-Hosted ASP.NET Core Apps - Azure Hosting Recommendations for ASP.NET Core Web Apps -## Running the sample +## 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) -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/`. 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). +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. @@ -56,13 +101,12 @@ 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 `appsettings.json` +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. @@ -96,6 +140,14 @@ You can also run the samples in Docker (see below). 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): diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 00000000..70836080 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,6 @@ +name: eShopOnWeb +services: + web: + project: ./src/Web + language: csharp + host: appservice \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml index be54c851..ef29f5e5 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -3,18 +3,18 @@ services: eshopwebmvc: environment: - ASPNETCORE_ENVIRONMENT=Docker - - ASPNETCORE_URLS=http://+:80 + - ASPNETCORE_URLS=http://+:8080 ports: - - "5106:80" + - "5106:8080" volumes: - ~/.aspnet/https:/root/.aspnet/https:ro - ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro eshoppublicapi: environment: - ASPNETCORE_ENVIRONMENT=Docker - - ASPNETCORE_URLS=http://+:80 + - ASPNETCORE_URLS=http://+:8080 ports: - - "5200:80" + - "5200:8080" volumes: - ~/.aspnet/https:/root/.aspnet/https:ro - ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro \ No newline at end of file diff --git a/eShopOnWeb.sln b/eShopOnWeb.sln index 2adaaf8c..ef1b62d6 100644 --- a/eShopOnWeb.sln +++ b/eShopOnWeb.sln @@ -24,6 +24,7 @@ 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 diff --git a/global.json b/global.json index 957199cf..7d33bfd5 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.x", + "version": "8.0.x", "rollForward": "latestFeature" } } diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 00000000..a4fc9dfe --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,135 @@ +{ + "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-" +} \ No newline at end of file diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep new file mode 100644 index 00000000..64477a74 --- /dev/null +++ b/infra/core/database/sqlserver/sqlserver.bicep @@ -0,0 +1,129 @@ +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 diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep new file mode 100644 index 00000000..c65f2b89 --- /dev/null +++ b/infra/core/host/appservice.bicep @@ -0,0 +1,101 @@ +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}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep new file mode 100644 index 00000000..69c35d78 --- /dev/null +++ b/infra/core/host/appserviceplan.bicep @@ -0,0 +1,20 @@ +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 diff --git a/infra/core/security/keyvault-access.bicep b/infra/core/security/keyvault-access.bicep new file mode 100644 index 00000000..aa989ebd --- /dev/null +++ b/infra/core/security/keyvault-access.bicep @@ -0,0 +1,21 @@ +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 +} diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep new file mode 100644 index 00000000..0eb4a86d --- /dev/null +++ b/infra/core/security/keyvault.bicep @@ -0,0 +1,25 @@ +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 diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 00000000..ffad0112 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,144 @@ +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 diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 00000000..0ef1d971 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,21 @@ +{ + "$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)" + } + } +} \ No newline at end of file diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index ef2c86d9..a0a5bbe2 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -1,17 +1,16 @@  - - net7.0 + Microsoft.eShopWeb.ApplicationCore - disable + enable - - - - - + + + + + diff --git a/src/ApplicationCore/CatalogSettings.cs b/src/ApplicationCore/CatalogSettings.cs index 5ea8bd2a..bd5a0b60 100644 --- a/src/ApplicationCore/CatalogSettings.cs +++ b/src/ApplicationCore/CatalogSettings.cs @@ -2,5 +2,5 @@ public class CatalogSettings { - public string CatalogBaseUrl { get; set; } + public string? CatalogBaseUrl { get; set; } } diff --git a/src/ApplicationCore/Entities/BasketAggregate/Basket.cs b/src/ApplicationCore/Entities/BasketAggregate/Basket.cs index 3558f5b9..f630e629 100644 --- a/src/ApplicationCore/Entities/BasketAggregate/Basket.cs +++ b/src/ApplicationCore/Entities/BasketAggregate/Basket.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; @@ -25,7 +26,7 @@ public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1) _items.Add(new BasketItem(catalogItemId, quantity, unitPrice)); return; } - var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId); + var existingItem = Items.First(i => i.CatalogItemId == catalogItemId); existingItem.AddQuantity(quantity); } diff --git a/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs index 72f7f1a8..8a553f1d 100644 --- a/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs +++ b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs @@ -12,10 +12,8 @@ public class Buyer : BaseEntity, IAggregateRoot public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); - private Buyer() - { - // required by EF - } + #pragma warning disable CS8618 // Required by Entity Framework + private Buyer() { } public Buyer(string identity) : this() { diff --git a/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs index f4d2e542..97ed93d1 100644 --- a/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs +++ b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs @@ -2,7 +2,7 @@ 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; } + 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; } } diff --git a/src/ApplicationCore/Entities/CatalogItem.cs b/src/ApplicationCore/Entities/CatalogItem.cs index abf5a026..1b9534de 100644 --- a/src/ApplicationCore/Entities/CatalogItem.cs +++ b/src/ApplicationCore/Entities/CatalogItem.cs @@ -11,9 +11,9 @@ public class CatalogItem : BaseEntity, IAggregateRoot 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 CatalogType? CatalogType { get; private set; } public int CatalogBrandId { get; private set; } - public CatalogBrand CatalogBrand { get; private set; } + public CatalogBrand? CatalogBrand { get; private set; } public CatalogItem(int catalogTypeId, int catalogBrandId, @@ -30,15 +30,15 @@ public CatalogItem(int catalogTypeId, PictureUri = pictureUri; } - public void UpdateDetails(string name, string description, decimal price) + public void UpdateDetails(CatalogItemDetails details) { - Guard.Against.NullOrEmpty(name, nameof(name)); - Guard.Against.NullOrEmpty(description, nameof(description)); - Guard.Against.NegativeOrZero(price, nameof(price)); + 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 = name; - Description = description; - Price = price; + Name = details.Name; + Description = details.Description; + Price = details.Price; } public void UpdateBrand(int catalogBrandId) @@ -62,4 +62,18 @@ public void UpdatePictureUri(string pictureName) } 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; + } + } } diff --git a/src/ApplicationCore/Entities/OrderAggregate/Address.cs b/src/ApplicationCore/Entities/OrderAggregate/Address.cs index 65bd2613..8b651cd2 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/Address.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/Address.cs @@ -12,6 +12,7 @@ public class Address // ValueObject 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) diff --git a/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs index 98cce229..59d1adb1 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs @@ -19,10 +19,8 @@ public CatalogItemOrdered(int catalogItemId, string productName, string pictureU PictureUri = pictureUri; } - private CatalogItemOrdered() - { - // required by EF - } + #pragma warning disable CS8618 // Required by Entity Framework + private CatalogItemOrdered() {} public int CatalogItemId { get; private set; } public string ProductName { get; private set; } diff --git a/src/ApplicationCore/Entities/OrderAggregate/Order.cs b/src/ApplicationCore/Entities/OrderAggregate/Order.cs index 53c587e6..ca9a86e1 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/Order.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/Order.cs @@ -7,16 +7,12 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; public class Order : BaseEntity, IAggregateRoot { - private Order() - { - // required by EF - } + #pragma warning disable CS8618 // Required by Entity Framework + private Order() {} public Order(string buyerId, Address shipToAddress, List items) { Guard.Against.NullOrEmpty(buyerId, nameof(buyerId)); - Guard.Against.Null(shipToAddress, nameof(shipToAddress)); - Guard.Against.Null(items, nameof(items)); BuyerId = buyerId; ShipToAddress = shipToAddress; diff --git a/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs index fec25825..43054d18 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs @@ -6,10 +6,8 @@ public class OrderItem : BaseEntity public decimal UnitPrice { get; private set; } public int Units { get; private set; } - private OrderItem() - { - // required by EF - } + #pragma warning disable CS8618 // Required by Entity Framework + private OrderItem() {} public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) { diff --git a/src/ApplicationCore/Extensions/GuardExtensions.cs b/src/ApplicationCore/Extensions/GuardExtensions.cs index 06a781af..138602f0 100644 --- a/src/ApplicationCore/Extensions/GuardExtensions.cs +++ b/src/ApplicationCore/Extensions/GuardExtensions.cs @@ -7,12 +7,6 @@ namespace Ardalis.GuardClauses; public static class BasketGuards { - public static void NullBasket(this IGuardClause guardClause, int basketId, Basket basket) - { - if (basket == null) - throw new BasketNotFoundException(basketId); - } - public static void EmptyBasketOnCheckout(this IGuardClause guardClause, IReadOnlyCollection basketItems) { if (!basketItems.Any()) diff --git a/src/ApplicationCore/Extensions/JsonExtensions.cs b/src/ApplicationCore/Extensions/JsonExtensions.cs index 1a2b8e07..622bcc92 100644 --- a/src/ApplicationCore/Extensions/JsonExtensions.cs +++ b/src/ApplicationCore/Extensions/JsonExtensions.cs @@ -9,7 +9,7 @@ public static class JsonExtensions PropertyNameCaseInsensitive = true }; - public static T FromJson(this string json) => + public static T? FromJson(this string json) => JsonSerializer.Deserialize(json, _jsonOptions); public static string ToJson(this T obj) => diff --git a/src/ApplicationCore/Interfaces/IBasketService.cs b/src/ApplicationCore/Interfaces/IBasketService.cs index 4dbdf9fc..204f5f14 100644 --- a/src/ApplicationCore/Interfaces/IBasketService.cs +++ b/src/ApplicationCore/Interfaces/IBasketService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Ardalis.Result; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; namespace Microsoft.eShopWeb.ApplicationCore.Interfaces; @@ -8,6 +9,6 @@ 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> SetQuantities(int basketId, Dictionary quantities); Task DeleteBasketAsync(int basketId); } diff --git a/src/ApplicationCore/Services/BasketService.cs b/src/ApplicationCore/Services/BasketService.cs index 167c1cb7..ec810f39 100644 --- a/src/ApplicationCore/Services/BasketService.cs +++ b/src/ApplicationCore/Services/BasketService.cs @@ -1,6 +1,7 @@ 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; @@ -22,7 +23,7 @@ public BasketService(IRepository basketRepository, public async Task AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1) { var basketSpec = new BasketWithItemsSpecification(username); - var basket = await _basketRepository.GetBySpecAsync(basketSpec); + var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); if (basket == null) { @@ -39,15 +40,15 @@ public async Task AddItemToBasket(string username, int catalogItemId, de 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) + public async Task> SetQuantities(int basketId, Dictionary quantities) { - Guard.Against.Null(quantities, nameof(quantities)); var basketSpec = new BasketWithItemsSpecification(basketId); - var basket = await _basketRepository.GetBySpecAsync(basketSpec); - Guard.Against.NullBasket(basketId, basket); + var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); + if (basket == null) return Result.NotFound(); foreach (var item in basket.Items) { @@ -64,13 +65,11 @@ public async Task SetQuantities(int basketId, Dictionary qu public async Task TransferBasketAsync(string anonymousId, string userName) { - Guard.Against.NullOrEmpty(anonymousId, nameof(anonymousId)); - Guard.Against.NullOrEmpty(userName, nameof(userName)); var anonymousBasketSpec = new BasketWithItemsSpecification(anonymousId); - var anonymousBasket = await _basketRepository.GetBySpecAsync(anonymousBasketSpec); + var anonymousBasket = await _basketRepository.FirstOrDefaultAsync(anonymousBasketSpec); if (anonymousBasket == null) return; var userBasketSpec = new BasketWithItemsSpecification(userName); - var userBasket = await _basketRepository.GetBySpecAsync(userBasketSpec); + var userBasket = await _basketRepository.FirstOrDefaultAsync(userBasketSpec); if (userBasket == null) { userBasket = new Basket(userName); diff --git a/src/ApplicationCore/Services/OrderService.cs b/src/ApplicationCore/Services/OrderService.cs index eca3abc5..3bc88f15 100644 --- a/src/ApplicationCore/Services/OrderService.cs +++ b/src/ApplicationCore/Services/OrderService.cs @@ -32,7 +32,7 @@ public async Task CreateOrderAsync(int basketId, Address shippingAddress) var basketSpec = new BasketWithItemsSpecification(basketId); var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); - Guard.Against.NullBasket(basketId, basket); + Guard.Against.Null(basket, nameof(basket)); Guard.Against.EmptyBasketOnCheckout(basket.Items); var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray()); diff --git a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs index 93278820..d5971469 100644 --- a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs +++ b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -3,7 +3,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Specifications; -public sealed class BasketWithItemsSpecification : Specification, ISingleResultSpecification +public sealed class BasketWithItemsSpecification : Specification { public BasketWithItemsSpecification(int basketId) { diff --git a/src/ApplicationCore/Specifications/CustomerOrdersSpecification.cs b/src/ApplicationCore/Specifications/CustomerOrdersSpecification.cs new file mode 100644 index 00000000..e3faa6ae --- /dev/null +++ b/src/ApplicationCore/Specifications/CustomerOrdersSpecification.cs @@ -0,0 +1,13 @@ +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); + } +} diff --git a/src/ApplicationCore/Specifications/OrderWithItemsByIdSpec.cs b/src/ApplicationCore/Specifications/OrderWithItemsByIdSpec.cs index 2f4fc290..c9b3a729 100644 --- a/src/ApplicationCore/Specifications/OrderWithItemsByIdSpec.cs +++ b/src/ApplicationCore/Specifications/OrderWithItemsByIdSpec.cs @@ -3,7 +3,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Specifications; -public class OrderWithItemsByIdSpec : Specification, ISingleResultSpecification +public class OrderWithItemsByIdSpec : Specification { public OrderWithItemsByIdSpec(int orderId) { diff --git a/src/BlazorAdmin/BlazorAdmin.csproj b/src/BlazorAdmin/BlazorAdmin.csproj index 624c823e..393b3304 100644 --- a/src/BlazorAdmin/BlazorAdmin.csproj +++ b/src/BlazorAdmin/BlazorAdmin.csproj @@ -1,20 +1,14 @@ - - - - net7.0 - disable - - + - - - - - - - - - + + + + + + + + + diff --git a/src/BlazorAdmin/CustomAuthStateProvider.cs b/src/BlazorAdmin/CustomAuthStateProvider.cs index b12d2325..30cf42cb 100644 --- a/src/BlazorAdmin/CustomAuthStateProvider.cs +++ b/src/BlazorAdmin/CustomAuthStateProvider.cs @@ -63,7 +63,7 @@ private async Task FetchUser() if (user == null || !user.IsAuthenticated) { - return null; + return new ClaimsPrincipal(new ClaimsIdentity()); } var identity = new ClaimsIdentity( diff --git a/src/BlazorAdmin/Pages/CatalogItemPage/Delete.razor b/src/BlazorAdmin/Pages/CatalogItemPage/Delete.razor index b1e5e120..5791195d 100644 --- a/src/BlazorAdmin/Pages/CatalogItemPage/Delete.razor +++ b/src/BlazorAdmin/Pages/CatalogItemPage/Delete.razor @@ -92,9 +92,9 @@ @code { [Parameter] - public IEnumerable Brands { get; set; } = default!; + public IEnumerable Brands { get; set; } [Parameter] - public IEnumerable Types { get; set; } = default!; + public IEnumerable Types { get; set; } [Parameter] public EventCallback OnSaveClick { get; set; } diff --git a/src/BlazorAdmin/Pages/CatalogItemPage/Details.razor b/src/BlazorAdmin/Pages/CatalogItemPage/Details.razor index 25f2f28b..c4883732 100644 --- a/src/BlazorAdmin/Pages/CatalogItemPage/Details.razor +++ b/src/BlazorAdmin/Pages/CatalogItemPage/Details.razor @@ -95,9 +95,9 @@ @code { [Parameter] - public IEnumerable Brands { get; set; } = default!; + public IEnumerable Brands { get; set; } [Parameter] - public IEnumerable Types { get; set; } = default!; + public IEnumerable Types { get; set; } [Parameter] public EventCallback OnEditClick { get; set; } diff --git a/src/BlazorAdmin/Pages/Logout.razor b/src/BlazorAdmin/Pages/Logout.razor index ddcd25a2..ada679c7 100644 --- a/src/BlazorAdmin/Pages/Logout.razor +++ b/src/BlazorAdmin/Pages/Logout.razor @@ -7,7 +7,7 @@ protected override async Task OnInitializedAsync() { - await HttpClient.PostAsync("Identity/Account/Logout", null); + await HttpClient.PostAsync("User/Logout", null); await new Route(JSRuntime).RouteOutside("/Identity/Account/Login"); } diff --git a/src/BlazorAdmin/Shared/RedirectToLogin.razor b/src/BlazorAdmin/Shared/RedirectToLogin.razor index 810a66ea..de9c49db 100644 --- a/src/BlazorAdmin/Shared/RedirectToLogin.razor +++ b/src/BlazorAdmin/Shared/RedirectToLogin.razor @@ -1,9 +1,12 @@ -@inject NavigationManager Navigation +@using System.Web; + +@inject NavigationManager Navigation +@inject IJSRuntime JsRuntime @code { protected override void OnInitialized() - { - Navigation.NavigateTo($"Identity/Account/Login?returnUrl=" + - $"/{Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri))}"); + { + var returnUrl = HttpUtility.UrlEncode($"/{Uri.EscapeDataString(Navigation.ToBaseRelativePath(Navigation.Uri))}"); + JsRuntime.InvokeVoidAsync("location.replace", $"Identity/Account/Login?returnUrl={returnUrl}"); } } \ No newline at end of file diff --git a/src/BlazorShared/BlazorShared.csproj b/src/BlazorShared/BlazorShared.csproj index 3d0fe1b4..8cd1e29d 100644 --- a/src/BlazorShared/BlazorShared.csproj +++ b/src/BlazorShared/BlazorShared.csproj @@ -1,15 +1,13 @@  - - net7.0 + BlazorShared BlazorShared - disable - - + + diff --git a/src/Infrastructure/Data/CatalogContext.cs b/src/Infrastructure/Data/CatalogContext.cs index f9f340a6..fc2e4b6a 100644 --- a/src/Infrastructure/Data/CatalogContext.cs +++ b/src/Infrastructure/Data/CatalogContext.cs @@ -8,9 +8,8 @@ namespace Microsoft.eShopWeb.Infrastructure.Data; public class CatalogContext : DbContext { - public CatalogContext(DbContextOptions options) : base(options) - { - } + #pragma warning disable CS8618 // Required by Entity Framework + public CatalogContext(DbContextOptions options) : base(options) {} public DbSet Baskets { get; set; } public DbSet CatalogItems { get; set; } diff --git a/src/Infrastructure/Data/Config/BasketConfiguration.cs b/src/Infrastructure/Data/Config/BasketConfiguration.cs index d66fcf48..96c3e07e 100644 --- a/src/Infrastructure/Data/Config/BasketConfiguration.cs +++ b/src/Infrastructure/Data/Config/BasketConfiguration.cs @@ -9,7 +9,7 @@ public class BasketConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items)); - navigation.SetPropertyAccessMode(PropertyAccessMode.Field); + navigation?.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Property(b => b.BuyerId) .IsRequired() diff --git a/src/Infrastructure/Data/Config/OrderConfiguration.cs b/src/Infrastructure/Data/Config/OrderConfiguration.cs index 3b5a8c3d..b1692377 100644 --- a/src/Infrastructure/Data/Config/OrderConfiguration.cs +++ b/src/Infrastructure/Data/Config/OrderConfiguration.cs @@ -10,7 +10,7 @@ public void Configure(EntityTypeBuilder builder) { var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems)); - navigation.SetPropertyAccessMode(PropertyAccessMode.Field); + navigation?.SetPropertyAccessMode(PropertyAccessMode.Field); builder.Property(b => b.BuyerId) .IsRequired() diff --git a/src/Infrastructure/Data/FileItem.cs b/src/Infrastructure/Data/FileItem.cs index 0229b886..329c0419 100644 --- a/src/Infrastructure/Data/FileItem.cs +++ b/src/Infrastructure/Data/FileItem.cs @@ -2,10 +2,10 @@ public class FileItem { - public string FileName { get; set; } - public string Url { get; set; } + 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; } + public string? Ext { get; set; } + public string? Type { get; set; } + public string? DataBase64 { get; set; } } diff --git a/src/Infrastructure/Dependencies.cs b/src/Infrastructure/Dependencies.cs index 7424af92..96456763 100644 --- a/src/Infrastructure/Dependencies.cs +++ b/src/Infrastructure/Dependencies.cs @@ -10,10 +10,10 @@ public static class Dependencies { public static void ConfigureServices(IConfiguration configuration, IServiceCollection services) { - var useOnlyInMemoryDatabase = true; + bool useOnlyInMemoryDatabase = false; if (configuration["UseOnlyInMemoryDatabase"] != null) { - useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]); + useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]!); } if (useOnlyInMemoryDatabase) diff --git a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs index 35310052..8be12f64 100644 --- a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs +++ b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs @@ -24,6 +24,9 @@ public static async Task SeedAsync(AppIdentityDbContext identityDbContext, UserM var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName }; await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD); adminUser = await userManager.FindByNameAsync(adminUserName); - await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS); + if (adminUser != null) + { + await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS); + } } } diff --git a/src/Infrastructure/Identity/IdentityTokenClaimService.cs b/src/Infrastructure/Identity/IdentityTokenClaimService.cs index c45f3550..36de7ae7 100644 --- a/src/Infrastructure/Identity/IdentityTokenClaimService.cs +++ b/src/Infrastructure/Identity/IdentityTokenClaimService.cs @@ -25,6 +25,7 @@ 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) }; diff --git a/src/Infrastructure/Identity/UserNotFoundException.cs b/src/Infrastructure/Identity/UserNotFoundException.cs new file mode 100644 index 00000000..0a98b9e8 --- /dev/null +++ b/src/Infrastructure/Identity/UserNotFoundException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Microsoft.eShopWeb.Infrastructure.Identity; + +public class UserNotFoundException : Exception +{ + public UserNotFoundException(string userName) : base($"No user found with username: {userName}") + { + } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 31760d81..026a8ca9 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -1,17 +1,16 @@  - - net7.0 + Microsoft.eShopWeb.Infrastructure - disable + enable - - - - - + + + + + diff --git a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs index 5296e715..9571c3a9 100644 --- a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; +namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; public class ClaimValue { @@ -17,6 +12,6 @@ public ClaimValue(string type, string value) Value = value; } - public string Type { get; set; } - public string Value { get; set; } + public string Type { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; } diff --git a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs index 8c55fbb7..8a4eaf8b 100644 --- a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; @@ -9,7 +6,7 @@ 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 IEnumerable Claims { get; set; } + public string NameClaimType { get; set; } = string.Empty; + public string RoleClaimType { get; set; } = string.Empty; + public IEnumerable Claims { get; set; } = new List(); } diff --git a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs index 0d5bece3..c5cce657 100644 --- a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs @@ -33,7 +33,8 @@ public AuthenticateEndpoint(SignInManager signInManager, OperationId = "auth.authenticate", Tags = new[] { "AuthEndpoints" }) ] - public override async Task> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken = default) + public override async Task> HandleAsync(AuthenticateRequest request, + CancellationToken cancellationToken = default) { var response = new AuthenticateResponse(request.CorrelationId()); diff --git a/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs b/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs index dce823e7..32b3d0b9 100644 --- a/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs +++ b/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs @@ -13,9 +13,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; /// /// List Catalog Brands /// -public class CatalogBrandListEndpoint : IEndpoint +public class CatalogBrandListEndpoint : IEndpoint> { - private IRepository _catalogBrandRepository; private readonly IMapper _mapper; public CatalogBrandListEndpoint(IMapper mapper) @@ -28,18 +27,17 @@ public void AddRoute(IEndpointRouteBuilder app) app.MapGet("api/catalog-brands", async (IRepository catalogBrandRepository) => { - _catalogBrandRepository = catalogBrandRepository; - return await HandleAsync(); + return await HandleAsync(catalogBrandRepository); }) .Produces() .WithTags("CatalogBrandEndpoints"); } - public async Task HandleAsync() + public async Task HandleAsync(IRepository catalogBrandRepository) { var response = new ListCatalogBrandsResponse(); - var items = await _catalogBrandRepository.ListAsync(); + var items = await catalogBrandRepository.ListAsync(); response.CatalogBrands.AddRange(items.Select(_mapper.Map)); diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs index a1f60111..9f15c72a 100644 --- a/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs @@ -11,9 +11,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Get a Catalog Item by Id /// -public class CatalogItemGetByIdEndpoint : IEndpoint +public class CatalogItemGetByIdEndpoint : IEndpoint> { - private IRepository _itemRepository; private readonly IUriComposer _uriComposer; public CatalogItemGetByIdEndpoint(IUriComposer uriComposer) @@ -26,18 +25,17 @@ public void AddRoute(IEndpointRouteBuilder app) app.MapGet("api/catalog-items/{catalogItemId}", async (int catalogItemId, IRepository itemRepository) => { - _itemRepository = itemRepository; - return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId)); + return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId), itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } - public async Task HandleAsync(GetByIdCatalogItemRequest request) + public async Task HandleAsync(GetByIdCatalogItemRequest request, IRepository itemRepository) { var response = new GetByIdCatalogItemResponse(request.CorrelationId()); - var item = await _itemRepository.GetByIdAsync(request.CatalogItemId); + var item = await itemRepository.GetByIdAsync(request.CatalogItemId); if (item is null) return Results.NotFound(); diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs index e9744d8b..19691af1 100644 --- a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs @@ -2,8 +2,8 @@ public class ListPagedCatalogItemRequest : BaseRequest { - public int? PageSize { get; init; } - public int? PageIndex { get; init; } + public int PageSize { get; init; } + public int PageIndex { get; init; } public int? CatalogBrandId { get; init; } public int? CatalogTypeId { get; init; } diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs index 308d3106..3e36d2f4 100644 --- a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs @@ -15,9 +15,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// List Catalog Items (paged) /// -public class CatalogItemListPagedEndpoint : IEndpoint +public class CatalogItemListPagedEndpoint : IEndpoint> { - private IRepository _itemRepository; private readonly IUriComposer _uriComposer; private readonly IMapper _mapper; @@ -32,27 +31,27 @@ public void AddRoute(IEndpointRouteBuilder app) app.MapGet("api/catalog-items", async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository itemRepository) => { - _itemRepository = itemRepository; - return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId)); - }) + return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId), itemRepository); + }) .Produces() .WithTags("CatalogItemEndpoints"); } - public async Task HandleAsync(ListPagedCatalogItemRequest request) + 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); + int totalItems = await itemRepository.CountAsync(filterSpec); var pagedSpec = new CatalogFilterPaginatedSpecification( - skip: request.PageIndex.Value * request.PageSize.Value, - take: request.PageSize.Value, + skip: request.PageIndex * request.PageSize, + take: request.PageSize, brandId: request.CatalogBrandId, typeId: request.CatalogTypeId); - var items = await _itemRepository.ListAsync(pagedSpec); + var items = await itemRepository.ListAsync(pagedSpec); response.CatalogItems.AddRange(items.Select(_mapper.Map)); foreach (CatalogItemDto item in response.CatalogItems) @@ -62,7 +61,7 @@ public async Task HandleAsync(ListPagedCatalogItemRequest request) if (request.PageSize > 0) { - response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize.Value).ToString()); + response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString()); } else { diff --git a/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs index 25527f93..c15346ff 100644 --- a/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs @@ -15,9 +15,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Creates a new Catalog Item /// -public class CreateCatalogItemEndpoint : IEndpoint +public class CreateCatalogItemEndpoint : IEndpoint> { - private IRepository _itemRepository; private readonly IUriComposer _uriComposer; public CreateCatalogItemEndpoint(IUriComposer uriComposer) @@ -31,26 +30,25 @@ public void AddRoute(IEndpointRouteBuilder app) [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async (CreateCatalogItemRequest request, IRepository itemRepository) => { - _itemRepository = itemRepository; - return await HandleAsync(request); + return await HandleAsync(request, itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } - public async Task HandleAsync(CreateCatalogItemRequest request) + 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); + 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); + newItem = await itemRepository.AddAsync(newItem); if (newItem.Id != 0) { @@ -59,7 +57,7 @@ public async Task HandleAsync(CreateCatalogItemRequest request) // 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); + await itemRepository.UpdateAsync(newItem); } var dto = new CatalogItemDto @@ -73,6 +71,6 @@ public async Task HandleAsync(CreateCatalogItemRequest request) Price = newItem.Price }; response.CatalogItem = dto; - return Results.Created($"api/catalog-items/{dto.Id}", response); + return Results.Created($"api/catalog-items/{dto.Id}", response); } } diff --git a/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs index 2a0d3e64..0e37f445 100644 --- a/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs @@ -13,32 +13,29 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Deletes a Catalog Item /// -public class DeleteCatalogItemEndpoint : IEndpoint +public class DeleteCatalogItemEndpoint : IEndpoint> { - private IRepository _itemRepository; - 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) => { - _itemRepository = itemRepository; - return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId)); + return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId), itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } - public async Task HandleAsync(DeleteCatalogItemRequest request) + public async Task HandleAsync(DeleteCatalogItemRequest request, IRepository itemRepository) { var response = new DeleteCatalogItemResponse(request.CorrelationId()); - var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId); + var itemToDelete = await itemRepository.GetByIdAsync(request.CatalogItemId); if (itemToDelete is null) return Results.NotFound(); - await _itemRepository.DeleteAsync(itemToDelete); + await itemRepository.DeleteAsync(itemToDelete); return Results.Ok(response); } diff --git a/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs index 7bc715b2..15efa684 100644 --- a/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs @@ -13,9 +13,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; /// /// Updates a Catalog Item /// -public class UpdateCatalogItemEndpoint : IEndpoint -{ - private IRepository _itemRepository; +public class UpdateCatalogItemEndpoint : IEndpoint> +{ private readonly IUriComposer _uriComposer; public UpdateCatalogItemEndpoint(IUriComposer uriComposer) @@ -29,24 +28,28 @@ public void AddRoute(IEndpointRouteBuilder app) [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async (UpdateCatalogItemRequest request, IRepository itemRepository) => { - _itemRepository = itemRepository; - return await HandleAsync(request); + return await HandleAsync(request, itemRepository); }) .Produces() .WithTags("CatalogItemEndpoints"); } - public async Task HandleAsync(UpdateCatalogItemRequest request) + public async Task HandleAsync(UpdateCatalogItemRequest request, IRepository itemRepository) { var response = new UpdateCatalogItemResponse(request.CorrelationId()); - var existingItem = await _itemRepository.GetByIdAsync(request.Id); + var existingItem = await itemRepository.GetByIdAsync(request.Id); + if (existingItem == null) + { + return Results.NotFound(); + } - existingItem.UpdateDetails(request.Name, request.Description, request.Price); + 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); + await itemRepository.UpdateAsync(existingItem); var dto = new CatalogItemDto { diff --git a/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs b/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs index 87aa0354..3e36735b 100644 --- a/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs +++ b/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs @@ -13,33 +13,31 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; /// /// List Catalog Types /// -public class CatalogTypeListEndpoint : IEndpoint +public class CatalogTypeListEndpoint : IEndpoint> { - private IRepository _catalogTypeRepository; private readonly IMapper _mapper; public CatalogTypeListEndpoint(IMapper mapper) - { + { _mapper = mapper; } public void AddRoute(IEndpointRouteBuilder app) { - app.MapGet("api/catalog-types", + app.MapGet("api/catalog-types", async (IRepository catalogTypeRepository) => { - _catalogTypeRepository = catalogTypeRepository; - return await HandleAsync(); + return await HandleAsync(catalogTypeRepository); }) .Produces() .WithTags("CatalogTypeEndpoints"); } - public async Task HandleAsync() + public async Task HandleAsync(IRepository catalogTypeRepository) { var response = new ListCatalogTypesResponse(); - var items = await _catalogTypeRepository.ListAsync(); + var items = await catalogTypeRepository.ListAsync(); response.CatalogTypes.AddRange(items.Select(_mapper.Map)); diff --git a/src/PublicApi/Dockerfile b/src/PublicApi/Dockerfile index 885a1551..51e74109 100644 --- a/src/PublicApi/Dockerfile +++ b/src/PublicApi/Dockerfile @@ -1,11 +1,11 @@ #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:7.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app COPY . . #COPY ["src/PublicApi/PublicApi.csproj", "./PublicApi/"] diff --git a/src/PublicApi/Middleware/ExceptionMiddleware.cs b/src/PublicApi/Middleware/ExceptionMiddleware.cs index ebd19a57..5773c2ee 100644 --- a/src/PublicApi/Middleware/ExceptionMiddleware.cs +++ b/src/PublicApi/Middleware/ExceptionMiddleware.cs @@ -41,12 +41,14 @@ await context.Response.WriteAsync(new ErrorDetails() Message = duplicationException.Message }.ToString()); } - - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - await context.Response.WriteAsync(new ErrorDetails() + else { - StatusCode = context.Response.StatusCode, - Message = exception.Message - }.ToString()); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await context.Response.WriteAsync(new ErrorDetails() + { + StatusCode = context.Response.StatusCode, + Message = exception.Message + }.ToString()); + } } } diff --git a/src/PublicApi/Program.cs b/src/PublicApi/Program.cs index a96fa99d..08c87eb5 100644 --- a/src/PublicApi/Program.cs +++ b/src/PublicApi/Program.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Text; using BlazorShared; -using BlazorShared.Models; -using MediatR; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; @@ -42,7 +40,8 @@ builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); builder.Services.Configure(builder.Configuration); -builder.Services.AddSingleton(new UriComposer(builder.Configuration.Get())); +var catalogSettings = builder.Configuration.Get() ?? new CatalogSettings(); +builder.Services.AddSingleton(new UriComposer(catalogSettings)); builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); builder.Services.AddScoped(); @@ -74,18 +73,17 @@ builder.Services.AddCors(options => { options.AddPolicy(name: CORS_POLICY, - corsPolicyBuilder => - { - corsPolicyBuilder.WithOrigins(baseUrlConfig.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); - corsPolicyBuilder.AllowAnyMethod(); - corsPolicyBuilder.AllowAnyHeader(); - }); + corsPolicyBuilder => + { + corsPolicyBuilder.WithOrigins(baseUrlConfig!.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); + corsPolicyBuilder.AllowAnyMethod(); + corsPolicyBuilder.AllowAnyHeader(); + }); }); builder.Services.AddControllers(); - -builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(CatalogItem).Assembly)); builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly); +builder.Configuration.AddEnvironmentVariables(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => @@ -176,6 +174,7 @@ app.MapControllers(); app.MapEndpoints(); + app.Logger.LogInformation("LAUNCHING PublicApi"); app.Run(); diff --git a/src/PublicApi/Properties/launchSettings.json b/src/PublicApi/Properties/launchSettings.json index fbed5248..c44d516f 100644 --- a/src/PublicApi/Properties/launchSettings.json +++ b/src/PublicApi/Properties/launchSettings.json @@ -1,13 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:52023", - "sslPort": 44339 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS Express": { "commandName": "IISExpress", @@ -25,6 +16,32 @@ "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 } } } \ No newline at end of file diff --git a/src/PublicApi/PublicApi.csproj b/src/PublicApi/PublicApi.csproj index a6180957..965ea6d2 100644 --- a/src/PublicApi/PublicApi.csproj +++ b/src/PublicApi/PublicApi.csproj @@ -1,36 +1,34 @@  - - net7.0 + Microsoft.eShopWeb.PublicApi 5b662463-1efd-4bae-bde4-befe0be3e8ff Linux ..\.. - disable + enable - - - - - - - - - - - - - - + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + diff --git a/src/PublicApi/appsettings.Docker.json b/src/PublicApi/appsettings.Docker.json index 0aa16e3b..0bf721d6 100644 --- a/src/PublicApi/appsettings.Docker.json +++ b/src/PublicApi/appsettings.Docker.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { - "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;", - "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;" + "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/", diff --git a/src/Web/.config/dotnet-tools.json b/src/Web/.config/dotnet-tools.json index 5060fd65..e3cadb92 100644 --- a/src/Web/.config/dotnet-tools.json +++ b/src/Web/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "6.0.4", + "version": "8.0.0", "commands": [ "dotnet-ef" ] diff --git a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs index 290a6449..fe22ef76 100644 --- a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Ardalis.GuardClauses; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -24,30 +25,30 @@ public LoginModel(SignInManager signInManager, ILogger ExternalLogins { get; set; } + public IList? ExternalLogins { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } [TempData] - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } public class InputModel { [Required] [EmailAddress] - public string Email { get; set; } + public string? Email { get; set; } [Required] [DataType(DataType.Password)] - public string Password { get; set; } + public string? Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } - public async Task OnGetAsync(string returnUrl = null) + public async Task OnGetAsync(string? returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { @@ -64,7 +65,7 @@ public async Task OnGetAsync(string returnUrl = null) ReturnUrl = returnUrl; } - public async Task OnPostAsync(string returnUrl = null) + public async Task OnPostAsync(string? returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); @@ -73,17 +74,18 @@ public async Task OnPostAsync(string returnUrl = null) // 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); + var result = await _signInManager.PasswordSignInAsync(Input!.Email!, Input!.Password!, + false, true); if (result.Succeeded) { _logger.LogInformation("User logged in."); - await TransferAnonymousBasketToUserAsync(Input.Email); + await TransferAnonymousBasketToUserAsync(Input?.Email); return LocalRedirect(returnUrl); } if (result.RequiresTwoFactor) { - return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input?.RememberMe }); } if (result.IsLockedOut) { @@ -101,13 +103,14 @@ public async Task OnPostAsync(string returnUrl = null) return Page(); } - private async Task TransferAnonymousBasketToUserAsync(string userName) + 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); diff --git a/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs index b6700e7e..80bb4670 100644 --- a/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs +++ b/src/Web/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; +using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; @@ -10,7 +7,6 @@ using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Configuration; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account; @@ -32,7 +28,7 @@ public void OnGet() { } - public async Task OnPost(string returnUrl = null) + public async Task OnPost(string? returnUrl = null) { await _signInManager.SignOutAsync(); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); diff --git a/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs index 120c0bf1..f0165fa1 100644 --- a/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -3,6 +3,7 @@ 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; @@ -34,41 +35,41 @@ public RegisterModel( } [BindProperty] - public InputModel Input { get; set; } + public required InputModel Input { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } public class InputModel { [Required] [EmailAddress] [Display(Name = "Email")] - public string Email { get; set; } + 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; } + 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? ConfirmPassword { get; set; } } - public void OnGet(string returnUrl = null) + public void OnGet(string? returnUrl = null) { ReturnUrl = returnUrl; } - public async Task OnPostAsync(string returnUrl = null) + 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); + 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."); @@ -80,7 +81,8 @@ public async Task OnPostAsync(string returnUrl = null) values: new { userId = user.Id, code = code }, protocol: Request.Scheme); - await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", + 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); diff --git a/src/Web/Configuration/ConfigureCoreServices.cs b/src/Web/Configuration/ConfigureCoreServices.cs index f38657db..cf39e9a5 100644 --- a/src/Web/Configuration/ConfigureCoreServices.cs +++ b/src/Web/Configuration/ConfigureCoreServices.cs @@ -4,8 +4,6 @@ using Microsoft.eShopWeb.Infrastructure.Data.Queries; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.Infrastructure.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.eShopWeb.Web.Configuration; @@ -20,7 +18,10 @@ public static IServiceCollection AddCoreServices(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddSingleton(new UriComposer(configuration.Get())); + + var catalogSettings = configuration.Get() ?? new CatalogSettings(); + services.AddSingleton(new UriComposer(catalogSettings)); + services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); services.AddTransient(); diff --git a/src/Web/Configuration/ConfigureWebServices.cs b/src/Web/Configuration/ConfigureWebServices.cs index c3e18225..e282276e 100644 --- a/src/Web/Configuration/ConfigureWebServices.cs +++ b/src/Web/Configuration/ConfigureWebServices.cs @@ -8,7 +8,8 @@ public static class ConfigureWebServices { public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration) { - services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(BasketViewModelService).Assembly)); + services.AddMediatR(cfg => + cfg.RegisterServicesFromAssembly(typeof(BasketViewModelService).Assembly)); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Web/Configuration/RevokeAuthenticationEvents.cs b/src/Web/Configuration/RevokeAuthenticationEvents.cs index 0e291633..09ba74ab 100644 --- a/src/Web/Configuration/RevokeAuthenticationEvents.cs +++ b/src/Web/Configuration/RevokeAuthenticationEvents.cs @@ -22,12 +22,12 @@ public RevokeAuthenticationEvents(IMemoryCache cache, ILogger c.Type == ClaimTypes.Name); + 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)) + if (_cache.TryGetValue($"{userId?.Value}:{identityKey}", out var revokeKeys)) { - _logger.LogDebug($"Access has been revoked for: {userId.Value}."); + _logger.LogDebug($"Access has been revoked for: {userId?.Value}."); context.RejectPrincipal(); await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } diff --git a/src/Web/Controllers/ManageController.cs b/src/Web/Controllers/ManageController.cs index 4f0604ee..d97473c3 100644 --- a/src/Web/Controllers/ManageController.cs +++ b/src/Web/Controllers/ManageController.cs @@ -1,5 +1,6 @@ using System.Text; using System.Text.Encodings.Web; +using Ardalis.GuardClauses; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -40,7 +41,7 @@ public ManageController( } [TempData] - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } [HttpGet] public async Task MyAccount() @@ -119,7 +120,13 @@ public async Task SendVerificationEmail(IndexViewModel model) 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."; @@ -160,7 +167,8 @@ public async Task ChangePassword(ChangePasswordViewModel model) throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + var changePasswordResult = await _userManager + .ChangePasswordAsync(user, model.OldPassword!, model.NewPassword!); if (!changePasswordResult.Succeeded) { AddErrors(changePasswordResult); @@ -209,7 +217,7 @@ public async Task SetPassword(SetPasswordViewModel model) throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword); + var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword!); if (!addPasswordResult.Succeeded) { AddErrors(addPasswordResult); @@ -291,6 +299,10 @@ public async Task RemoveLogin(RemoveLoginViewModel model) { 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) @@ -377,7 +389,7 @@ public async Task EnableAuthenticator() [HttpGet] public IActionResult ShowRecoveryCodes() { - var recoveryCodes = (string[])TempData[RecoveryCodesKey]; + var recoveryCodes = (string[]?)TempData[RecoveryCodesKey]; if (recoveryCodes == null) { return RedirectToAction(nameof(TwoFactorAuthentication)); @@ -405,7 +417,7 @@ public async Task EnableAuthenticator(EnableAuthenticatorViewMode } // Strip spaces and hypens - var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + string verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty) ?? ""; var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); @@ -419,7 +431,7 @@ public async Task EnableAuthenticator(EnableAuthenticatorViewMode 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); + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List(); TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); return RedirectToAction(nameof(ShowRecoveryCodes)); @@ -463,7 +475,7 @@ public async Task GenerateRecoveryCodes() 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); + 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() }; @@ -531,8 +543,8 @@ private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAu unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } - model.SharedKey = FormatKey(unformattedKey); - model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + model.SharedKey = FormatKey(unformattedKey!); + model.AuthenticatorUri = GenerateQrCodeUri(user.Email!, unformattedKey!); } } diff --git a/src/Web/Controllers/OrderController.cs b/src/Web/Controllers/OrderController.cs index f8e40939..5a2e1e1c 100644 --- a/src/Web/Controllers/OrderController.cs +++ b/src/Web/Controllers/OrderController.cs @@ -1,4 +1,5 @@ -using MediatR; +using Ardalis.GuardClauses; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.Web.Features.MyOrders; @@ -20,7 +21,8 @@ public OrderController(IMediator 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); @@ -29,6 +31,7 @@ public async Task MyOrders() [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) diff --git a/src/Web/Controllers/UserController.cs b/src/Web/Controllers/UserController.cs index 6ce18182..79968c18 100644 --- a/src/Web/Controllers/UserController.cs +++ b/src/Web/Controllers/UserController.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; +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; @@ -14,10 +17,19 @@ namespace Microsoft.eShopWeb.Web.Controllers; public class UserController : ControllerBase { private readonly ITokenClaimsService _tokenClaimsService; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + private readonly IMemoryCache _cache; - public UserController(ITokenClaimsService tokenClaimsService) + public UserController(ITokenClaimsService tokenClaimsService, + SignInManager signInManager, + ILogger logger, + IMemoryCache cache) { _tokenClaimsService = tokenClaimsService; + _signInManager = signInManager; + _logger = logger; + _cache = cache; } [HttpGet] @@ -26,9 +38,28 @@ public UserController(ITokenClaimsService tokenClaimsService) 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.IsAuthenticated) + if (claimsPrincipal.Identity == null || claimsPrincipal.Identity.Name == null || !claimsPrincipal.Identity.IsAuthenticated) { return UserInfo.Anonymous; } diff --git a/src/Web/Dockerfile b/src/Web/Dockerfile index 9a5e23d9..a2682100 100644 --- a/src/Web/Dockerfile +++ b/src/Web/Dockerfile @@ -7,7 +7,7 @@ # # RUN COMMAND # docker run --name eshopweb --rm -it -p 5106:5106 web -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app COPY *.sln . @@ -17,7 +17,7 @@ RUN dotnet restore RUN dotnet publish -c Release -o out -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app COPY --from=build /app/src/Web/out ./ diff --git a/src/Web/Extensions/UrlHelperExtensions.cs b/src/Web/Extensions/UrlHelperExtensions.cs index 75ea5988..e643e69f 100644 --- a/src/Web/Extensions/UrlHelperExtensions.cs +++ b/src/Web/Extensions/UrlHelperExtensions.cs @@ -2,7 +2,7 @@ public static class UrlHelperExtensions { - public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + public static string? EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) { return urlHelper.Action( action: "GET", diff --git a/src/Web/Features/MyOrders/GetMyOrders.cs b/src/Web/Features/MyOrders/GetMyOrders.cs index aedfde15..5baf32ca 100644 --- a/src/Web/Features/MyOrders/GetMyOrders.cs +++ b/src/Web/Features/MyOrders/GetMyOrders.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using MediatR; +using MediatR; using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Features.MyOrders; diff --git a/src/Web/Features/MyOrders/GetMyOrdersHandler.cs b/src/Web/Features/MyOrders/GetMyOrdersHandler.cs index a22961ff..df6db4bf 100644 --- a/src/Web/Features/MyOrders/GetMyOrdersHandler.cs +++ b/src/Web/Features/MyOrders/GetMyOrdersHandler.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediatR; +using MediatR; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; @@ -22,20 +18,12 @@ public GetMyOrdersHandler(IReadRepository orderRepository) public async Task> Handle(GetMyOrders request, CancellationToken cancellationToken) { - var specification = new CustomerOrdersWithItemsSpecification(request.UserName); + var specification = new CustomerOrdersSpecification(request.UserName); var orders = await _orderRepository.ListAsync(specification, cancellationToken); return orders.Select(o => new OrderViewModel { OrderDate = o.OrderDate, - OrderItems = o.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 = o.Id, ShippingAddress = o.ShipToAddress, Total = o.Total() diff --git a/src/Web/Features/OrderDetails/GetOrderDetails.cs b/src/Web/Features/OrderDetails/GetOrderDetails.cs index 2cc07213..deb1fb56 100644 --- a/src/Web/Features/OrderDetails/GetOrderDetails.cs +++ b/src/Web/Features/OrderDetails/GetOrderDetails.cs @@ -3,7 +3,7 @@ namespace Microsoft.eShopWeb.Web.Features.OrderDetails; -public class GetOrderDetails : IRequest +public class GetOrderDetails : IRequest { public string UserName { get; set; } public int OrderId { get; set; } diff --git a/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs b/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs index 4fa7e54c..4c11199b 100644 --- a/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs +++ b/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediatR; +using MediatR; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; @@ -9,7 +6,7 @@ namespace Microsoft.eShopWeb.Web.Features.OrderDetails; -public class GetOrderDetailsHandler : IRequestHandler +public class GetOrderDetailsHandler : IRequestHandler { private readonly IReadRepository _orderRepository; @@ -18,18 +15,18 @@ public GetOrderDetailsHandler(IReadRepository orderRepository) _orderRepository = orderRepository; } - public async Task Handle(GetOrderDetails request, + public async Task Handle(GetOrderDetails request, CancellationToken cancellationToken) { var spec = new OrderWithItemsByIdSpec(request.OrderId); - var order = await _orderRepository.GetBySpecAsync(spec, cancellationToken); + var order = await _orderRepository.FirstOrDefaultAsync(spec, cancellationToken); if (order == null) { return null; } - return new OrderViewModel + return new OrderDetailViewModel { OrderDate = order.OrderDate, OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel diff --git a/src/Web/HealthChecks/HomePageHealthCheck.cs b/src/Web/HealthChecks/HomePageHealthCheck.cs index 0579dd78..0896954d 100644 --- a/src/Web/HealthChecks/HomePageHealthCheck.cs +++ b/src/Web/HealthChecks/HomePageHealthCheck.cs @@ -19,8 +19,8 @@ public async Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { - var request = _httpContextAccessor.HttpContext.Request; - string myUrl = request.Scheme + "://" + request.Host.ToString(); + var request = _httpContextAccessor.HttpContext?.Request; + string myUrl = request?.Scheme + "://" + request?.Host.ToString(); var client = new HttpClient(); var response = await client.GetAsync(myUrl); diff --git a/src/Web/Pages/Basket/BasketItemViewModel.cs b/src/Web/Pages/Basket/BasketItemViewModel.cs index 61c77c76..8c3a142f 100644 --- a/src/Web/Pages/Basket/BasketItemViewModel.cs +++ b/src/Web/Pages/Basket/BasketItemViewModel.cs @@ -6,11 +6,12 @@ public class BasketItemViewModel { public int Id { get; set; } public int CatalogItemId { get; set; } - public string ProductName { 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; } + + public string? PictureUrl { get; set; } } diff --git a/src/Web/Pages/Basket/BasketViewModel.cs b/src/Web/Pages/Basket/BasketViewModel.cs index 07b10282..a48ddbde 100644 --- a/src/Web/Pages/Basket/BasketViewModel.cs +++ b/src/Web/Pages/Basket/BasketViewModel.cs @@ -4,7 +4,7 @@ public class BasketViewModel { public int Id { get; set; } public List Items { get; set; } = new List(); - public string BuyerId { get; set; } + public string? BuyerId { get; set; } public decimal Total() { diff --git a/src/Web/Pages/Basket/Checkout.cshtml.cs b/src/Web/Pages/Basket/Checkout.cshtml.cs index 90f6a92f..ee545927 100644 --- a/src/Web/Pages/Basket/Checkout.cshtml.cs +++ b/src/Web/Pages/Basket/Checkout.cshtml.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authorization; +using Ardalis.GuardClauses; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -16,7 +17,7 @@ public class CheckoutModel : PageModel private readonly IBasketService _basketService; private readonly SignInManager _signInManager; private readonly IOrderService _orderService; - private string _username = null; + private string? _username = null; private readonly IBasketViewModelService _basketViewModelService; private readonly IAppLogger _logger; @@ -68,6 +69,7 @@ public async Task OnPost(IEnumerable items) 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); @@ -75,7 +77,7 @@ private async Task SetBasketModelAsync() else { GetOrSetBasketCookieAndUserName(); - BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username); + BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username!); } } diff --git a/src/Web/Pages/Basket/Index.cshtml.cs b/src/Web/Pages/Basket/Index.cshtml.cs index b8b7024b..11e7a69d 100644 --- a/src/Web/Pages/Basket/Index.cshtml.cs +++ b/src/Web/Pages/Basket/Index.cshtml.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Ardalis.GuardClauses; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; @@ -66,11 +67,13 @@ public async Task OnPostUpdate(IEnumerable items) private string GetOrSetBasketCookieAndUserName() { - string userName = null; + Guard.Against.Null(Request.HttpContext.User.Identity, nameof(Request.HttpContext.User.Identity)); + string? userName = null; if (Request.HttpContext.User.Identity.IsAuthenticated) { - return Request.HttpContext.User.Identity.Name; + 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)) diff --git a/src/Web/Pages/Error.cshtml.cs b/src/Web/Pages/Error.cshtml.cs index 532e5a97..dc3bf1e1 100644 --- a/src/Web/Pages/Error.cshtml.cs +++ b/src/Web/Pages/Error.cshtml.cs @@ -7,7 +7,7 @@ namespace Microsoft.eShopWeb.Web.Pages; [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class ErrorModel : PageModel { - public string RequestId { get; set; } + public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); diff --git a/src/Web/Pages/Index.cshtml b/src/Web/Pages/Index.cshtml index cc07e690..e90def0c 100644 --- a/src/Web/Pages/Index.cshtml +++ b/src/Web/Pages/Index.cshtml @@ -5,9 +5,7 @@ }
- - - +
@@ -41,7 +39,7 @@ else {
- @Model.SettingsModel.NoResultsMessage + THERE ARE NO RESULTS THAT MATCH YOUR SEARCH
} diff --git a/src/Web/Pages/Index.cshtml.cs b/src/Web/Pages/Index.cshtml.cs index 1fe38b97..f41ba30a 100644 --- a/src/Web/Pages/Index.cshtml.cs +++ b/src/Web/Pages/Index.cshtml.cs @@ -1,22 +1,19 @@ 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) + public IndexModel(ICatalogViewModelService catalogViewModelService) { _catalogViewModelService = catalogViewModelService; - SettingsModel = options.Value; } - public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); + public required CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) { diff --git a/src/Web/Pages/Shared/Components/BasketComponent/Basket.cs b/src/Web/Pages/Shared/Components/BasketComponent/Basket.cs index 519a335e..986c7b8a 100644 --- a/src/Web/Pages/Shared/Components/BasketComponent/Basket.cs +++ b/src/Web/Pages/Shared/Components/BasketComponent/Basket.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Ardalis.GuardClauses; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopWeb.Infrastructure.Identity; @@ -33,17 +34,18 @@ 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(); + string? anonymousId = GetAnnonymousIdFromCookie(); if (anonymousId == null) return 0; return await _basketService.CountTotalBasketItems(anonymousId); } - private string GetAnnonymousIdFromCookie() + private string? GetAnnonymousIdFromCookie() { if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) { diff --git a/src/Web/Pages/_ViewImports.cshtml b/src/Web/Pages/_ViewImports.cshtml index d85ca5f5..2bf31ee4 100644 --- a/src/Web/Pages/_ViewImports.cshtml +++ b/src/Web/Pages/_ViewImports.cshtml @@ -7,4 +7,3 @@ @using Microsoft.eShopWeb.Infrastructure.Identity @namespace Microsoft.eShopWeb.Web.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, Microsoft.FeatureManagement.AspNetCore diff --git a/src/Web/Program.cs b/src/Web/Program.cs index c892fd4d..9761361d 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,5 +1,6 @@ using System.Net.Mime; using Ardalis.ListStartupServices; +using Azure.Identity; using BlazorAdmin; using BlazorAdmin.Services; using Blazored.LocalStorage; @@ -8,6 +9,7 @@ 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; @@ -16,16 +18,29 @@ using Microsoft.eShopWeb.Web.Configuration; using Microsoft.eShopWeb.Web.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Azure.Identity; -using Microsoft.eShopWeb.Web.Pages; -using Microsoft.FeatureManagement; -using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); - builder.Logging.AddConsole(); -Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); +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(); @@ -43,7 +58,7 @@ .AddDefaultTokenProviders(); builder.Services.AddScoped(); - +builder.Configuration.AddEnvironmentVariables(); builder.Services.AddCoreServices(builder.Configuration); builder.Services.AddWebServices(builder.Configuration); @@ -78,33 +93,6 @@ config.Path = "/allservices"; }); -// Bind configuration "eShopWeb:Settings" section to the Settings object -builder.Services.Configure(builder.Configuration.GetSection("eShopWeb:Settings")); -// 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(); -builder.Services.AddFeatureManagement(); -// Load configuration from Azure App Configuration -if (useAppConfig) -{ - builder.Configuration.AddAzureAppConfiguration(options => - { - options.Connect(new Uri(builder.Configuration["AppConfigEndpoint"]), new DefaultAzureCredential()) - .ConfigureRefresh(refresh => - { - // Default cache expiration is 30 seconds - refresh.Register("eShopWeb:Settings:NoResultsMessage").SetCacheExpiration(TimeSpan.FromSeconds(10)); - }) - .UseFeatureFlags(featureFlagOptions => - { - // Default cache expiration is 30 seconds - featureFlagOptions.CacheExpirationInterval = TimeSpan.FromSeconds(10); - }); - }); -} - // blazor configuration var configSection = builder.Configuration.GetRequiredSection(BaseUrlConfiguration.CONFIG_NAME); builder.Services.Configure(configSection); @@ -113,7 +101,7 @@ // Blazor Admin Required Services for Prerendering builder.Services.AddScoped(s => new HttpClient { - BaseAddress = new Uri(baseUrlConfig.WebBase) + BaseAddress = new Uri(baseUrlConfig!.WebBase) }); // add blazor services @@ -127,12 +115,6 @@ 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..."); diff --git a/src/Web/Properties/launchSettings.json b/src/Web/Properties/launchSettings.json index edbd81b6..4dfb1e9d 100644 --- a/src/Web/Properties/launchSettings.json +++ b/src/Web/Properties/launchSettings.json @@ -22,10 +22,7 @@ "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}" + "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, diff --git a/src/Web/Services/BasketViewModelService.cs b/src/Web/Services/BasketViewModelService.cs index 16d4eff5..658d5712 100644 --- a/src/Web/Services/BasketViewModelService.cs +++ b/src/Web/Services/BasketViewModelService.cs @@ -28,7 +28,7 @@ public BasketViewModelService(IRepository basketRepository, public async Task GetOrCreateBasketForUser(string userName) { var basketSpec = new BasketWithItemsSpecification(userName); - var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); + var basket = (await _basketRepository.FirstOrDefaultAsync(basketSpec)); if (basket == null) { diff --git a/src/Web/Services/CachedCatalogViewModelService.cs b/src/Web/Services/CachedCatalogViewModelService.cs index d190d438..e7a506b4 100644 --- a/src/Web/Services/CachedCatalogViewModelService.cs +++ b/src/Web/Services/CachedCatalogViewModelService.cs @@ -21,30 +21,30 @@ public CachedCatalogViewModelService(IMemoryCache cache, public async Task> GetBrands() { - return await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry => + 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 => + 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 => + return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetTypes(); - }); + })) ?? new List(); } } diff --git a/src/Web/Services/CatalogItemViewModelService.cs b/src/Web/Services/CatalogItemViewModelService.cs index 7c66d617..4b63ad09 100644 --- a/src/Web/Services/CatalogItemViewModelService.cs +++ b/src/Web/Services/CatalogItemViewModelService.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Web.Interfaces; @@ -18,7 +18,11 @@ public CatalogItemViewModelService(IRepository catalogItemRepositor public async Task UpdateCatalogItem(CatalogItemViewModel viewModel) { var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id); - existingCatalogItem.UpdateDetails(viewModel.Name, existingCatalogItem.Description, viewModel.Price); + + Guard.Against.Null(existingCatalogItem, nameof(existingCatalogItem)); + + CatalogItem.CatalogItemDetails details = new(viewModel.Name, existingCatalogItem.Description, viewModel.Price); + existingCatalogItem.UpdateDetails(details); await _catalogItemRepository.UpdateAsync(existingCatalogItem); } } diff --git a/src/Web/SlugifyParameterTransformer.cs b/src/Web/SlugifyParameterTransformer.cs index c0e18cd9..d2d1c568 100644 --- a/src/Web/SlugifyParameterTransformer.cs +++ b/src/Web/SlugifyParameterTransformer.cs @@ -5,11 +5,13 @@ namespace Microsoft.eShopWeb.Web; public class SlugifyParameterTransformer : IOutboundParameterTransformer { - public string TransformOutbound(object value) + 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(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower(); + return Regex.Replace(str, "([a-z])([A-Z])", "$1-$2").ToLower(); } } diff --git a/src/Web/ViewModels/Account/LoginViewModel.cs b/src/Web/ViewModels/Account/LoginViewModel.cs index 2230bb7d..846407a4 100644 --- a/src/Web/ViewModels/Account/LoginViewModel.cs +++ b/src/Web/ViewModels/Account/LoginViewModel.cs @@ -6,11 +6,11 @@ public class LoginViewModel { [Required] [EmailAddress] - public string Email { get; set; } + public string? Email { get; set; } [Required] [DataType(DataType.Password)] - public string Password { get; set; } + public string? Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } diff --git a/src/Web/ViewModels/Account/LoginWith2faViewModel.cs b/src/Web/ViewModels/Account/LoginWith2faViewModel.cs index f09954be..fc159af4 100644 --- a/src/Web/ViewModels/Account/LoginWith2faViewModel.cs +++ b/src/Web/ViewModels/Account/LoginWith2faViewModel.cs @@ -8,7 +8,7 @@ public class LoginWith2faViewModel [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; } + public string? TwoFactorCode { get; set; } [Display(Name = "Remember this machine")] public bool RememberMachine { get; set; } diff --git a/src/Web/ViewModels/Account/RegisterViewModel.cs b/src/Web/ViewModels/Account/RegisterViewModel.cs index f73b17e3..f8e3c0d6 100644 --- a/src/Web/ViewModels/Account/RegisterViewModel.cs +++ b/src/Web/ViewModels/Account/RegisterViewModel.cs @@ -7,16 +7,16 @@ public class RegisterViewModel [Required] [EmailAddress] [Display(Name = "Email")] - public string Email { get; set; } + 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; } + 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? ConfirmPassword { get; set; } } diff --git a/src/Web/ViewModels/Account/ResetPasswordViewModel.cs b/src/Web/ViewModels/Account/ResetPasswordViewModel.cs index 6bf3979a..9ccd3669 100644 --- a/src/Web/ViewModels/Account/ResetPasswordViewModel.cs +++ b/src/Web/ViewModels/Account/ResetPasswordViewModel.cs @@ -6,17 +6,17 @@ public class ResetPasswordViewModel { [Required] [EmailAddress] - public string Email { get; set; } + 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; } + 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? ConfirmPassword { get; set; } - public string Code { get; set; } + public string? Code { get; set; } } diff --git a/src/Web/ViewModels/CatalogIndexViewModel.cs b/src/Web/ViewModels/CatalogIndexViewModel.cs index b3457bd0..69e09e39 100644 --- a/src/Web/ViewModels/CatalogIndexViewModel.cs +++ b/src/Web/ViewModels/CatalogIndexViewModel.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Rendering; namespace Microsoft.eShopWeb.Web.ViewModels; public class CatalogIndexViewModel { - public List CatalogItems { get; set; } - public List Brands { get; set; } - public List Types { get; set; } + 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; } + public PaginationInfoViewModel? PaginationInfo { get; set; } } diff --git a/src/Web/ViewModels/CatalogItemViewModel.cs b/src/Web/ViewModels/CatalogItemViewModel.cs index 6b92830c..5a2f9ede 100644 --- a/src/Web/ViewModels/CatalogItemViewModel.cs +++ b/src/Web/ViewModels/CatalogItemViewModel.cs @@ -3,7 +3,7 @@ public class CatalogItemViewModel { public int Id { get; set; } - public string Name { get; set; } - public string PictureUri { get; set; } + public string? Name { get; set; } + public string? PictureUri { get; set; } public decimal Price { get; set; } } diff --git a/src/Web/ViewModels/File/FileViewModel.cs b/src/Web/ViewModels/File/FileViewModel.cs index bae90288..0a0a76c0 100644 --- a/src/Web/ViewModels/File/FileViewModel.cs +++ b/src/Web/ViewModels/File/FileViewModel.cs @@ -2,7 +2,7 @@ public class FileViewModel { - public string FileName { get; set; } - public string Url { get; set; } - public string DataBase64 { get; set; } + public string? FileName { get; set; } + public string? Url { get; set; } + public string? DataBase64 { get; set; } } diff --git a/src/Web/ViewModels/Manage/ChangePasswordViewModel.cs b/src/Web/ViewModels/Manage/ChangePasswordViewModel.cs index 0739cab1..bfe5df97 100644 --- a/src/Web/ViewModels/Manage/ChangePasswordViewModel.cs +++ b/src/Web/ViewModels/Manage/ChangePasswordViewModel.cs @@ -7,18 +7,18 @@ public class ChangePasswordViewModel [Required] [DataType(DataType.Password)] [Display(Name = "Current password")] - public string OldPassword { get; set; } + 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; } + 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? ConfirmPassword { get; set; } - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } } diff --git a/src/Web/ViewModels/Manage/EnableAuthenticatorViewModel.cs b/src/Web/ViewModels/Manage/EnableAuthenticatorViewModel.cs index f59f3641..840858c1 100644 --- a/src/Web/ViewModels/Manage/EnableAuthenticatorViewModel.cs +++ b/src/Web/ViewModels/Manage/EnableAuthenticatorViewModel.cs @@ -10,11 +10,11 @@ public class EnableAuthenticatorViewModel [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; } + public string? Code { get; set; } [BindNever] - public string SharedKey { get; set; } + public string? SharedKey { get; set; } [BindNever] - public string AuthenticatorUri { get; set; } + public string? AuthenticatorUri { get; set; } } diff --git a/src/Web/ViewModels/Manage/ExternalLoginsViewModel.cs b/src/Web/ViewModels/Manage/ExternalLoginsViewModel.cs index 9dfa9646..9f861355 100644 --- a/src/Web/ViewModels/Manage/ExternalLoginsViewModel.cs +++ b/src/Web/ViewModels/Manage/ExternalLoginsViewModel.cs @@ -6,8 +6,8 @@ namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class ExternalLoginsViewModel { - public IList CurrentLogins { get; set; } - public IList OtherLogins { get; set; } + public IList? CurrentLogins { get; set; } + public IList? OtherLogins { get; set; } public bool ShowRemoveButton { get; set; } - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } } diff --git a/src/Web/ViewModels/Manage/IndexViewModel.cs b/src/Web/ViewModels/Manage/IndexViewModel.cs index 6be25539..212ecc99 100644 --- a/src/Web/ViewModels/Manage/IndexViewModel.cs +++ b/src/Web/ViewModels/Manage/IndexViewModel.cs @@ -4,17 +4,17 @@ namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class IndexViewModel { - public string Username { get; set; } + public string? Username { get; set; } public bool IsEmailConfirmed { get; set; } [Required] [EmailAddress] - public string Email { get; set; } + public string? Email { get; set; } [Phone] [Display(Name = "Phone number")] - public string PhoneNumber { get; set; } + public string? PhoneNumber { get; set; } - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } } diff --git a/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs b/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs index 2cd871cd..78ddac16 100644 --- a/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs +++ b/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs @@ -1,7 +1,11 @@ -namespace Microsoft.eShopWeb.Web.ViewModels.Manage; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class RemoveLoginViewModel { - public string LoginProvider { get; set; } - public string ProviderKey { get; set; } + [Required] + public string LoginProvider { get; set; } = string.Empty; + [Required] + public string ProviderKey { get; set; } = string.Empty; } diff --git a/src/Web/ViewModels/Manage/SetPasswordViewModel.cs b/src/Web/ViewModels/Manage/SetPasswordViewModel.cs index b0c2897f..cdf74e9d 100644 --- a/src/Web/ViewModels/Manage/SetPasswordViewModel.cs +++ b/src/Web/ViewModels/Manage/SetPasswordViewModel.cs @@ -8,12 +8,12 @@ public class SetPasswordViewModel [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; } + 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? ConfirmPassword { get; set; } - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } } diff --git a/src/Web/ViewModels/Manage/ShowRecoveryCodesViewModel.cs b/src/Web/ViewModels/Manage/ShowRecoveryCodesViewModel.cs index 6a2b561a..a9eb4da5 100644 --- a/src/Web/ViewModels/Manage/ShowRecoveryCodesViewModel.cs +++ b/src/Web/ViewModels/Manage/ShowRecoveryCodesViewModel.cs @@ -2,6 +2,6 @@ public class ShowRecoveryCodesViewModel { - public string[] RecoveryCodes { get; set; } + public string[]? RecoveryCodes { get; set; } } diff --git a/src/Web/ViewModels/OrderDetailViewModel.cs b/src/Web/ViewModels/OrderDetailViewModel.cs new file mode 100644 index 00000000..18a3aa0d --- /dev/null +++ b/src/Web/ViewModels/OrderDetailViewModel.cs @@ -0,0 +1,6 @@ +namespace Microsoft.eShopWeb.Web.ViewModels; + +public class OrderDetailViewModel : OrderViewModel +{ + public List OrderItems { get; set; } = new(); +} diff --git a/src/Web/ViewModels/OrderItemViewModel.cs b/src/Web/ViewModels/OrderItemViewModel.cs index 6db3962b..45d5000e 100644 --- a/src/Web/ViewModels/OrderItemViewModel.cs +++ b/src/Web/ViewModels/OrderItemViewModel.cs @@ -3,9 +3,9 @@ public class OrderItemViewModel { public int ProductId { get; set; } - public string ProductName { 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; } + public string? PictureUrl { get; set; } } diff --git a/src/Web/ViewModels/OrderViewModel.cs b/src/Web/ViewModels/OrderViewModel.cs index 362a928c..d34866aa 100644 --- a/src/Web/ViewModels/OrderViewModel.cs +++ b/src/Web/ViewModels/OrderViewModel.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.Web.ViewModels; @@ -12,6 +10,5 @@ public class OrderViewModel public DateTimeOffset OrderDate { get; set; } public decimal Total { get; set; } public string Status => DEFAULT_STATUS; - public Address ShippingAddress { get; set; } - public List OrderItems { get; set; } = new List(); + public Address? ShippingAddress { get; set; } } diff --git a/src/Web/ViewModels/PaginationInfoViewModel.cs b/src/Web/ViewModels/PaginationInfoViewModel.cs index 1d1dd725..6d37c1f1 100644 --- a/src/Web/ViewModels/PaginationInfoViewModel.cs +++ b/src/Web/ViewModels/PaginationInfoViewModel.cs @@ -6,6 +6,6 @@ public class PaginationInfoViewModel 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; } + public string? Previous { get; set; } + public string? Next { get; set; } } diff --git a/src/Web/Views/Manage/ManageNavPages.cs b/src/Web/Views/Manage/ManageNavPages.cs index f64885dd..244a707e 100644 --- a/src/Web/Views/Manage/ManageNavPages.cs +++ b/src/Web/Views/Manage/ManageNavPages.cs @@ -27,7 +27,7 @@ public static class ManageNavPages public static string PageNavClass(ViewContext viewContext, string page) { var activePage = viewContext.ViewData["ActivePage"] as string; - return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : string.Empty; } public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage; diff --git a/src/Web/Views/Manage/ShowRecoverCodes.cshtml b/src/Web/Views/Manage/ShowRecoverCodes.cshtml index ed6bc957..0d012e2f 100644 --- a/src/Web/Views/Manage/ShowRecoverCodes.cshtml +++ b/src/Web/Views/Manage/ShowRecoverCodes.cshtml @@ -16,10 +16,13 @@
- @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + @if (Model.RecoveryCodes != null) { - @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } }
-© 2021 GitHub, Inc. \ No newline at end of file +© 2023 GitHub, Inc. \ No newline at end of file diff --git a/src/Web/Views/Order/Detail.cshtml b/src/Web/Views/Order/Detail.cshtml index cb5db3fd..c5eb47b7 100644 --- a/src/Web/Views/Order/Detail.cshtml +++ b/src/Web/Views/Order/Detail.cshtml @@ -1,4 +1,4 @@ -@model OrderViewModel +@model OrderDetailViewModel @{ ViewData["Title"] = "My Order History"; } @@ -30,15 +30,15 @@
-
@Model.ShippingAddress.Street
+
@Model.ShippingAddress?.Street
-
@Model.ShippingAddress.City
+
@Model.ShippingAddress?.City
-
@Model.ShippingAddress.Country
+
@Model.ShippingAddress?.Country
diff --git a/src/Web/Views/Shared/_LoginPartial.cshtml b/src/Web/Views/Shared/_LoginPartial.cshtml index f9862afd..0e52feae 100644 --- a/src/Web/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/Views/Shared/_LoginPartial.cshtml @@ -1,4 +1,4 @@ -@if (Context.User.Identity != null && Context.User.Identity.IsAuthenticated) +@if (Context!.User!.Identity!.IsAuthenticated) {
diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 7d648fab..6ed273bf 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -1,8 +1,7 @@  - - net7.0 - disable + + enable enable Microsoft.eShopWeb.Web aspnet-Web2-1FA3F72E-E7E3-4360-9E49-1CCCD7FE85F7 @@ -14,28 +13,27 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + diff --git a/src/Web/appsettings.Docker.json b/src/Web/appsettings.Docker.json index bac53d64..07ea75ea 100644 --- a/src/Web/appsettings.Docker.json +++ b/src/Web/appsettings.Docker.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { - "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;", - "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;" + "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/", diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index d7d44636..c2bc6592 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -16,13 +16,5 @@ "System": "Warning" }, "AllowedHosts": "*" - }, - - "eShopWeb": { - "Settings": { - "NoResultsMessage": "THERE ARE NO RESULTS THAT MATCH YOUR SEARCH" - } - }, - "UseAppConfig": false, - "AppConfigEndpoint": "{appconfig-endpoint}" -} \ No newline at end of file + } +} diff --git a/src/Web/libman.json b/src/Web/libman.json index ac8c0f95..1343cb4d 100644 --- a/src/Web/libman.json +++ b/src/Web/libman.json @@ -3,11 +3,11 @@ "defaultProvider": "cdnjs", "libraries": [ { - "library": "jquery@3.3.1", + "library": "jquery@3.6.3", "destination": "wwwroot/lib/jquery/" }, { - "library": "twitter-bootstrap@3.3.7", + "library": "twitter-bootstrap@3.4.1", "files": [ "css/bootstrap.css", "css/bootstrap.css.map", @@ -19,11 +19,11 @@ "destination": "wwwroot/lib/bootstrap/dist/" }, { - "library": "jquery-validation-unobtrusive@3.2.10", + "library": "jquery-validation-unobtrusive@4.0.0", "destination": "wwwroot/lib/jquery-validation-unobtrusive/" }, { - "library": "jquery-validate@1.17.0", + "library": "jquery-validate@1.19.5", "destination": "wwwroot/lib/jquery-validate/", "files": [ "jquery.validate.min.js", @@ -35,7 +35,7 @@ "destination": "wwwroot/lib/toastr/" }, { - "library": "aspnet-signalr@1.0.3", + "library": "aspnet-signalr@1.0.27", "files": [ "signalr.js", "signalr.min.js" @@ -43,4 +43,4 @@ "destination": "wwwroot/lib/@aspnet/signalr/dist/browser/" } ] -} \ No newline at end of file +} diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index b80bd24d..e703b4d5 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -1,10 +1,9 @@  - - net7.0 + Microsoft.eShopWeb.FunctionalTests false - disable + enable enable @@ -15,14 +14,11 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - + + + + + diff --git a/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs b/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs index 72d1f23c..88b37bf1 100644 --- a/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs +++ b/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs @@ -23,7 +23,7 @@ public OrderIndexOnGet(TestApplication factory) public async Task ReturnsRedirectGivenAnonymousUser() { var response = await Client.GetAsync("/order/my-orders"); - var redirectLocation = response.Headers.Location.OriginalString; + var redirectLocation = response!.Headers.Location!.OriginalString; Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Contains("/Account/Login", redirectLocation); diff --git a/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs b/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs index dd19a714..be2dbb0c 100644 --- a/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs +++ b/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs @@ -45,6 +45,6 @@ public async Task RedirectsToLoginIfNotAuthenticated() formContent = new FormUrlEncodedContent(keyValues); var postResponse2 = await Client.PostAsync("/Basket/Checkout", formContent); - Assert.Contains("/Identity/Account/Login", postResponse2.RequestMessage.RequestUri.ToString()); + Assert.Contains("/Identity/Account/Login", postResponse2!.RequestMessage!.RequestUri!.ToString()!); } } diff --git a/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs b/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs index 4657d77d..8d6c0be1 100644 --- a/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs +++ b/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs @@ -62,7 +62,7 @@ public async Task SucessfullyPay() var checkOutResponse = await Client.PostAsync("/basket/checkout", checkOutContent); var stringCheckOutResponse = await checkOutResponse.Content.ReadAsStringAsync(); - Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage.RequestUri.ToString()); + Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage!.RequestUri!.ToString()); Assert.Contains("Thanks for your Order!", stringCheckOutResponse); } } diff --git a/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs b/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs index cd364589..1f68da7e 100644 --- a/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs +++ b/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs @@ -52,7 +52,7 @@ public async Task OnPostUpdateTo50Successfully() var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync(); - Assert.Contains("/basket/update", updateResponse.RequestMessage.RequestUri.ToString()); + Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!); decimal expectedTotalAmount = 416.50M; Assert.Contains(expectedTotalAmount.ToString("N2"), stringUpdateResponse); } @@ -92,7 +92,7 @@ public async Task OnPostUpdateTo0EmptyBasket() var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync(); - Assert.Contains("/basket/update", updateResponse.RequestMessage.RequestUri.ToString()); + Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!); Assert.Contains("Basket is empty", stringUpdateResponse); } } diff --git a/tests/FunctionalTests/Web/WebPageHelpers.cs b/tests/FunctionalTests/Web/WebPageHelpers.cs index d858bfbb..1259c9ee 100644 --- a/tests/FunctionalTests/Web/WebPageHelpers.cs +++ b/tests/FunctionalTests/Web/WebPageHelpers.cs @@ -22,6 +22,6 @@ private static string RegexSearch(string regexpression, string input) { var regex = new Regex(regexpression); var match = regex.Match(input); - return match.Groups.Values.LastOrDefault().Value; + return match!.Groups!.Values!.LastOrDefault()!.Value; } } diff --git a/tests/FunctionalTests/Web/WebTestFixture.cs b/tests/FunctionalTests/Web/WebTestFixture.cs index 55b2e741..d9bb4914 100644 --- a/tests/FunctionalTests/Web/WebTestFixture.cs +++ b/tests/FunctionalTests/Web/WebTestFixture.cs @@ -23,6 +23,16 @@ protected override IHost CreateHost(IHostBuilder builder) // 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 diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index c4acd73e..0923dc66 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -1,17 +1,20 @@  - - net7.0 + Microsoft.eShopWeb.IntegrationTests false - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all runtime; build; native; contentfiles; analyzers diff --git a/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync.cs b/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync.cs index a7d3e56d..77e91900 100644 --- a/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync.cs +++ b/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetByIdWithItemsAsync.cs @@ -49,7 +49,7 @@ public async Task GetOrderAndItemsByOrderIdWhenMultipleOrdersPresent() //Act var spec = new OrderWithItemsByIdSpec(secondOrderId); - var orderFromRepo = await _orderRepository.GetBySpecAsync(spec); + var orderFromRepo = await _orderRepository.FirstOrDefaultAsync(spec); //Assert Assert.Equal(secondOrderId, orderFromRepo.Id); diff --git a/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs b/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs index 03a969c7..62550e6f 100644 --- a/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs @@ -7,29 +7,28 @@ using Microsoft.eShopWeb.PublicApi.AuthEndpoints; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace PublicApiIntegrationTests.AuthEndpoints +namespace PublicApiIntegrationTests.AuthEndpoints; + +[TestClass] +public class AuthenticateEndpoint { - [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) { - [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() { - 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(); + 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); - } + Assert.AreEqual(expectedResult, model!.Result); } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs index 5882db04..9baefdec 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs @@ -4,29 +4,28 @@ using System.Net; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.CatalogItemEndpoints +namespace PublicApiIntegrationTests.CatalogItemEndpoints; + +[TestClass] +public class CatalogItemGetByIdEndpointTest { - [TestClass] - public class CatalogItemGetByIdEndpointTest + [TestMethod] + public async Task ReturnsItemGivenValidId() { - [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(); + 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); - } + 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"); + [TestMethod] + public async Task ReturnsNotFoundGivenInvalidId() + { + var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0"); - Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); - } + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs index 1040463c..54701115 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs @@ -2,48 +2,71 @@ 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 +namespace PublicApiIntegrationTests.CatalogItemEndpoints; + +[TestClass] +public class CatalogItemListPagedEndpoint { - [TestClass] - public class CatalogItemListPagedEndpoint + [TestMethod] + public async Task ReturnsFirst10CatalogItems() { - [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(); + 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()); - } + Assert.AreEqual(10, model!.CatalogItems.Count()); + } - [TestMethod] - public async Task ReturnsCorrectCatalogItemsGivenPageIndex1() - { + [TestMethod] + public async Task ReturnsCorrectCatalogItemsGivenPageIndex1() + { - var pageSize = 10; - var pageIndex = 1; + 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 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 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); + var totalExpected = totalItem - (pageSize * pageIndex); - Assert.AreEqual(totalExpected, model2.CatalogItems.Count()); + 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); } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs index a85923d0..6c5d79e4 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs @@ -8,62 +8,61 @@ using System.Text.Json; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.AuthEndpoints +namespace PublicApiIntegrationTests.AuthEndpoints; + +[TestClass] +public class CreateCatalogItemEndpointTest { - [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; + 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); + [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); - } + 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(); + [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); - } + 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() + private StringContent GetValidNewItemJson() + { + var request = new CreateCatalogItemRequest() { - 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"); + CatalogBrandId = _testBrandId, + CatalogTypeId = _testTypeId, + Description = _testDescription, + Name = _testName, + Price = _testPrice + }; + var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - return jsonContent; - } + return jsonContent; } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs index f41976ec..98c82123 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs @@ -5,34 +5,33 @@ using System.Net.Http.Headers; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.CatalogItemEndpoints +namespace PublicApiIntegrationTests.CatalogItemEndpoints; + +[TestClass] +public class DeleteCatalogItemEndpointTest { - [TestClass] - public class DeleteCatalogItemEndpointTest + [TestMethod] + public async Task ReturnsSuccessGivenValidIdAndAdminUserToken() { - [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(); + 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); - } + 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"); + [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); - } + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/PublicApiIntegrationTests/ProgramTest.cs b/tests/PublicApiIntegrationTests/ProgramTest.cs index ca922346..3f13136f 100644 --- a/tests/PublicApiIntegrationTests/ProgramTest.cs +++ b/tests/PublicApiIntegrationTests/ProgramTest.cs @@ -2,26 +2,25 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net.Http; -namespace PublicApiIntegrationTests +namespace PublicApiIntegrationTests; + +[TestClass] +public class ProgramTest { - [TestClass] - public class ProgramTest - { - private static WebApplicationFactory _application; + private static WebApplicationFactory _application = new(); - public static HttpClient NewClient + public static HttpClient NewClient + { + get { - get - { - return _application.CreateClient(); - } + return _application.CreateClient(); } + } - [AssemblyInitialize] - public static void AssemblyInitialize(TestContext _) - { - _application = new WebApplicationFactory(); + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext _) + { + _application = new WebApplicationFactory(); - } } } diff --git a/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj b/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj index 9d7ea094..467bc1f1 100644 --- a/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj +++ b/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj @@ -1,7 +1,6 @@ - - net7.0 + enable false @@ -20,11 +19,14 @@ - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/UnitTests/ApplicationCore/Extensions/TestParent.cs b/tests/UnitTests/ApplicationCore/Extensions/TestParent.cs index bd20e36b..ec791b41 100644 --- a/tests/UnitTests/ApplicationCore/Extensions/TestParent.cs +++ b/tests/UnitTests/ApplicationCore/Extensions/TestParent.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions; @@ -9,12 +6,22 @@ public class TestParent : IEquatable { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } - public IEnumerable Children { get; set; } + public IEnumerable? Children { get; set; } - public bool Equals([AllowNull] TestParent other) => - other?.Id == Id && other?.Name == Name && - (other?.Children is null && Children is null || - (other?.Children?.Zip(Children)?.All(t => t.First?.Equals(t.Second) ?? false) ?? false)); + 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; + } } diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/AddItemToBasket.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/AddItemToBasket.cs index 7937d082..75e1744e 100644 --- a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/AddItemToBasket.cs +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/AddItemToBasket.cs @@ -1,9 +1,10 @@ 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 Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; @@ -11,33 +12,35 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes public class AddItemToBasket { private readonly string _buyerId = "Test buyerId"; - private readonly Mock> _mockBasketRepo = new(); + 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, It.IsAny(), It.IsAny()); - _mockBasketRepo.Setup(x => x.GetBySpecAsync(It.IsAny(), default)).ReturnsAsync(basket); + basket.AddItem(1, 1.5m); - var basketService = new BasketService(_mockBasketRepo.Object, null); + _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(basket); + + var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m); - _mockBasketRepo.Verify(x => x.GetBySpecAsync(It.IsAny(), default), Times.Once); + await _mockBasketRepo.Received().FirstOrDefaultAsync(Arg.Any(), default); } [Fact] public async Task InvokesBasketRepositoryUpdateAsyncOnce() { var basket = new Basket(_buyerId); - basket.AddItem(1, It.IsAny(), It.IsAny()); - _mockBasketRepo.Setup(x => x.GetBySpecAsync(It.IsAny(), default)).ReturnsAsync(basket); + basket.AddItem(1, 1.1m, 1); + _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(basket); - var basketService = new BasketService(_mockBasketRepo.Object, null); + var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m); - _mockBasketRepo.Verify(x => x.UpdateAsync(basket, default), Times.Once); + await _mockBasketRepo.Received().UpdateAsync(basket, default); } } diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs index 01eff048..0856d3d2 100644 --- a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/DeleteBasket.cs @@ -2,7 +2,8 @@ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; -using Moq; +//using Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; @@ -10,20 +11,21 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes public class DeleteBasket { private readonly string _buyerId = "Test buyerId"; - private readonly Mock> _mockBasketRepo = new(); + 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, It.IsAny(), It.IsAny()); - basket.AddItem(2, It.IsAny(), It.IsAny()); - _mockBasketRepo.Setup(x => x.GetByIdAsync(It.IsAny(), default)) - .ReturnsAsync(basket); - var basketService = new BasketService(_mockBasketRepo.Object, null); + 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(It.IsAny()); + await basketService.DeleteBasketAsync(1); - _mockBasketRepo.Verify(x => x.DeleteAsync(It.IsAny(), default), Times.Once); + await _mockBasketRepo.Received().DeleteAsync(Arg.Any(), default); } } diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs index 93d46084..ad873fe2 100644 --- a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs @@ -4,7 +4,7 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests; @@ -15,35 +15,36 @@ public class TransferBasket 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 Mock> _mockBasketRepo = new(); + private readonly IRepository _mockBasketRepo = Substitute.For>(); + private readonly IAppLogger _mockLogger = Substitute.For>(); - [Fact] - public async Task ThrowsGivenNullAnonymousId() + public class Results { - var basketService = new BasketService(null, null); - - await Assert.ThrowsAsync(async () => await basketService.TransferBasketAsync(null, "steve")); + 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 ThrowsGivenNullUserId() + [Fact] + public async Task InvokesBasketRepositoryFirstOrDefaultAsyncOnceIfAnonymousBasketNotExists() { - var basketService = new BasketService(null, null); + var anonymousBasket = null as Basket; + var userBasket = new Basket(_existentUserBasketBuyerId); + + var results = new Results(anonymousBasket) + .Then(userBasket); - await Assert.ThrowsAsync(async () => await basketService.TransferBasketAsync("abcdefg", null)); - } - [Fact] - public async Task InvokesBasketRepositoryFirstOrDefaultAsyncOnceIfAnonymousBasketNotExists() - { - var anonymousBasket = null as Basket; - var userBasket = new Basket(_existentUserBasketBuyerId); - _mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny(), default)) - .ReturnsAsync(anonymousBasket) - .ReturnsAsync(userBasket); - var basketService = new BasketService(_mockBasketRepo.Object, null); + _mockBasketRepo.FirstOrDefaultAsync(Arg.Any(), default).Returns(x => results.Next()); + var basketService = new BasketService(_mockBasketRepo, _mockLogger); await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); - _mockBasketRepo.Verify(x => x.GetBySpecAsync(It.IsAny(), default), Times.Once); + await _mockBasketRepo.Received().FirstOrDefaultAsync(Arg.Any(), default); } [Fact] @@ -55,12 +56,15 @@ public async Task TransferAnonymousBasketItemsWhilePreservingExistingUserBasketI var userBasket = new Basket(_existentUserBasketBuyerId); userBasket.AddItem(1, 10, 4); userBasket.AddItem(2, 99, 3); - _mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny(), default)) - .ReturnsAsync(anonymousBasket) - .ReturnsAsync(userBasket); - var basketService = new BasketService(_mockBasketRepo.Object, null); + + 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); - _mockBasketRepo.Verify(x => x.UpdateAsync(userBasket, default), Times.Once); + 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); @@ -72,13 +76,15 @@ public async Task RemovesAnonymousBasketAfterUpdatingUserBasket() { var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); var userBasket = new Basket(_existentUserBasketBuyerId); - _mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny(), default)) - .ReturnsAsync(anonymousBasket) - .ReturnsAsync(userBasket); - var basketService = new BasketService(_mockBasketRepo.Object, null); + + 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); - _mockBasketRepo.Verify(x => x.UpdateAsync(userBasket, default), Times.Once); - _mockBasketRepo.Verify(x => x.DeleteAsync(anonymousBasket, default), Times.Once); + await _mockBasketRepo.Received().UpdateAsync(userBasket, default); + await _mockBasketRepo.Received().DeleteAsync(anonymousBasket, default); } [Fact] @@ -86,11 +92,13 @@ public async Task CreatesNewUserBasketIfNotExists() { var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); var userBasket = null as Basket; - _mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny(), default)) - .ReturnsAsync(anonymousBasket) - .ReturnsAsync(userBasket); - var basketService = new BasketService(_mockBasketRepo.Object, null); + + 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); - _mockBasketRepo.Verify(x => x.AddAsync(It.Is(x => x.BuyerId == _nonexistentUserBasketBuyerId), default), Times.Once); + await _mockBasketRepo.Received().AddAsync(Arg.Is(x => x.BuyerId == _nonexistentUserBasketBuyerId), default); } } diff --git a/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs index d2e342ac..009bc84e 100644 --- a/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs +++ b/tests/UnitTests/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -2,7 +2,7 @@ using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; @@ -58,18 +58,18 @@ public void MatchesNoBasketsIfBuyerIdNotPresent() public List GetTestBasketCollection() { - var basket1Mock = new Mock(_buyerId); - basket1Mock.SetupGet(s => s.Id).Returns(1); - var basket2Mock = new Mock(_buyerId); - basket2Mock.SetupGet(s => s.Id).Returns(2); - var basket3Mock = new Mock(_buyerId); - basket3Mock.SetupGet(s => s.Id).Returns(_testBasketId); + 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.Object, - basket2Mock.Object, - basket3Mock.Object + basket1Mock, + basket2Mock, + basket3Mock }; } } diff --git a/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs index c3d38289..5ac89a24 100644 --- a/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs +++ b/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterPaginatedSpecification.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Entities; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; @@ -12,9 +10,7 @@ public void ReturnsAllCatalogItems() { var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterPaginatedSpecification(0, 10, null, null); - var result = GetTestCollection() - .AsQueryable() - .Where(spec.WhereExpressions.FirstOrDefault().Filter); + var result = spec.Evaluate(GetTestCollection()); Assert.NotNull(result); Assert.Equal(4, result.ToList().Count); @@ -25,9 +21,7 @@ public void Returns2CatalogItemsWithSameBrandAndTypeId() { var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterPaginatedSpecification(0, 10, 1, 1); - var result = GetTestCollection() - .AsQueryable() - .Where(spec.WhereExpressions.FirstOrDefault().Filter); + var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Equal(2, result.ToList().Count); diff --git a/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterSpecification.cs index 56b1e0d9..cb065df3 100644 --- a/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterSpecification.cs +++ b/tests/UnitTests/ApplicationCore/Specifications/CatalogFilterSpecification.cs @@ -19,9 +19,7 @@ public void MatchesExpectedNumberOfItems(int? brandId, int? typeId, int expected { var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterSpecification(brandId, typeId); - var result = GetTestItemCollection() - .AsQueryable() - .Where(spec.WhereExpressions.FirstOrDefault().Filter); + var result = spec.Evaluate(GetTestItemCollection()).ToList(); Assert.Equal(expectedCount, result.Count()); } diff --git a/tests/UnitTests/ApplicationCore/Specifications/CatalogItemsSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/CatalogItemsSpecification.cs index 0085ca71..f9fc0494 100644 --- a/tests/UnitTests/ApplicationCore/Specifications/CatalogItemsSpecification.cs +++ b/tests/UnitTests/ApplicationCore/Specifications/CatalogItemsSpecification.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.eShopWeb.ApplicationCore.Entities; -using Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications; @@ -14,9 +14,7 @@ public void MatchesSpecificCatalogItem() var catalogItemIds = new int[] { 1 }; var spec = new eShopWeb.ApplicationCore.Specifications.CatalogItemsSpecification(catalogItemIds); - var result = GetTestCollection() - .AsQueryable() - .Where(spec.WhereExpressions.FirstOrDefault().Filter); + var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Single(result.ToList()); @@ -28,9 +26,7 @@ public void MatchesAllCatalogItems() var catalogItemIds = new int[] { 1, 3 }; var spec = new eShopWeb.ApplicationCore.Specifications.CatalogItemsSpecification(catalogItemIds); - var result = GetTestCollection() - .AsQueryable() - .Where(spec.WhereExpressions.FirstOrDefault().Filter); + var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Equal(2, result.ToList().Count); @@ -40,14 +36,14 @@ private List GetTestCollection() { var catalogItems = new List(); - var mockCatalogItem1 = new Mock(1, 1, "Item 1 description", "Item 1", 1.5m, "Item1Uri"); - mockCatalogItem1.SetupGet(x => x.Id).Returns(1); + var mockCatalogItem1 = Substitute.For(1, 1, "Item 1 description", "Item 1", 1.5m, "Item1Uri"); + mockCatalogItem1.Id.Returns(1); - var mockCatalogItem3 = new Mock(3, 3, "Item 3 description", "Item 3", 3.5m, "Item3Uri"); - mockCatalogItem3.SetupGet(x => x.Id).Returns(3); + var mockCatalogItem3 = Substitute.For(3, 3, "Item 3 description", "Item 3", 3.5m, "Item3Uri"); + mockCatalogItem3.Id.Returns(3); - catalogItems.Add(mockCatalogItem1.Object); - catalogItems.Add(mockCatalogItem3.Object); + catalogItems.Add(mockCatalogItem1); + catalogItems.Add(mockCatalogItem3); return catalogItems; } diff --git a/tests/UnitTests/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs b/tests/UnitTests/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs index fe3281e3..0a066d07 100644 --- a/tests/UnitTests/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs +++ b/tests/UnitTests/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs @@ -15,14 +15,12 @@ public void ReturnsOrderWithOrderedItem() { var spec = new eShopWeb.ApplicationCore.Specifications.CustomerOrdersWithItemsSpecification(_buyerId); - var result = GetTestCollection() - .AsQueryable() - .FirstOrDefault(spec.WhereExpressions.FirstOrDefault().Filter); + var result = spec.Evaluate(GetTestCollection()).FirstOrDefault(); Assert.NotNull(result); Assert.NotNull(result.OrderItems); Assert.Equal(1, result.OrderItems.Count); - Assert.NotNull(result.OrderItems.FirstOrDefault().ItemOrdered); + Assert.NotNull(result.OrderItems.FirstOrDefault()?.ItemOrdered); } [Fact] @@ -30,15 +28,12 @@ public void ReturnsAllOrderWithAllOrderedItem() { var spec = new eShopWeb.ApplicationCore.Specifications.CustomerOrdersWithItemsSpecification(_buyerId); - var result = GetTestCollection() - .AsQueryable() - .Where(spec.WhereExpressions.FirstOrDefault().Filter) - .ToList(); + var result = spec.Evaluate(GetTestCollection()).ToList(); Assert.NotNull(result); Assert.Equal(2, result.Count); Assert.Equal(1, result[0].OrderItems.Count); - Assert.NotNull(result[0].OrderItems.FirstOrDefault().ItemOrdered); + 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); diff --git a/tests/UnitTests/Builders/BasketBuilder.cs b/tests/UnitTests/Builders/BasketBuilder.cs index 08dd04a4..708df646 100644 --- a/tests/UnitTests/Builders/BasketBuilder.cs +++ b/tests/UnitTests/Builders/BasketBuilder.cs @@ -1,5 +1,5 @@ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; -using Moq; +using NSubstitute; namespace Microsoft.eShopWeb.UnitTests.Builders; @@ -22,17 +22,17 @@ public Basket Build() public Basket WithNoItems() { - var basketMock = new Mock(BasketBuyerId); - basketMock.SetupGet(s => s.Id).Returns(BasketId); + var basketMock = Substitute.For(BasketBuyerId); + basketMock.Id.Returns(BasketId); - _basket = basketMock.Object; + _basket = basketMock; return _basket; } public Basket WithOneBasketItem() { - var basketMock = new Mock(BasketBuyerId); - _basket = basketMock.Object; + var basketMock = Substitute.For(BasketBuyerId); + _basket = basketMock; _basket.AddItem(2, 3.40m, 4); return _basket; } diff --git a/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders.cs b/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders.cs index e0bd0038..ccb4129c 100644 --- a/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders.cs +++ b/tests/UnitTests/MediatorHandlers/OrdersTests/GetMyOrders.cs @@ -5,23 +5,22 @@ using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Web.Features.MyOrders; -using Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests; public class GetMyOrders { - private readonly Mock> _mockOrderRepository; + private readonly IReadRepository _mockOrderRepository = Substitute.For>(); public GetMyOrders() { var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); - var address = new Address(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()); + var address = new Address("", "", "", "", ""); Order order = new Order("buyerId", address, new List { item }); - - _mockOrderRepository = new Mock>(); - _mockOrderRepository.Setup(x => x.ListAsync(It.IsAny>(), default)).ReturnsAsync(new List { order }); + + _mockOrderRepository.ListAsync(Arg.Any>(), default).Returns(new List { order }); } [Fact] @@ -29,7 +28,7 @@ public async Task NotReturnNullIfOrdersArePresIent() { var request = new eShopWeb.Web.Features.MyOrders.GetMyOrders("SomeUserName"); - var handler = new GetMyOrdersHandler(_mockOrderRepository.Object); + var handler = new GetMyOrdersHandler(_mockOrderRepository); var result = await handler.Handle(request, CancellationToken.None); diff --git a/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails.cs b/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails.cs index ec4cadde..625de042 100644 --- a/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails.cs +++ b/tests/UnitTests/MediatorHandlers/OrdersTests/GetOrderDetails.cs @@ -6,24 +6,23 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; using Microsoft.eShopWeb.Web.Features.OrderDetails; -using Moq; +using NSubstitute; using Xunit; namespace Microsoft.eShopWeb.UnitTests.MediatorHandlers.OrdersTests; public class GetOrderDetails { - private readonly Mock> _mockOrderRepository; - + private readonly IReadRepository _mockOrderRepository = Substitute.For>(); + public GetOrderDetails() { var item = new OrderItem(new CatalogItemOrdered(1, "ProductName", "URI"), 10.00m, 10); - var address = new Address(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()); + var address = new Address("", "", "", "", ""); Order order = new Order("buyerId", address, new List { item }); - - _mockOrderRepository = new Mock>(); - _mockOrderRepository.Setup(x => x.GetBySpecAsync(It.IsAny(), default)) - .ReturnsAsync(order); + + _mockOrderRepository.FirstOrDefaultAsync(Arg.Any(), default) + .Returns(order); } [Fact] @@ -31,7 +30,7 @@ public async Task NotBeNullIfOrderExists() { var request = new eShopWeb.Web.Features.OrderDetails.GetOrderDetails("SomeUserName", 0); - var handler = new GetOrderDetailsHandler(_mockOrderRepository.Object); + var handler = new GetOrderDetailsHandler(_mockOrderRepository); var result = await handler.Handle(request, CancellationToken.None); diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index 665f5a9d..c2d6e0c8 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -1,8 +1,7 @@  - - net7.0 - disable + + enable Microsoft.eShopWeb.UnitTests false latest @@ -10,18 +9,15 @@ - - - - - + + + all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers + runtime; build; native; contentfiles; analyzers; buildtransitive + + +