diff --git a/KUBERNETES.md b/KUBERNETES.md
new file mode 100644
index 000000000..6e107b3a1
--- /dev/null
+++ b/KUBERNETES.md
@@ -0,0 +1,134 @@
+# Kubernetes 101
+## Docker vs. Kubernetes
+Docker helps you package applications into images, and execute them in containers. Kubernetes is a robust platform for containerized applications. It abstracts away the underlying network infrastructure and hardware required to run them, simplifying their deployment, scaling, and management.
+
+## Kubernetes from the container up
+### Pods
+The basic unit of a Kubernetes deployment is the **Pod**. A Pod encapsulates one or more containers. For example, the `basket` Pod specifies two containers:
+>`deployments.yaml`
+>
+>The first container runs the `eshop/basket.api` image:
+>```yaml
+>spec:
+> containers:
+> - name: basket
+> image: eshop/basket.api
+> env:
+> - name: ConnectionString
+> value: 127.0.0.1
+>```
+>Note the `ConnectionString` environment variable: containers within a Pod are networked via `localhost`. The second container runs the `redis` image:
+>```yaml
+>- name: basket-data
+> image: redis:3.2-alpine
+> ports:
+> - containerPort: 6379
+>```
+Placing `basket` and `basket-data` in the same Pod is reasonable here because the former requires the latter, and owns all its data. If we wanted to scale the service, however, it would be better to place the containers in separate Pods because the basket API and redis scale at different rates.
+
+If the containers were in separate Pods, they would no longer be able to communicate via `localhost`; a **Service** would be required.
+
+### Services
+Services expose Pods to external networks. For example, the `basket` Service exposes Pods with labels `app=eshop` and `component=basket` to the cluster at large:
+>`services.yaml`
+>```yaml
+>kind: Service
+>metadata:
+> ...
+> name: basket
+>spec:
+> ports:
+> - port: 80
+> selector:
+> app: eshop
+> component: basket
+>```
+Kubernetes's built-in DNS service resolves Service names to cluster-internal IP addresses. This allows the nginx frontend to proxy connections to the app's microservices by name:
+>`nginx.conf`
+>```
+>location /basket-api {
+> proxy_pass http://basket;
+>```
+The frontend Pod is different in that it needs to be exposed outside the cluster. This is accomplished with another Service:
+>`frontend.yaml`
+>```yaml
+>spec:
+> ports:
+> - port: 80
+> targetPort: 8080
+> selector:
+> app: eshop
+> component: frontend
+> type: LoadBalancer
+>```
+`type: LoadBalancer` tells Kubernetes to expose the Service behind a load balancer appropriate for the cluster's platform. For Azure Container Service, this creates an Azure load balancer rule with a public IP.
+
+### Deployments
+Kubernetes uses Pods to organize containers, and Services to network them. It uses **Deployments** to organize creating, and modifying, Pods. A Deployment describes a state of one or more Pods. When a Deployment is created or modified, Kubernetes attempts to realize that state.
+
+The Deployments in this project are basic. Still, `deploy.ps1` shows some more advanced Deployment capabilities. For example, Deployments can be paused. Each Deployment of this app is paused at creation:
+>`deployments.yaml`
+>```yaml
+>kind: Deployment
+>spec:
+> paused: true
+>```
+This allows the deployment script to change images before Kubernetes creates the Pods:
+>`deploy.ps1`
+>```powershell
+>kubectl set image -f deployments.yaml basket=$registry/basket.api ...
+>kubectl rollout resume -f deployments.yaml
+>```
+
+### ConfigMaps
+A **ConfigMap** is a collection of key/value pairs commonly used to provide configuration information to Pods. The deployment script uses one to store the frontend's configuration:
+>`deploy.ps1`
+>```
+>kubectl create configmap config-files from-file=nginx-conf=nginx.conf
+>```
+This creates a ConfigMap named `config-files` with key `nginx-conf` whose value is the content of nginx.conf. The frontend Pod mounts that value as `/etc/nginx/nginx.conf`:
+>`frontend.yaml`
+>```yaml
+>spec:
+> containers:
+> - name: nginx
+> ...
+> volumeMounts:
+> - name: config
+> mountPath: /etc/nginx
+> volumes:
+> - name: config
+> configMap:
+> name: config-files
+> items:
+> - key: nginx-conf
+> path: nginx.conf
+>```
+This facilitates rapid iteration better than other techniques, e.g. building an image to bake in configuration.
+
+The script also stores public URLs for the app's components in a ConfigMap:
+>`deploy.ps1`
+>```powershell
+>kubectl create configmap urls --from-literal=BasketUrl=http://$($frontendUrl)/basket-api ...
+>```
+>Here's how the `webspa` Deployment uses it:
+>
+>`deployments.yaml`
+>```yaml
+>spec:
+> containers:
+> - name: webspa
+> ...
+> env:
+> ...
+> - name: BasketUrl
+> valueFrom:
+> configMapKeyRef:
+> name: urls
+> key: BasketUrl
+>```
+
+### Further reading
+* [Kubernetes Concepts](https://kubernetes.io/docs/concepts/)
+* [kubectl for Docker Users](https://kubernetes.io/docs/user-guide/docker-cli-to-kubectl/)
+* [Kubernetes API reference](https://kubernetes.io/docs/api-reference/v1.5/)
\ No newline at end of file
diff --git a/README.k8s.md b/README.k8s.md
new file mode 100644
index 000000000..28ebd49e8
--- /dev/null
+++ b/README.k8s.md
@@ -0,0 +1,28 @@
+# eShopOnContainers on Kubernetes
+The k8s directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each eShopOnContainers microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy specified in `frontend.yaml` and `nginx.conf`.
+
+## Prerequisites
+* A Kubernetes cluster. Follow Azure Container Service's [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough) to create one.
+* A private Docker registry. Follow Azure Container Registry's [guide](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-portal) to create one.
+* Optionally, previous steps can be skipped if you run gen-k8s-env.ps1 script to automatically create the azure environment needed for kubernetes deployment. Azure cli 2.0 must be previously installed [installation guide](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). For example:
+>```
+>./gen-k8s-env -resourceGroupName k8sGroup -location westeurope -registryName k8sregistry -orchestratorName k8s-cluster -dnsName k8s-dns
+>```
+* A Docker development environment with `docker` and `docker-compose`.
+ * Visit [docker.com](https://docker.com) to download the tools and set up the environment. Docker's [installation guide](https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation) covers verifying your Docker installation.
+* The Kubernetes command line client, `kubectl`.
+ * This can be installed with the `az` tool as described in the Azure Container Service [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough). `az` is also helpful for getting the credentials `kubectl` needs to access your cluster. For other installation options, and information about configuring `kubectl` yourself, see the [Kubernetes documentation](https://kubernetes.io/docs/tasks/kubectl/install/).
+
+## Deploy the application with the deployment script
+1. Open a PowerShell command line at the `k8s` directory of your local eShopOnContainers repository.
+1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster.
+1. Run `deploy.ps1` with your registry information. The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. Optionally, ACR credentials can be obtained by running the following command:
+>```
+>az acr credential show -n eshopregistry
+>```
+
+Once the user and password are retrieved, run the following script for deployment. For example:
+>```
+>./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword
+>```
+The script will build the code and corresponding Docker images, push the latter to your registry, and deploy the application to your cluster. You can watch the deployment unfold from the Kubernetes web interface: run `kubectl proxy` and open a browser to [http://localhost:8001/ui](http://localhost:8001/ui)
diff --git a/README.md b/README.md
index faf758f96..bcb1b2a41 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**BETA state** - Visual Studio 2017 and CLI environments compatible)
Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers.
+**Note for Pull Requests**: We accept pull request from the community. When doing it, please do it onto the DEV branch which is the consolidated work-in-progress branch. Do not request it onto Master, if possible.
> ### DISCLAIMER
> **IMPORTANT:** The current state of this sample application is **BETA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.**
@@ -91,6 +92,26 @@ https://github.com/dotnet/eShopOnContainers/wiki/04.-Setting-eShopOnContainer-so
The Windows Containers scenario is currently being implemented/tested yet. The application should be able to run on Windows Nano Containers based on different Docker base images, as well, as the .NET Core services have also been tested running on plain Windows (with no Docker).
The app was also partially tested on "Docker for Mac" using a development MacOS machine with .NET Core and VS Code installed, which is still a scenario using Linux containers running on the VM setup in the Mac by the "Docker for Windows" setup. But further testing and feedback on Mac environments and Windows Containers, from the community, will be appreciated.
+## Kubernetes
+The k8s directory contains Kubernetes configuration for the eShopOnContainers app and a PowerShell script to deploy it to a cluster. Each eShopOnContainers microservice has a deployment configuration in `deployments.yaml`, and is exposed to the cluster by a service in `services.yaml`. The microservices are exposed externally on individual routes (`/basket-api`, `/webmvc`, etc.) by an nginx reverse proxy specified in `frontend.yaml` and `nginx.conf`.
+
+### Prerequisites
+* A Kubernetes cluster. Follow Azure Container Service's [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough) to create one.
+* A private Docker registry. Follow Azure Container Registry's [guide](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-portal) to create one.
+* A Docker development environment with `docker` and `docker-compose`.
+ * Visit [docker.com](https://docker.com) to download the tools and set up the environment. Docker's [installation guide](https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation) covers verifying your Docker installation.
+* The Kubernetes command line client, `kubectl`.
+ * This can be installed with the `az` tool as described in the Azure Container Service [walkthrough](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough). `az` is also helpful for getting the credentials `kubectl` needs to access your cluster. For other installation options, and information about configuring `kubectl` yourself, see the [Kubernetes documentation](https://kubernetes.io/docs/tasks/kubectl/install/).
+
+### Deploy the application with the deployment script
+1. Open a PowerShell command line at the `k8s` directory of your local eShopOnContainers repository.
+1. Ensure `docker`, `docker-compose`, and `kubectl` are on the path, and configured for your Docker machine and Kubernetes cluster.
+1. Run `deploy.ps1` with your registry information. The Docker username and password are provided by Azure Container Registry, and can be retrieved from the Azure portal. For example:
+>```
+>./deploy.ps1 -registry myregistry.azurecr.io -dockerUser User -dockerPassword SecretPassword
+>```
+The script will build the code and corresponding Docker images, push the latter to your registry, and deploy the application to your cluster. You can watch the deployment unfold from the Kubernetes web interface: run `kubectl proxy` and open a browser to [http://localhost:8001/ui](http://localhost:8001/ui)
+
## Sending feedback and pull requests
As mentioned, we'd appreciate to your feedback, improvements and ideas.
You can create new issues at the issues section, do pull requests and/or send emails to **eshop_feedback@service.microsoft.com**
diff --git a/cli-mac/build-bits.sh b/cli-mac/build-bits.sh
old mode 100644
new mode 100755
index f8ad2e2f9..681c3605d
--- a/cli-mac/build-bits.sh
+++ b/cli-mac/build-bits.sh
@@ -10,6 +10,12 @@ projectList=(
"../src/Web/WebStatus"
)
+
+pushd $(pwd)/../src/Web/WebSPA
+npm install
+npm rebuild node-sass
+popd
+
for project in "${projectList[@]}"
do
echo -e "\e[33mWorking on $(pwd)/$project"
diff --git a/cli-windows/build-bits-simple.ps1 b/cli-windows/build-bits-simple.ps1
index 7c5156da3..461384cc2 100644
--- a/cli-windows/build-bits-simple.ps1
+++ b/cli-windows/build-bits-simple.ps1
@@ -11,8 +11,7 @@ if ([string]::IsNullOrEmpty($rootPath)) {
}
Write-Host "Root path used is $rootPath" -ForegroundColor Yellow
-
-$SolutionFilePath = $rootPath + "eShopOnContainers-ServicesAndWebApps.sln"
+$SolutionFilePath = [IO.Path]::Combine($rootPath, "eShopOnContainers-ServicesAndWebApps.sln")
dotnet restore $SolutionFilePath
diff --git a/docker-compose.ci.build.yml b/docker-compose.ci.build.yml
index 546b7690f..1b3f3bea3 100644
--- a/docker-compose.ci.build.yml
+++ b/docker-compose.ci.build.yml
@@ -6,5 +6,5 @@ services:
volumes:
- .:/src
working_dir: /src
- command: /bin/bash -c "dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish"
-
+ command: /bin/bash -c "pushd ./src/Web/WebSPA && npm rebuild node-sass && popd && dotnet restore ./eShopOnContainers-ServicesAndWebApps.sln && dotnet publish ./eShopOnContainers-ServicesAndWebApps.sln -c Release -o ./obj/Docker/publish"
+
\ No newline at end of file
diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln
index 9de852355..80c95a417 100644
--- a/eShopOnContainers-ServicesAndWebApps.sln
+++ b/eShopOnContainers-ServicesAndWebApps.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26403.7
+VisualStudioVersion = 15.0.26403.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}"
EndProject
diff --git a/k8s/deploy.ps1 b/k8s/deploy.ps1
new file mode 100644
index 000000000..61c7bd909
--- /dev/null
+++ b/k8s/deploy.ps1
@@ -0,0 +1,80 @@
+Param(
+ [parameter(Mandatory=$true)][string]$registry,
+ [parameter(Mandatory=$true)][string]$dockerUser,
+ [parameter(Mandatory=$true)][string]$dockerPassword
+)
+
+$requiredCommands = ("docker", "docker-compose", "kubectl")
+foreach ($command in $requiredCommands) {
+ if ((Get-Command $command -ErrorAction SilentlyContinue) -eq $null) {
+ Write-Host "$command must be on path" -ForegroundColor Red
+ exit
+ }
+}
+
+Write-Host "Logging in to $registry" -ForegroundColor Yellow
+docker login -u $dockerUser -p $dockerPassword $registry
+if (-not $LastExitCode -eq 0) {
+ Write-Host "Login failed" -ForegroundColor Red
+ exit
+}
+
+# create registry key secret
+kubectl create secret docker-registry registry-key `
+ --docker-server=$registry `
+ --docker-username=$dockerUser `
+ --docker-password=$dockerPassword `
+ --docker-email=not@used.com
+
+# start sql, rabbitmq, frontend deployments
+kubectl create configmap config-files --from-file=nginx-conf=nginx.conf
+kubectl label configmap config-files app=eshop
+kubectl create -f sql-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml
+
+Write-Host "Building and publishing eShopOnContainers..." -ForegroundColor Yellow
+dotnet restore ../eShopOnContainers-ServicesAndWebApps.sln
+dotnet publish -c Release -o obj/Docker/publish ../eShopOnContainers-ServicesAndWebApps.sln
+
+Write-Host "Building Docker images..." -ForegroundColor Yellow
+docker-compose -p .. -f ../docker-compose.yml build
+
+Write-Host "Pushing images to $registry..." -ForegroundColor Yellow
+$services = ("basket.api", "catalog.api", "identity.api", "ordering.api", "webmvc", "webspa")
+foreach ($service in $services) {
+ docker tag eshop/$service $registry/eshop/$service
+ docker push $registry/eshop/$service
+}
+
+Write-Host "Waiting for frontend's external ip..." -ForegroundColor Yellow
+while ($true) {
+ $frontendUrl = kubectl get svc frontend -o=jsonpath="{.status.loadBalancer.ingress[0].ip}"
+ if ([bool]($frontendUrl -as [ipaddress])) {
+ break
+ }
+ Start-Sleep -s 15
+}
+
+kubectl create configmap urls `
+ --from-literal=BasketUrl=http://$($frontendUrl)/basket-api `
+ --from-literal=CatalogUrl=http://$($frontendUrl)/catalog-api `
+ --from-literal=IdentityUrl=http://$($frontendUrl)/identity `
+ --from-literal=OrderingUrl=http://$($frontendUrl)/ordering-api `
+ --from-literal=MvcClient=http://$($frontendUrl)/webmvc `
+ --from-literal=SpaClient=http://$($frontendUrl)
+kubectl label configmap urls app=eshop
+
+Write-Host "Creating deployments..."
+kubectl apply -f deployments.yaml
+
+# update deployments with the private registry before k8s tries to pull images
+# (deployment templating, or Helm, would obviate this)
+kubectl set image -f deployments.yaml `
+ basket=$registry/eshop/basket.api `
+ catalog=$registry/eshop/catalog.api `
+ identity=$registry/eshop/identity.api `
+ ordering=$registry/eshop/ordering.api `
+ webmvc=$registry/eshop/webmvc `
+ webspa=$registry/eshop/webspa
+kubectl rollout resume -f deployments.yaml
+
+Write-Host "WebSPA is exposed at http://$frontendUrl, WebMVC at http://$frontendUrl/webmvc" -ForegroundColor Yellow
diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml
new file mode 100644
index 000000000..cce221db6
--- /dev/null
+++ b/k8s/deployments.yaml
@@ -0,0 +1,236 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: basket
+spec:
+ paused: true
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: basket
+ spec:
+ containers:
+ - name: basket
+ image: eshop/basket.api
+ imagePullPolicy: Always
+ env:
+ - name: ASPNETCORE_URLS
+ value: http://0.0.0.0:80/basket-api
+ - name: ConnectionString
+ value: 127.0.0.1
+ - name: EventBusConnection
+ value: rabbitmq
+ - name: IdentityUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: IdentityUrl
+ ports:
+ - containerPort: 80
+ - name: basket-data
+ image: redis:3.2-alpine
+ ports:
+ - containerPort: 6379
+ imagePullSecrets:
+ - name: registry-key
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: catalog
+spec:
+ paused: true
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: catalog
+ spec:
+ containers:
+ - name: catalog
+ image: eshop/catalog.api
+ imagePullPolicy: Always
+ env:
+ - name: ASPNETCORE_URLS
+ value: http://0.0.0.0:80/catalog-api
+ - name: ConnectionString
+ value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word"
+ - name: EventBusConnection
+ value: rabbitmq
+ - name: ExternalCatalogBaseUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: CatalogUrl
+ ports:
+ - containerPort: 80
+ imagePullSecrets:
+ - name: registry-key
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: identity
+spec:
+ paused: true
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: identity
+ spec:
+ containers:
+ - name: identity
+ image: eshop/identity.api
+ imagePullPolicy: Always
+ env:
+ - name: ASPNETCORE_URLS
+ value: http://0.0.0.0:80/identity
+ - name: ConnectionStrings__DefaultConnection
+ value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word"
+ - name: MvcClient
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: MvcClient
+ - name: SpaClient
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: SpaClient
+ ports:
+ - containerPort: 80
+ imagePullSecrets:
+ - name: registry-key
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: ordering
+spec:
+ paused: true
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: ordering
+ spec:
+ containers:
+ - name: ordering
+ image: eshop/ordering.api
+ imagePullPolicy: Always
+ env:
+ - name: ASPNETCORE_URLS
+ value: http://0.0.0.0:80/ordering-api
+ - name: ConnectionString
+ value: "Server=sql-data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;"
+ - name: EventBusConnection
+ value: rabbitmq
+ - name: IdentityUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: IdentityUrl
+ ports:
+ - containerPort: 80
+ imagePullSecrets:
+ - name: registry-key
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: webmvc
+spec:
+ paused: true
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: webmvc
+ spec:
+ containers:
+ - name: webmvc
+ image: eshop/webmvc
+ imagePullPolicy: Always
+ env:
+ - name: ASPNETCORE_URLS
+ value: http://0.0.0.0:80/webmvc
+ - name: BasketUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: BasketUrl
+ - name: CallBackUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: MvcClient
+ - name: CatalogUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: CatalogUrl
+ - name: IdentityUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: IdentityUrl
+ - name: OrderingUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: OrderingUrl
+ ports:
+ - containerPort: 80
+ imagePullSecrets:
+ - name: registry-key
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: webspa
+spec:
+ paused: true
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: webspa
+ spec:
+ containers:
+ - name: webspa
+ image: eshop/webspa
+ imagePullPolicy: Always
+ env:
+ - name: ASPNETCORE_URLS
+ value: http://0.0.0.0:80
+ - name: BasketUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: BasketUrl
+ - name: CallBackUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: SpaClient
+ - name: CatalogUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: CatalogUrl
+ - name: IdentityUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: IdentityUrl
+ - name: OrderingUrl
+ valueFrom:
+ configMapKeyRef:
+ name: urls
+ key: OrderingUrl
+ ports:
+ - containerPort: 80
+ imagePullSecrets:
+ - name: registry-key
diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml
new file mode 100644
index 000000000..291542404
--- /dev/null
+++ b/k8s/frontend.yaml
@@ -0,0 +1,47 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: frontend
+ name: frontend
+spec:
+ ports:
+ - port: 80
+ targetPort: 8080
+ selector:
+ app: eshop
+ component: frontend
+ type: LoadBalancer
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: frontend
+spec:
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: frontend
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.11.10-alpine
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8080
+ lifecycle:
+ preStop:
+ exec:
+ command: ["/usr/sbin/nginx","-s","quit"]
+ volumeMounts:
+ - name: config
+ mountPath: /etc/nginx
+ volumes:
+ - name: config
+ configMap:
+ name: config-files
+ items:
+ - key: nginx-conf
+ path: nginx.conf
diff --git a/k8s/gen-k8s-env.ps1 b/k8s/gen-k8s-env.ps1
new file mode 100644
index 000000000..39dacba24
--- /dev/null
+++ b/k8s/gen-k8s-env.ps1
@@ -0,0 +1,25 @@
+Param(
+ [parameter(Mandatory=$true)][string]$resourceGroupName,
+ [parameter(Mandatory=$true)][string]$location,
+ [parameter(Mandatory=$true)][string]$registryName,
+ [parameter(Mandatory=$true)][string]$orchestratorName,
+ [parameter(Mandatory=$true)][string]$dnsName
+)
+
+# Create resource group
+Write-Host "Creating resource group..." -ForegroundColor Yellow
+az group create --name=$resourceGroupName --location=$location
+
+# Create Azure Container Registry
+Write-Host "Creating Azure Container Registry..." -ForegroundColor Yellow
+az acr create -n $registryName -g $resourceGroupName -l $location --admin-enabled true --sku Basic
+
+# Create kubernetes orchestrator
+Write-Host "Creating kubernetes orchestrator..." -ForegroundColor Yellow
+az acs create --orchestrator-type=kubernetes --resource-group $resourceGroupName --name=$orchestratorName --dns-prefix=$dnsName --generate-ssh-keys
+
+# Retrieve kubernetes cluster configuration and save it under ~/.kube/config
+az acs kubernetes get-credentials --resource-group=$resourceGroupName --name=$orchestratorName
+
+# Show ACR credentials
+az acr credential show -n $registryName
\ No newline at end of file
diff --git a/k8s/nginx.conf b/k8s/nginx.conf
new file mode 100644
index 000000000..33526d486
--- /dev/null
+++ b/k8s/nginx.conf
@@ -0,0 +1,74 @@
+pid /tmp/nginx.pid;
+
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ server_tokens off;
+
+ add_header X-Frame-Options SAMEORIGIN;
+ add_header X-Content-Type-Options nosniff;
+ add_header X-XSS-Protection "1; mode=block";
+
+ client_body_temp_path /tmp/client_body;
+ fastcgi_temp_path /tmp/fastcgi_temp;
+ proxy_temp_path /tmp/proxy_temp;
+ scgi_temp_path /tmp/scgi_temp;
+ uwsgi_temp_path /tmp/uwsgi_temp;
+
+ gzip on;
+ gzip_comp_level 6;
+ gzip_min_length 1024;
+ gzip_buffers 4 32k;
+ gzip_types text/plain application/javascript text/css;
+ gzip_vary on;
+
+ keepalive_timeout 65;
+
+ proxy_buffer_size 128k;
+ proxy_buffers 4 256k;
+ proxy_busy_buffers_size 256k;
+
+ server {
+ listen 8080;
+
+ location /basket-api {
+ proxy_pass http://basket;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ }
+
+ location /catalog-api {
+ proxy_pass http://catalog;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ }
+
+ location /identity {
+ proxy_pass http://identity;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ }
+
+ location /ordering-api {
+ proxy_pass http://ordering;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ }
+
+ location /webmvc {
+ proxy_pass http://webmvc;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ }
+
+ location / {
+ proxy_pass http://webspa;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ }
+ }
+}
\ No newline at end of file
diff --git a/k8s/rabbitmq.yaml b/k8s/rabbitmq.yaml
new file mode 100644
index 000000000..a0d87549c
--- /dev/null
+++ b/k8s/rabbitmq.yaml
@@ -0,0 +1,30 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: rabbitmq
+ name: rabbitmq
+spec:
+ ports:
+ - port: 5672
+ selector:
+ app: eshop
+ component: rabbitmq
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: rabbitmq
+spec:
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: rabbitmq
+ spec:
+ containers:
+ - name: rabbitmq
+ image: rabbitmq:3.6.9-alpine
+ ports:
+ - containerPort: 5672
diff --git a/k8s/services.yaml b/k8s/services.yaml
new file mode 100644
index 000000000..fda3e5ea3
--- /dev/null
+++ b/k8s/services.yaml
@@ -0,0 +1,83 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: basket
+ name: basket
+spec:
+ ports:
+ - port: 80
+ selector:
+ app: eshop
+ component: basket
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: catalog
+ name: catalog
+spec:
+ ports:
+ - port: 80
+ selector:
+ app: eshop
+ component: catalog
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: identity
+ name: identity
+spec:
+ ports:
+ - port: 80
+ selector:
+ app: eshop
+ component: identity
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: ordering
+ name: ordering
+spec:
+ ports:
+ - port: 80
+ selector:
+ app: eshop
+ component: ordering
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: webmvc
+ name: webmvc
+spec:
+ ports:
+ - port: 80
+ selector:
+ app: eshop
+ component: webmvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: webspa
+ name: webspa
+spec:
+ ports:
+ - port: 80
+ selector:
+ app: eshop
+ component: webspa
\ No newline at end of file
diff --git a/k8s/sql-data.yaml b/k8s/sql-data.yaml
new file mode 100644
index 000000000..6edcd21bc
--- /dev/null
+++ b/k8s/sql-data.yaml
@@ -0,0 +1,33 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: eshop
+ component: sql-data
+ name: sql-data
+spec:
+ ports:
+ - port: 1433
+ selector:
+ app: eshop
+ component: sql-data
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: sql-data
+spec:
+ template:
+ metadata:
+ labels:
+ app: eshop
+ component: sql-data
+ spec:
+ containers:
+ - name: sql-data
+ image: microsoft/mssql-server-linux:ctp1-3
+ env:
+ - name: ACCEPT_EULA
+ value: "Y"
+ - name: SA_PASSWORD
+ value: Pass@word
diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs
index 4998c91ed..1837d9638 100644
--- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs
+++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs
@@ -7,9 +7,18 @@ using System.Data.SqlClient;
namespace Microsoft.Extensions.HealthChecks
{
+ // REVIEW: What are the appropriate guards for these functions?
+
public static class HealthCheckBuilderSqlServerExtensions
{
public static HealthCheckBuilder AddSqlCheck(this HealthCheckBuilder builder, string name, string connectionString)
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddSqlCheck(builder, name, connectionString, builder.DefaultCacheDuration);
+ }
+
+ public static HealthCheckBuilder AddSqlCheck(this HealthCheckBuilder builder, string name, string connectionString, TimeSpan cacheDuration)
{
builder.AddCheck($"SqlCheck({name})", async () =>
{
@@ -37,7 +46,7 @@ namespace Microsoft.Extensions.HealthChecks
{
return HealthCheckResult.Unhealthy($"SqlCheck({name}): Exception during check: {ex.GetType().FullName}");
}
- });
+ }, cacheDuration);
return builder;
}
diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs
index f3c795629..4c958234e 100644
--- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs
+++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs
@@ -10,7 +10,14 @@ namespace Microsoft.Extensions.HealthChecks
{
// Numeric checks
- public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc)
+ public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc) where T : IComparable
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddMinValueCheck(builder, name, minValue, currentValueFunc, builder.DefaultCacheDuration);
+ }
+
+ public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc, TimeSpan cacheDuration)
where T : IComparable
{
Guard.ArgumentNotNull(nameof(builder), builder);
@@ -26,12 +33,19 @@ namespace Microsoft.Extensions.HealthChecks
$"min={minValue}, current={currentValue}",
new Dictionary { { "min", minValue }, { "current", currentValue } }
);
- });
+ }, cacheDuration);
return builder;
}
- public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc)
+ public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc) where T : IComparable
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddMaxValueCheck(builder, name, maxValue, currentValueFunc, builder.DefaultCacheDuration);
+ }
+
+ public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc, TimeSpan cacheDuration)
where T : IComparable
{
Guard.ArgumentNotNull(nameof(builder), builder);
@@ -47,7 +61,7 @@ namespace Microsoft.Extensions.HealthChecks
$"max={maxValue}, current={currentValue}",
new Dictionary { { "max", maxValue }, { "current", currentValue } }
);
- });
+ }, cacheDuration);
return builder;
}
diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs
index d4491fda4..dbd9feff2 100644
--- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs
+++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Diagnostics;
namespace Microsoft.Extensions.HealthChecks
@@ -12,10 +13,19 @@ namespace Microsoft.Extensions.HealthChecks
public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize)
=> AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64);
+ public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration)
+ => AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64, cacheDuration);
+
public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize)
=> AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64);
+ public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration)
+ => AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64, cacheDuration);
+
public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize)
=> AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64);
+
+ public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration)
+ => AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64, cacheDuration);
}
}
diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs
index d7df58def..2a6cfe908 100644
--- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs
+++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs
@@ -10,36 +10,73 @@ namespace Microsoft.Extensions.HealthChecks
{
public static partial class HealthCheckBuilderExtensions
{
- // URL checks
+ // Default URL check
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url)
- => AddUrlCheck(builder, url, response => UrlChecker.DefaultUrlCheck(response));
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddUrlCheck(builder, url, builder.DefaultCacheDuration);
+ }
+
+ public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, TimeSpan cacheDuration)
+ => AddUrlCheck(builder, url, response => UrlChecker.DefaultUrlCheck(response), cacheDuration);
+
+ // Func returning IHealthCheckResult
+
+ public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func checkFunc)
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration);
+ }
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
- Func checkFunc)
+ Func checkFunc,
+ TimeSpan cacheDuration)
{
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc);
- return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)));
+ return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)), cacheDuration);
+ }
+
+ // Func returning Task
+
+ public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func> checkFunc)
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration);
}
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
- Func> checkFunc)
+ Func> checkFunc,
+ TimeSpan cacheDuration)
{
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc);
- return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)));
+ return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)), cacheDuration);
+ }
+
+ // Func returning ValueTask
+
+ public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func> checkFunc)
+ {
+ Guard.ArgumentNotNull(nameof(builder), builder);
+
+ return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration);
}
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url,
- Func> checkFunc)
+ Func> checkFunc,
+ TimeSpan cacheDuration)
{
Guard.ArgumentNotNull(nameof(builder), builder);
Guard.ArgumentNotNullOrEmpty(nameof(url), url);
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc);
var urlCheck = new UrlChecker(checkFunc, url);
- builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync());
+ builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync(), cacheDuration);
return builder;
}
}
diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs
index 0e56a66da..5ea3003ed 100644
--- a/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs
+++ b/src/BuildingBlocks/Resilience/Resilience.Http/IHttpClient.cs
@@ -1,16 +1,16 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
+using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
{
public interface IHttpClient
{
- HttpClient Inst { get; }
- Task GetStringAsync(string uri);
- Task PostAsync(string uri, T item);
- Task DeleteAsync(string uri);
+ Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer");
+
+ Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer");
+
+ Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer");
+
+ Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer");
}
}
diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs
deleted file mode 100644
index 63eadc857..000000000
--- a/src/BuildingBlocks/Resilience/Resilience.Http/ResiliencePolicy.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
-{
- public class ResiliencePolicy
- {
- }
-}
diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs
index 2ccc84aaa..88f6cb1d6 100644
--- a/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs
+++ b/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs
@@ -3,9 +3,12 @@ using Newtonsoft.Json;
using Polly;
using Polly.Wrap;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
@@ -18,47 +21,155 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
public class ResilientHttpClient : IHttpClient
{
private HttpClient _client;
- private PolicyWrap _policyWrapper;
+ private readonly ConcurrentDictionary _policiesPerOrigin;
private ILogger _logger;
- public HttpClient Inst => _client;
+ private readonly Func> _policyCreator;
- public ResilientHttpClient(Policy[] policies, ILogger logger)
+
+ public ResilientHttpClient(Func> policyCreator, ILogger logger)
{
_client = new HttpClient();
_logger = logger;
+ _policiesPerOrigin = new ConcurrentDictionary();
+ _policyCreator = policyCreator;
+ }
+
+
+ public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ return DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationMethod);
+ }
+
+ public Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ return DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationMethod);
+ }
+
+ public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ var origin = GetOriginFromUri(uri);
+
+ return HttpInvoker(origin, async () =>
+ {
+ var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri);
+
+ if (authorizationToken != null)
+ {
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
+ }
+
+ if (requestId != null)
+ {
+ requestMessage.Headers.Add("x-requestid", requestId);
+ }
+
+ return await _client.SendAsync(requestMessage);
+ });
+ }
+
+ public Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer")
+ {
+ var origin = GetOriginFromUri(uri);
+
+ return HttpInvoker(origin, async () =>
+ {
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
- // Add Policies to be applied
- _policyWrapper = Policy.WrapAsync(policies);
- }
+ if (authorizationToken != null)
+ {
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
+ }
- public Task GetStringAsync(string uri) =>
- HttpInvoker(() =>
- _client.GetStringAsync(uri));
+ var response = await _client.SendAsync(requestMessage);
+
+ return await response.Content.ReadAsStringAsync();
+ });
+ }
+
+ private Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ if (method != HttpMethod.Post && method != HttpMethod.Put)
+ {
+ throw new ArgumentException("Value must be either post or put.", nameof(method));
+ }
- public Task PostAsync(string uri, T item) =>
// a new StringContent must be created for each retry
// as it is disposed after each call
- HttpInvoker(() =>
+ var origin = GetOriginFromUri(uri);
+
+ return HttpInvoker(origin, async () =>
{
- var response = _client.PostAsync(uri, new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"));
+ var requestMessage = new HttpRequestMessage(method, uri);
+
+ requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json");
+
+ if (authorizationToken != null)
+ {
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
+ }
+
+ if (requestId != null)
+ {
+ requestMessage.Headers.Add("x-requestid", requestId);
+ }
+
+ var response = await _client.SendAsync(requestMessage);
+
// raise exception if HttpResponseCode 500
// needed for circuit breaker to track fails
- if (response.Result.StatusCode == HttpStatusCode.InternalServerError)
+
+ if (response.StatusCode == HttpStatusCode.InternalServerError)
{
throw new HttpRequestException();
}
return response;
});
+ }
- public Task DeleteAsync(string uri) =>
- HttpInvoker(() => _client.DeleteAsync(uri));
+ private Task HttpInvoker(string origin, Func> action)
+ {
+ var policyWrapper = GetPolicyForOrigin(origin);
+ if (policyWrapper != null)
+ {
+ // Executes the action applying all
+ // the policies defined in the wrapper
+ return policyWrapper.ExecuteAsync(() => action());
+ }
+ else
+ {
+ throw new InvalidOperationException($"PolicyWrapper can't be created for origin {origin}");
+ }
+ }
- private Task HttpInvoker(Func> action) =>
- // Executes the action applying all
- // the policies defined in the wrapper
- _policyWrapper.ExecuteAsync(() => action());
- }
+ private PolicyWrap GetPolicyForOrigin(string origin)
+ {
+ var normalizedOrigin = NormalizeOrigin(origin);
+
+ if (!_policiesPerOrigin.TryGetValue(normalizedOrigin, out PolicyWrap policyWrapper))
+ {
+ policyWrapper = Policy.WrapAsync(_policyCreator(normalizedOrigin)
+ .ToArray());
+
+ _policiesPerOrigin.TryAdd(normalizedOrigin, policyWrapper);
+ }
+
+ return policyWrapper;
+ }
+ private static string NormalizeOrigin(string origin)
+ {
+ return origin?.Trim()?.ToLower();
+ }
+
+ private static string GetOriginFromUri(string uri)
+ {
+ var url = new Uri(uri);
+
+ var origin = $"{url.Scheme}://{url.DnsSafeHost}:{url.Port}";
+
+ return origin;
+ }
+ }
}
diff --git a/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs b/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs
index 4f400caf5..3d5217064 100644
--- a/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs
+++ b/src/BuildingBlocks/Resilience/Resilience.Http/StandardHttpClient.cs
@@ -1,7 +1,9 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
+using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
@@ -10,24 +12,90 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http
{
private HttpClient _client;
private ILogger _logger;
- public HttpClient Inst => _client;
+
public StandardHttpClient(ILogger logger)
{
_client = new HttpClient();
_logger = logger;
}
-
- public Task GetStringAsync(string uri) =>
- _client.GetStringAsync(uri);
- public Task PostAsync(string uri, T item)
+ public async Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer")
{
- var contentString = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json");
- return _client.PostAsync(uri, contentString);
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
+
+ if (authorizationToken != null)
+ {
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
+ }
+
+ var response = await _client.SendAsync(requestMessage);
+
+ return await response.Content.ReadAsStringAsync();
}
- public Task DeleteAsync(string uri) =>
- _client.DeleteAsync(uri);
+ private async Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ if (method != HttpMethod.Post && method != HttpMethod.Put)
+ {
+ throw new ArgumentException("Value must be either post or put.", nameof(method));
+ }
+
+ // a new StringContent must be created for each retry
+ // as it is disposed after each call
+
+ var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
+
+ requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json");
+
+ if (authorizationToken != null)
+ {
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
+ }
+
+ if (requestId != null)
+ {
+ requestMessage.Headers.Add("x-requestid", requestId);
+ }
+
+ var response = await _client.SendAsync(requestMessage);
+
+ // raise exception if HttpResponseCode 500
+ // needed for circuit breaker to track fails
+
+ if (response.StatusCode == HttpStatusCode.InternalServerError)
+ {
+ throw new HttpRequestException();
+ }
+
+ return response;
+ }
+
+
+ public async Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ return await DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationToken);
+ }
+
+ public async Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ return await DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationToken);
+ }
+ public async Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer")
+ {
+ var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri);
+
+ if (authorizationToken != null)
+ {
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken);
+ }
+
+ if (requestId != null)
+ {
+ requestMessage.Headers.Add("x-requestid", requestId);
+ }
+
+ return await _client.SendAsync(requestMessage);
+ }
}
}
diff --git a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs
index 01a8b4728..4ca90f383 100644
--- a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs
+++ b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs
@@ -111,4 +111,3 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
}
}
-
diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs
index 8d78524ea..253b01e9c 100644
--- a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs
+++ b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs
@@ -1,13 +1,14 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries
{
+ using System.Collections.Generic;
using System.Threading.Tasks;
public interface IOrderQueries
{
Task GetOrderAsync(int id);
- Task GetOrdersAsync();
+ Task> GetOrdersAsync();
- Task GetCardTypesAsync();
+ Task> GetCardTypesAsync();
}
}
diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
index 9d909e254..e51cf04ce 100644
--- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
+++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
@@ -44,7 +44,7 @@
}
}
- public async Task GetOrdersAsync()
+ public async Task> GetOrdersAsync()
{
using (var connection = new SqlConnection(_connectionString))
{
@@ -58,7 +58,7 @@
}
}
- public async Task GetCardTypesAsync()
+ public async Task> GetCardTypesAsync()
{
using (var connection = new SqlConnection(_connectionString))
{
diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs
index d9a3752ed..902eb007b 100644
--- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs
+++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs
@@ -30,25 +30,21 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
[HttpPost]
public async Task CreateOrder([FromBody]CreateOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId)
{
- bool result = false;
+ bool commandResult = false;
if (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty)
{
var requestCreateOrder = new IdentifiedCommand(command, guid);
- result = await _mediator.SendAsync(requestCreateOrder);
+ commandResult = await _mediator.SendAsync(requestCreateOrder);
}
else
{
// If no x-requestid header is found we process the order anyway. This is just temporary to not break existing clients
// that aren't still updated. When all clients were updated this could be removed.
- result = await _mediator.SendAsync(command);
+ commandResult = await _mediator.SendAsync(command);
}
- if (result)
- {
- return Ok();
- }
+ return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
- return BadRequest();
}
[Route("{orderId:int}")]
@@ -72,8 +68,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
[HttpGet]
public async Task GetOrders()
{
- var orders = await _orderQueries
- .GetOrdersAsync();
+ var orderTask = _orderQueries.GetOrdersAsync();
+
+ var orders = await orderTask;
return Ok(orders);
}
diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs
index 7616a3230..086fbc904 100644
--- a/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs
+++ b/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs
@@ -50,6 +50,9 @@
if (Object.ReferenceEquals(this, obj))
return true;
+ if (this.GetType() != obj.GetType())
+ return false;
+
Entity item = (Entity)obj;
if (item.IsTransient() || this.IsTransient())
diff --git a/src/Web/WebMVC/Controllers/OrderController.cs b/src/Web/WebMVC/Controllers/OrderController.cs
index 90be194dc..83152d697 100644
--- a/src/Web/WebMVC/Controllers/OrderController.cs
+++ b/src/Web/WebMVC/Controllers/OrderController.cs
@@ -51,7 +51,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers
}
catch(BrokenCircuitException)
{
- ModelState.AddModelError("Error", "It was not possible to create a new order, please try later on");
+ ModelState.AddModelError("Error", "It was not possible to create a new order, please try later on. (Business Msg Due to Circuit-Breaker)");
}
return View(model);
}
diff --git a/src/Web/WebMVC/Infrastructure/API.cs b/src/Web/WebMVC/Infrastructure/API.cs
new file mode 100644
index 000000000..c837b8067
--- /dev/null
+++ b/src/Web/WebMVC/Infrastructure/API.cs
@@ -0,0 +1,68 @@
+namespace WebMVC.Infrastructure
+{
+ public static class API
+ {
+ public static class Basket
+ {
+ public static string GetBasket(string baseUri, string basketId)
+ {
+ return $"{baseUri}/{basketId}";
+ }
+
+ public static string UpdateBasket(string baseUri)
+ {
+ return baseUri;
+ }
+
+ public static string CleanBasket(string baseUri, string basketId)
+ {
+ return $"{baseUri}/{basketId}";
+ }
+ }
+
+ public static class Order
+ {
+ public static string GetOrder(string baseUri, string orderId)
+ {
+ return $"{baseUri}/{orderId}";
+ }
+
+ public static string GetAllMyOrders(string baseUri)
+ {
+ return baseUri;
+ }
+
+ public static string AddNewOrder(string baseUri)
+ {
+ return $"{baseUri}/new";
+ }
+ }
+
+ public static class Catalog
+ {
+ public static string GetAllCatalogItems(string baseUri, int page, int take, int? brand, int? type)
+ {
+ var filterQs = "";
+
+ if (brand.HasValue || type.HasValue)
+ {
+ var brandQs = (brand.HasValue) ? brand.Value.ToString() : "null";
+ var typeQs = (type.HasValue) ? type.Value.ToString() : "null";
+ filterQs = $"/type/{typeQs}/brand/{brandQs}";
+ }
+
+ return $"{baseUri}items{filterQs}?pageIndex={page}&pageSize={take}";
+ }
+
+ public static string GetAllBrands(string baseUri)
+ {
+ return $"{baseUri}catalogBrands";
+ }
+
+ public static string GetAllTypes(string baseUri)
+ {
+ return $"{baseUri}catalogTypes";
+ }
+ }
+ }
+}
diff --git a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs
index 8efadf366..8eb43179d 100644
--- a/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs
+++ b/src/Web/WebMVC/Infrastructure/ResilientHttpClientFactory.cs
@@ -17,7 +17,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Infrastructure
=>_logger = logger;
public ResilientHttpClient CreateResilientHttpClient()
- => new ResilientHttpClient(CreatePolicies(), _logger);
+ => new ResilientHttpClient((origin) => CreatePolicies(), _logger);
private Policy[] CreatePolicies()
diff --git a/src/Web/WebMVC/Services/BasketService.cs b/src/Web/WebMVC/Services/BasketService.cs
index 7d82a0fc6..bd418ea26 100644
--- a/src/Web/WebMVC/Services/BasketService.cs
+++ b/src/Web/WebMVC/Services/BasketService.cs
@@ -5,9 +5,8 @@ using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
using System.Threading.Tasks;
+using WebMVC.Infrastructure;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
@@ -28,15 +27,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task GetBasket(ApplicationUser user)
{
- var context = _httpContextAccesor.HttpContext;
- var token = await context.Authentication.GetTokenAsync("access_token");
+ var token = await GetUserTokenAsync();
+ var getBasketUri = API.Basket.GetBasket(_remoteServiceBaseUrl, user.Id);
- _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
+ var dataString = await _apiClient.GetStringAsync(getBasketUri, token);
- var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id}";
- var dataString = await _apiClient.GetStringAsync(basketUrl);
// Use the ?? Null conditional operator to simplify the initialization of response
- var response = JsonConvert.DeserializeObject(dataString) ??
+ var response = JsonConvert.DeserializeObject(dataString) ??
new Basket()
{
BuyerId = user.Id
@@ -47,14 +44,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task UpdateBasket(Basket basket)
{
- var context = _httpContextAccesor.HttpContext;
- var token = await context.Authentication.GetTokenAsync("access_token");
-
- _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
+ var token = await GetUserTokenAsync();
+ var updateBasketUri = API.Basket.UpdateBasket(_remoteServiceBaseUrl);
- var basketUrl = _remoteServiceBaseUrl;
-
- var response = await _apiClient.PostAsync(basketUrl, basket);
+ var response = await _apiClient.PostAsync(updateBasketUri, basket, token);
response.EnsureSuccessStatusCode();
@@ -88,7 +81,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
order.OrderItems.Add(new OrderItem()
{
ProductId = int.Parse(x.ProductId),
-
+
PictureUrl = x.PictureUrl,
ProductName = x.ProductName,
Units = x.Quantity,
@@ -102,7 +95,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task AddItemToBasket(ApplicationUser user, BasketItem product)
{
- Basket basket = await GetBasket(user);
+ var basket = await GetBasket(user);
+
if (basket == null)
{
basket = new Basket()
@@ -113,20 +107,25 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
}
basket.Items.Add(product);
+
await UpdateBasket(basket);
}
public async Task CleanBasket(ApplicationUser user)
{
- var context = _httpContextAccesor.HttpContext;
- var token = await context.Authentication.GetTokenAsync("access_token");
+ var token = await GetUserTokenAsync();
+ var cleanBasketUri = API.Basket.CleanBasket(_remoteServiceBaseUrl, user.Id);
+
+ var response = await _apiClient.DeleteAsync(cleanBasketUri, token);
- _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
- var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id}";
- var response = await _apiClient.DeleteAsync(basketUrl);
-
//CCE: response status code...
}
+
+ async Task GetUserTokenAsync()
+ {
+ var context = _httpContextAccesor.HttpContext;
+ return await context.Authentication.GetTokenAsync("access_token");
+ }
}
}
diff --git a/src/Web/WebMVC/Services/CatalogService.cs b/src/Web/WebMVC/Services/CatalogService.cs
index f7225ff0b..2af428e2e 100644
--- a/src/Web/WebMVC/Services/CatalogService.cs
+++ b/src/Web/WebMVC/Services/CatalogService.cs
@@ -7,43 +7,32 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
+using WebMVC.Infrastructure;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
public class CatalogService : ICatalogService
{
private readonly IOptionsSnapshot _settings;
- private IHttpClient _apiClient;
+ private readonly IHttpClient _apiClient;
+ private readonly ILogger _logger;
+
private readonly string _remoteServiceBaseUrl;
-
- public CatalogService(IOptionsSnapshot settings, ILoggerFactory loggerFactory, IHttpClient httpClient) {
+
+ public CatalogService(IOptionsSnapshot settings, IHttpClient httpClient, ILogger logger)
+ {
_settings = settings;
- _remoteServiceBaseUrl = $"{_settings.Value.CatalogUrl}/api/v1/catalog/";
_apiClient = httpClient;
- var log = loggerFactory.CreateLogger("catalog service");
- log.LogDebug(settings.Value.CatalogUrl);
- }
-
- public async Task GetCatalogItems(int page,int take, int? brand, int? type)
- {
- var itemsQs = $"items?pageIndex={page}&pageSize={take}";
- var filterQs = "";
+ _logger = logger;
- if (brand.HasValue || type.HasValue)
- {
- var brandQs = (brand.HasValue) ? brand.Value.ToString() : "null";
- var typeQs = (type.HasValue) ? type.Value.ToString() : "null";
- filterQs = $"/type/{typeQs}/brand/{brandQs}";
- }
-
- var catalogUrl = $"{_remoteServiceBaseUrl}items{filterQs}?pageIndex={page}&pageSize={take}";
+ _remoteServiceBaseUrl = $"{_settings.Value.CatalogUrl}/api/v1/catalog/";
+ }
- var dataString = "";
+ public async Task GetCatalogItems(int page, int take, int? brand, int? type)
+ {
+ var allcatalogItemsUri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl, page, take, brand, type);
- //
- // Using a HttpClient wrapper with Retry and Exponential Backoff
- //
- dataString = await _apiClient.GetStringAsync(catalogUrl);
+ var dataString = await _apiClient.GetStringAsync(allcatalogItemsUri);
var response = JsonConvert.DeserializeObject(dataString);
@@ -52,14 +41,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task> GetBrands()
{
- var url = $"{_remoteServiceBaseUrl}catalogBrands";
- var dataString = await _apiClient.GetStringAsync(url);
+ var getBrandsUri = API.Catalog.GetAllBrands(_remoteServiceBaseUrl);
+
+ var dataString = await _apiClient.GetStringAsync(getBrandsUri);
var items = new List();
items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true });
- JArray brands = JArray.Parse(dataString);
- foreach (JObject brand in brands.Children())
+ var brands = JArray.Parse(dataString);
+
+ foreach (var brand in brands.Children())
{
items.Add(new SelectListItem()
{
@@ -73,14 +64,15 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task> GetTypes()
{
- var url = $"{_remoteServiceBaseUrl}catalogTypes";
- var dataString = await _apiClient.GetStringAsync(url);
+ var getTypesUri = API.Catalog.GetAllTypes(_remoteServiceBaseUrl);
+
+ var dataString = await _apiClient.GetStringAsync(getTypesUri);
var items = new List();
items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true });
- JArray brands = JArray.Parse(dataString);
- foreach (JObject brand in brands.Children())
+ var brands = JArray.Parse(dataString);
+ foreach (var brand in brands.Children())
{
items.Add(new SelectListItem()
{
diff --git a/src/Web/WebMVC/Services/OrderingService.cs b/src/Web/WebMVC/Services/OrderingService.cs
index 570a48361..8f198fbed 100644
--- a/src/Web/WebMVC/Services/OrderingService.cs
+++ b/src/Web/WebMVC/Services/OrderingService.cs
@@ -1,14 +1,13 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.eShopOnContainers.WebMVC.ViewModels;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
+using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http;
+using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.Extensions.Options;
-using System.Net.Http;
using Newtonsoft.Json;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using WebMVC.Infrastructure;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
@@ -27,15 +26,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
_apiClient = httpClient;
}
- async public Task GetOrder(ApplicationUser user, string Id)
+ async public Task GetOrder(ApplicationUser user, string id)
{
- var context = _httpContextAccesor.HttpContext;
- var token = await context.Authentication.GetTokenAsync("access_token");
- _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
+ var token = await GetUserTokenAsync();
+ var getOrderUri = API.Order.GetOrder(_remoteServiceBaseUrl, id);
+
+ var dataString = await _apiClient.GetStringAsync(getOrderUri, token);
- var ordersUrl = $"{_remoteServiceBaseUrl}/{Id}";
- var dataString = await _apiClient.GetStringAsync(ordersUrl);
-
var response = JsonConvert.DeserializeObject(dataString);
return response;
@@ -43,16 +40,13 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
async public Task> GetMyOrders(ApplicationUser user)
{
- var context = _httpContextAccesor.HttpContext;
- var token = await context.Authentication.GetTokenAsync("access_token");
+ var token = await GetUserTokenAsync();
+ var allMyOrdersUri = API.Order.GetAllMyOrders(_remoteServiceBaseUrl);
- _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
-
- var ordersUrl = _remoteServiceBaseUrl;
- var dataString = await _apiClient.GetStringAsync(ordersUrl);
+ var dataString = await _apiClient.GetStringAsync(allMyOrdersUri, token);
var response = JsonConvert.DeserializeObject>(dataString);
- return response;
+ return response;
}
public Order MapUserInfoIntoOrder(ApplicationUser user, Order order)
@@ -62,10 +56,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
order.State = user.State;
order.Country = user.Country;
order.ZipCode = user.ZipCode;
-
+
order.CardNumber = user.CardNumber;
order.CardHolderName = user.CardHolderName;
- order.CardExpiration = new DateTime(int.Parse("20" + user.Expiration.Split('/')[1]),int.Parse(user.Expiration.Split('/')[0]), 1);
+ order.CardExpiration = new DateTime(int.Parse("20" + user.Expiration.Split('/')[1]), int.Parse(user.Expiration.Split('/')[0]), 1);
order.CardSecurityNumber = user.SecurityNumber;
return order;
@@ -73,21 +67,21 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
async public Task CreateOrder(Order order)
{
- var context = _httpContextAccesor.HttpContext;
- var token = await context.Authentication.GetTokenAsync("access_token");
-
- _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
- _apiClient.Inst.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString());
+ var token = await GetUserTokenAsync();
+ var requestId = order.RequestId.ToString();
+ var addNewOrderUri = API.Order.AddNewOrder(_remoteServiceBaseUrl);
- var ordersUrl = $"{_remoteServiceBaseUrl}/new";
order.CardTypeId = 1;
order.CardExpirationApiFormat();
+
SetFakeIdToProducts(order);
- var response = await _apiClient.PostAsync(ordersUrl, order);
+ var response = await _apiClient.PostAsync(addNewOrderUri, order, token, requestId);
if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
- throw new Exception("Error creating order, try later");
+ {
+ throw new Exception("Error creating order, try later.");
+ }
response.EnsureSuccessStatusCode();
}
@@ -106,10 +100,17 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
destination.CardSecurityNumber = original.CardSecurityNumber;
}
- private void SetFakeIdToProducts(Order order)
+ void SetFakeIdToProducts(Order order)
{
var id = 1;
order.OrderItems.ForEach(x => { x.ProductId = id; id++; });
}
+
+ async Task GetUserTokenAsync()
+ {
+ var context = _httpContextAccesor.HttpContext;
+
+ return await context.Authentication.GetTokenAsync("access_token");
+ }
}
}
diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs
index e86c88c04..afc266051 100644
--- a/src/Web/WebMVC/Startup.cs
+++ b/src/Web/WebMVC/Startup.cs
@@ -70,11 +70,11 @@ namespace Microsoft.eShopOnContainers.WebMVC
if (Configuration.GetValue("UseResilientHttp") == bool.TrueString)
{
services.AddTransient();
- services.AddTransient(sp => sp.GetService().CreateResilientHttpClient());
+ services.AddSingleton(sp => sp.GetService().CreateResilientHttpClient());
}
else
{
- services.AddTransient();
+ services.AddSingleton();
}
}
diff --git a/src/Web/WebMVC/Views/Shared/_Layout.cshtml b/src/Web/WebMVC/Views/Shared/_Layout.cshtml
index 456248b94..990018b09 100644
--- a/src/Web/WebMVC/Views/Shared/_Layout.cshtml
+++ b/src/Web/WebMVC/Views/Shared/_Layout.cshtml
@@ -55,7 +55,7 @@
-
+
diff --git a/src/Web/WebMVC/wwwroot/css/site.min.css b/src/Web/WebMVC/wwwroot/css/site.min.css
index f5fc90999..2a4a53b47 100644
--- a/src/Web/WebMVC/wwwroot/css/site.min.css
+++ b/src/Web/WebMVC/wwwroot/css/site.min.css
@@ -1 +1 @@
-.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");height:7px;content:'';position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}}
\ No newline at end of file
+.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");height:7px;content:'';position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}
\ No newline at end of file
diff --git a/src/Web/WebSPA/AppSettings.cs b/src/Web/WebSPA/AppSettings.cs
index 19e2b5cfe..9fab0d981 100644
--- a/src/Web/WebSPA/AppSettings.cs
+++ b/src/Web/WebSPA/AppSettings.cs
@@ -7,6 +7,7 @@ namespace eShopOnContainers.WebSPA
{
public class AppSettings
{
+ public string BaseUrl { get; set; }
public string CatalogUrl { get; set; }
public string OrderingUrl { get; set; }
public string IdentityUrl { get; set; }
diff --git a/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts b/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts
index 6921fd167..12fa85815 100644
--- a/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts
+++ b/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts
@@ -22,7 +22,8 @@ export class ConfigurationService {
constructor(private http: Http, private storageService: StorageService) { }
load() {
- let url = "/Home/Configuration";
+ const baseURI = document.baseURI.endsWith('/') ? document.baseURI : `${document.baseURI}/`;
+ let url = `${baseURI}Home/Configuration`;
this.http.get(url).subscribe((response: Response) => {
console.log('server settings loaded');
this.serverSettings = response.json();
diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs
index f06cb8ee1..ee932f476 100644
--- a/src/Web/WebSPA/Startup.cs
+++ b/src/Web/WebSPA/Startup.cs
@@ -33,6 +33,9 @@ namespace eShopConContainers.WebSPA
}
Configuration = builder.Build();
+
+ var localPath = new Uri(Configuration["ASPNETCORE_URLS"])?.LocalPath ?? "/";
+ Configuration["BaseUrl"] = localPath;
}
public static IConfigurationRoot Configuration { get; set;}
diff --git a/src/Web/WebSPA/WebSPA.csproj b/src/Web/WebSPA/WebSPA.csproj
index cebe4377d..03c3cd442 100644
--- a/src/Web/WebSPA/WebSPA.csproj
+++ b/src/Web/WebSPA/WebSPA.csproj
@@ -72,6 +72,13 @@
+
+
+
+
+
+
+
diff --git a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs
index 8c7659862..c0656f050 100644
--- a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs
+++ b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs
@@ -6,6 +6,7 @@ using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers;
using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services;
using Moq;
using System;
+using System.Linq;
using System.Threading.Tasks;
using Xunit;
@@ -59,7 +60,7 @@ namespace UnitTest.Ordering.Application
public async Task Get_orders_success()
{
//Arrange
- var fakeDynamicResult = new Object();
+ var fakeDynamicResult = Enumerable.Empty