diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 3fc7543..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,82 +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:7.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 1448ea9..2f003b0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,47 +1,29 @@ -// 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" - } - }, + "image": "mcr.microsoft.com/devcontainers/dotnet:0-7.0", - // 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", - "runArgs": [ - "-v", - "/var/run/docker.sock:/var/run/docker.sock" - ], - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.profiles.linux": { - "bash": { - "path": "bash", - "icon": "terminal-bash" - }, - }, + "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", + "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" @@ -51,14 +33,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/.devcontainer/devcontainerreadme.md b/.devcontainer/devcontainerreadme.md new file mode 100644 index 0000000..ff6ee13 --- /dev/null +++ b/.devcontainer/devcontainerreadme.md @@ -0,0 +1,36 @@ +# Dev container + +This project includes a [dev container](https://containers.dev/), which lets you use a container as a full-featured dev environment. + +You can use the dev container configuration in this folder to build and run the app without needing to install any of its tools locally! You can use it in [GitHub Codespaces](https://github.com/features/codespaces) or the [VS Code Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). + +## GitHub Codespaces +Follow these steps to open this sample in a Codespace: +1. Click the **Code** drop-down menu at the top of https://github.com/dotnet-architecture/eShopOnWeb. +1. Click on the **Codespaces** tab. +1. Click **Create codespace on main** . + +For more info, check out the [GitHub documentation](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace#creating-a-codespace). + +## VS Code Dev Containers + +If you already have VS Code and Docker installed, you can click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/dotnet-architecture/eShopOnWeb) to get started. This will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +You can also follow these steps to open this sample in a container using the VS Code Dev Containers extension: + +1. If this is your first time using a development container, please ensure your system meets the pre-reqs (i.e. have Docker installed) in the [getting started steps](https://aka.ms/vscode-remote/containers/getting-started). + +2. Open a locally cloned copy of the code: + + - Clone this repository to your local filesystem. + - Press F1 and select the **Dev Containers: Open Folder in Container...** command. + - Select the cloned copy of this folder, wait for the container to start, and try things out! + +You can learn more in the [Dev Containers documentation](https://code.visualstudio.com/docs/devcontainers/containers). + +## Tips and tricks + +* Since the dev container is Linux-based, you won't be able to use LocalDB. Add ` "UseOnlyInMemoryDatabase": true,` to the [appsettings.json](../src/Web/appsettings.json) file (there's additional context on this [in the app's readme](../README.md#configuring-the-sample-to-use-sql-server)). +* If you get a `502 bad gateway` error, you may need to set your port to the https protocol. You can do this by opening the Ports view in VS Code (**Ports: Focus on Ports View**), right-clicking on the port you're using, select **Change Port Protocol**, and set **https**. +* If you are working with the same repository folder in a container and Windows, you'll want consistent line endings (otherwise you may see hundreds of changes in the SCM view). The `.gitattributes` file in the root of this repo disables line ending conversion and should prevent this. See [tips and tricks](https://code.visualstudio.com/docs/devcontainers/tips-and-tricks#_resolving-git-line-ending-issues-in-containers-resulting-in-many-modified-files) for more info. +* If you'd like to review the contents of the image used in this dev container, you can check it out in the [devcontainers/images](https://github.com/devcontainers/images/tree/main/src/dotnet) repo. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5dc46e6 --- /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/workflows/richnav.yml b/.github/workflows/richnav.yml index 977ee7c..d83d19f 100644 --- a/.github/workflows/richnav.yml +++ b/.github/workflows/richnav.yml @@ -1,11 +1,11 @@ -name: eShopOnWeb Rich Code Navigation +name: eShopOnWeb - Code Index -on: [push, pull_request, workflow_dispatch] +on: workflow_dispatch jobs: build: - runs-on: windows-2019 + runs-on: windows-latest steps: - uses: actions/checkout@v2 @@ -15,8 +15,10 @@ jobs: dotnet-version: 7.0.x - name: Build with dotnet - run: dotnet build ./eShopOnWeb.sln --configuration Release + run: dotnet build ./Everything.sln --configuration Release /bl - uses: microsoft/RichCodeNavIndexer@v0.1 with: - repo-token: ${{ github.token }} \ No newline at end of file + repo-token: ${{ github.token }} + languages: 'csharp' + environment: 'internal' diff --git a/.gitignore b/.gitignore index 1148ecd..91a682b 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 d0663c2..680470c 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/Directory.Packages.props b/Directory.Packages.props index 67c8051..73b1ebd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,39 +1,40 @@ true - true net7.0 - + - + - + + + - + - - - - - - - - - + + + + + + + + + - + - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -42,13 +43,13 @@ - + - - + + all @@ -61,6 +62,6 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 0a5e5e8..156b14f 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,55 @@ 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) +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). @@ -58,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. @@ -98,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 0000000..c63402c --- /dev/null +++ b/azure.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/wbreza/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: eShopOnWeb +services: + web: + project: ./src/Web + language: csharp + host: appservice \ No newline at end of file diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..a4fc9df --- /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 0000000..64477a7 --- /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 0000000..c65f2b8 --- /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 0000000..69c35d7 --- /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 0000000..aa989eb --- /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 0000000..0eb4a86 --- /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 0000000..f1187e7 --- /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: '7.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 0000000..0ef1d97 --- /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/Specifications/CustomerOrdersSpecification.cs b/src/ApplicationCore/Specifications/CustomerOrdersSpecification.cs new file mode 100644 index 0000000..e3faa6a --- /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/PublicApi/appsettings.Docker.json b/src/PublicApi/appsettings.Docker.json index 0aa16e3..0bf721d 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/Features/MyOrders/GetMyOrders.cs b/src/Web/Features/MyOrders/GetMyOrders.cs index aedfde1..5baf32c 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 125f0a9..df6db4b 100644 --- a/src/Web/Features/MyOrders/GetMyOrdersHandler.cs +++ b/src/Web/Features/MyOrders/GetMyOrdersHandler.cs @@ -18,20 +18,12 @@ public class GetMyOrdersHandler : IRequestHandler> 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 2cc0721..deb1fb5 100644 --- a/src/Web/Features/OrderDetails/GetOrderDetails.cs +++ b/src/Web/Features/OrderDetails/GetOrderDetails.cs @@ -3,7 +3,7 @@ using Microsoft.eShopWeb.Web.ViewModels; 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 be7b8dc..4c11199 100644 --- a/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs +++ b/src/Web/Features/OrderDetails/GetOrderDetailsHandler.cs @@ -6,7 +6,7 @@ using Microsoft.eShopWeb.Web.ViewModels; namespace Microsoft.eShopWeb.Web.Features.OrderDetails; -public class GetOrderDetailsHandler : IRequestHandler +public class GetOrderDetailsHandler : IRequestHandler { private readonly IReadRepository _orderRepository; @@ -15,7 +15,7 @@ public class GetOrderDetailsHandler : IRequestHandler Handle(GetOrderDetails request, + public async Task Handle(GetOrderDetails request, CancellationToken cancellationToken) { var spec = new OrderWithItemsByIdSpec(request.OrderId); @@ -26,7 +26,7 @@ public class GetOrderDetailsHandler : IRequestHandler new OrderItemViewModel diff --git a/src/Web/Program.cs b/src/Web/Program.cs index c5dbcc1..9761361 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.Authentication.Cookies; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Data; @@ -18,10 +20,27 @@ using Microsoft.eShopWeb.Web.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; 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(); diff --git a/src/Web/ViewModels/OrderDetailViewModel.cs b/src/Web/ViewModels/OrderDetailViewModel.cs new file mode 100644 index 0000000..18a3aa0 --- /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/OrderViewModel.cs b/src/Web/ViewModels/OrderViewModel.cs index 582a504..d34866a 100644 --- a/src/Web/ViewModels/OrderViewModel.cs +++ b/src/Web/ViewModels/OrderViewModel.cs @@ -11,5 +11,4 @@ public class OrderViewModel public decimal Total { get; set; } public string Status => DEFAULT_STATUS; public Address? ShippingAddress { get; set; } - public List OrderItems { get; set; } = new(); } diff --git a/src/Web/Views/Order/Detail.cshtml b/src/Web/Views/Order/Detail.cshtml index 097d17e..c5eb47b 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"; } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index fdb6574..6ed273b 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index 70989a6..c2bc659 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -17,4 +17,4 @@ }, "AllowedHosts": "*" } -} \ No newline at end of file +} diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index f484cf9..e703b4d 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -19,7 +19,7 @@ - + diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index f8bbe01..4b069f2 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -9,7 +9,6 @@ -