Merge branch 'dev' into feature/118
This commit is contained in:
commit
4f62175592
18
README.md
18
README.md
@ -17,12 +17,12 @@ The **dockerfiles** in the solution have also been updated and now support [**Do
|
|||||||
>**PLEASE** Read our [branch guide](./branch-guide.md) to know about our branching policy
|
>**PLEASE** Read our [branch guide](./branch-guide.md) to know about our branching policy
|
||||||
|
|
||||||
> ### DISCLAIMER
|
> ### DISCLAIMER
|
||||||
> **IMPORTANT:** The current state of this sample application is **BETA**, because we are constantly evolving towards new released technologies. 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.
|
> **IMPORTANT:** The current state of this sample application is **BETA**, because we are constantly evolving towards newly released technologies. Therefore, many areas could be improved and change significantly while refactoring the current code and implementing new features. Feedback with improvements and pull requests from the community will be highly appreciated and accepted.
|
||||||
>
|
>
|
||||||
> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. The chosen domain is an eShop/eCommerce but simply because it is a well-know domain by most people/developers.
|
> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. The chosen domain is eShop/eCommerce but simply because it is a well-known domain by most people/developers.
|
||||||
However, this sample application should not be considered as an "eCommerce reference model", at all. The implemented business domain might not be ideal from an eCommerce business point of view. It is neither trying to solve all the problems in a large, scalable and mission-critical distributed system. It is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core.
|
However, this sample application should not be considered as an "eCommerce reference model" at all. The implemented business domain might not be ideal from an eCommerce business point of view. It is neither trying to solve all the problems in a large, scalable and mission-critical distributed system. It is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core.
|
||||||
> <p>For example, the next step after running the solution in the local dev PC and understanding Docker containers and microservices development with .NET Core, is to select a microservice cluster/orchestrator like Kubernetes in Azure (AKS) or Azure Service Fabric, both environments tested and supported by this solution.
|
> <p>For example, the next step after running the solution in the local dev PC and understanding Docker containers and microservices development with .NET Core, is to select a microservice cluster/orchestrator like Kubernetes in Azure (AKS) or Azure Service Fabric, both environments tested and supported by this solution.
|
||||||
> Additional steps would be to move your databases to HA cloud services (like Azure SQL Database), or switch your EventBus to use Azure Service Bus (instead of bare-bone RabbitMQ) or any other production ready Service Bus in the market.
|
> Additional steps would be to move your databases to HA cloud services (like Azure SQL Database) or switch your EventBus to use Azure Service Bus (instead of bare-bone RabbitMQ) or any other production-ready Service Bus in the market.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -39,7 +39,7 @@ The architecture proposes a microservice oriented architecture implementation wi
|
|||||||
> ### Important Note on API Gateways and published APIs
|
> ### Important Note on API Gateways and published APIs
|
||||||
> Since April 2018, we have introduced the implementation of the [API Gateway pattern](http://microservices.io/patterns/apigateway.html) and [Backend-For-Front-End (BFF) pattern](https://samnewman.io/patterns/architectural/bff/) in eShopOnContainers architecture, so you can filter and publish simplified APIs and URIs and apply additional security in that tier while hiding/securing the internal microservices to the client apps or outside consumers. These sample API Gateways in eShopOnContainers are based on [Ocelot](https://github.com/ThreeMammals/Ocelot), an OSS lightweight API Gateway solution explained [here](http://threemammals.com/ocelot). The deployed API Gateways are autonomous and can be deployed as your own custom microservices/containers, as it is currently done in eShopOnContainers, so you can test it even in a simple development environment with just Docker engine or deploy it into orchestrators like Kubernetes in AKS or Service Fabric.
|
> Since April 2018, we have introduced the implementation of the [API Gateway pattern](http://microservices.io/patterns/apigateway.html) and [Backend-For-Front-End (BFF) pattern](https://samnewman.io/patterns/architectural/bff/) in eShopOnContainers architecture, so you can filter and publish simplified APIs and URIs and apply additional security in that tier while hiding/securing the internal microservices to the client apps or outside consumers. These sample API Gateways in eShopOnContainers are based on [Ocelot](https://github.com/ThreeMammals/Ocelot), an OSS lightweight API Gateway solution explained [here](http://threemammals.com/ocelot). The deployed API Gateways are autonomous and can be deployed as your own custom microservices/containers, as it is currently done in eShopOnContainers, so you can test it even in a simple development environment with just Docker engine or deploy it into orchestrators like Kubernetes in AKS or Service Fabric.
|
||||||
|
|
||||||
> For your production-ready architecture you can either keep using [Ocelot](https://github.com/ThreeMammals/Ocelot) which is simple and easy to use and used in production by significant companies or if you need further functionality and a much richer set of features suittable for commercial APIs, you can also substitute those API Gateways and use [Azure API Management](https://azure.microsoft.com/en-us/services/api-management/) or any other commercial API Gateway, as shown in the following image.
|
> For your production-ready architecture you can either keep using [Ocelot](https://github.com/ThreeMammals/Ocelot) which is simple and easy to use and used in production by significant companies or if you need further functionality and a much richer set of features suitable for commercial APIs, you can also substitute those API Gateways and use [Azure API Management](https://azure.microsoft.com/en-us/services/api-management/) or any other commercial API Gateway, as shown in the following image.
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<img src="img/eShopOnContainers-Architecture-With-Azure-API-Management.png">
|
<img src="img/eShopOnContainers-Architecture-With-Azure-API-Management.png">
|
||||||
@ -56,9 +56,9 @@ The architecture proposes a microservice oriented architecture implementation wi
|
|||||||
<p>
|
<p>
|
||||||
|
|
||||||
> ### Important Note on Database Servers/Containers
|
> ### Important Note on Database Servers/Containers
|
||||||
> In this solution's current configuration for a development environment, the SQL databases are automatically deployed with sample data into a single SQL Server container (a single shared Docker container for SQL databases) so the whole solution can be up and running without any dependency to any cloud or specific server. Each database could also be deployed as a single Docker container, but then you'd need more than 8GB of RAM assigned to Docker in your development machine in order to be able to run 3 SQL Server Docker containers in your Docker Linux host in "Docker for Windows" or "Docker for Mac" development environments.
|
> In this solution's current configuration for a development environment, the SQL databases are automatically deployed with sample data into a single SQL Server container (a single shared Docker container for SQL databases) so the whole solution can be up and running without any dependency to any cloud or a specific server. Each database could also be deployed as a single Docker container, but then you'd need more than 8GB of RAM assigned to Docker in your development machine in order to be able to run 3 SQL Server Docker containers in your Docker Linux host in "Docker for Windows" or "Docker for Mac" development environments.
|
||||||
> <p> A similar case is defined in regard to Redis cache running as a container for the development environment. Or a No-SQL database (MongoDB) running as a container.
|
> <p> A similar case is defined in regard to Redis cache running as a container for the development environment. Or a No-SQL database (MongoDB) running as a container.
|
||||||
> <p> However, in a real production environment it is recommended to have your databases (SQL Server, Redis, and the NO-SQL database, in this case) in HA (High Available) services like Azure SQL Database, Redis as a service and Azure CosmosDB instead the MongoDB container (as both systems share the same access protocol). If you want to change to a production configuration, you'll just need to change the connection strings once you have set up the servers in a HA cloud or on-premises.
|
> <p> However, in a real production environment it is recommended to have your databases (SQL Server, Redis, and the NO-SQL database, in this case) in HA (High Available) services like Azure SQL Database, Redis as a service and Azure CosmosDB instead the MongoDB container (as both systems share the same access protocol). If you want to change to a production configuration, you'll just need to change the connection strings once you have set up the servers in an HA cloud or on-premises.
|
||||||
|
|
||||||
## Related documentation and guidance
|
## Related documentation and guidance
|
||||||
While developing this reference application, we've been creating a reference <b>Guide/eBook</b> focusing on <b>architecting and developing containerized and microservice based .NET Applications</b> (download link available below) which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers.
|
While developing this reference application, we've been creating a reference <b>Guide/eBook</b> focusing on <b>architecting and developing containerized and microservice based .NET Applications</b> (download link available below) which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers.
|
||||||
@ -76,12 +76,12 @@ Download in other formats (**eReaders** like **MOBI**, **EPUB**) and other eBook
|
|||||||
|
|
||||||
Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)
|
Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)
|
||||||
|
|
||||||
However, we encourage to download and review the [Architecting and Developing Microservices eBook](https://aka.ms/microservicesebook) because the architectural styles and architectural patterns and technologies explained in the guidance are using this reference application when explaining many pattern implementations, so you'll understand much better the context, design and decisions taken in the current architecture and internal designs.
|
However, we encourage you to download and review the [Architecting and Developing Microservices eBook](https://aka.ms/microservicesebook) because the architectural styles and architectural patterns and technologies explained in the guide are using this reference application when explaining many pattern implementations, so you'll understand the context, design and decisions taken in the current architecture and internal designs much better.
|
||||||
|
|
||||||
## Overview of the application code
|
## Overview of the application code
|
||||||
In this repo you can find a sample reference application that will help you to understand how to implement a microservice architecture based application using <b>.NET Core</b> and <b>Docker</b>.
|
In this repo you can find a sample reference application that will help you to understand how to implement a microservice architecture based application using <b>.NET Core</b> and <b>Docker</b>.
|
||||||
|
|
||||||
The example business domain or scenario is based on an eShop or eCommerce which is implemented as a multi-container application. Each container is a microservice deployment (like the basket-microservice, catalog-microservice, ordering-microservice and the identity-microservice) which are developed using ASP.NET Core running on .NET Core so they can run either on Linux Containers and Windows Containers.
|
The example business domain or scenario is based on an eShop or eCommerce which is implemented as a multi-container application. Each container is a microservice deployment (like the basket-microservice, catalog-microservice, ordering-microservice and the identity-microservice) which is developed using ASP.NET Core running on .NET Core so they can run either on Linux Containers and Windows Containers.
|
||||||
The screenshot below shows the VS Solution structure for those microservices/containers and client apps.
|
The screenshot below shows the VS Solution structure for those microservices/containers and client apps.
|
||||||
|
|
||||||
- (*Recommended when getting started*) Open <b>eShopOnContainers-ServicesAndWebApps.sln</b> for a solution containing just the server-side projects related to the microservices and web applications.
|
- (*Recommended when getting started*) Open <b>eShopOnContainers-ServicesAndWebApps.sln</b> for a solution containing just the server-side projects related to the microservices and web applications.
|
||||||
|
BIN
docs/Decks/BRK3175_CesarDeIaTorre.pptx
Normal file
BIN
docs/Decks/BRK3175_CesarDeIaTorre.pptx
Normal file
Binary file not shown.
BIN
docs/Decks/eShopOnContainers-Architecture-v2.1.pptx
Normal file
BIN
docs/Decks/eShopOnContainers-Architecture-v2.1.pptx
Normal file
Binary file not shown.
@ -1,3 +1 @@
|
|||||||
kubectl patch deployment -n ingress-nginx nginx-ingress-controller --type=json --patch="$(cat nginx-ingress\publish-service-patch.yaml)"
|
kubectl apply -f nginx-ingress\cloud-generic.yaml
|
||||||
kubectl apply -f nginx-ingress\azure\service.yaml
|
|
||||||
kubectl apply -f nginx-ingress\patch-service-without-rbac.yaml
|
|
2
k8s/deploy-ingress-dockerlocal.ps1
Normal file
2
k8s/deploy-ingress-dockerlocal.ps1
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
kubectl apply -f nginx-ingress\cm.yaml
|
||||||
|
kubectl apply -f nginx-ingress\cloud-generic.yaml
|
@ -1,12 +1,5 @@
|
|||||||
kubectl apply -f ingress.yaml
|
|
||||||
|
|
||||||
# Deploy nginx-ingress core files
|
# Deploy nginx-ingress core files
|
||||||
kubectl apply -f nginx-ingress\namespace.yaml
|
kubectl apply -f nginx-ingress\mandatory.yaml
|
||||||
kubectl apply -f nginx-ingress\default-backend.yaml
|
|
||||||
kubectl apply -f nginx-ingress\configmap.yaml
|
|
||||||
kubectl apply -f nginx-ingress\tcp-services-configmap.yaml
|
|
||||||
kubectl apply -f nginx-ingress\udp-services-configmap.yaml
|
|
||||||
kubectl apply -f nginx-ingress\without-rbac.yaml
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,6 +113,7 @@ ExecKube -cmd 'delete configmap internalurls'
|
|||||||
ExecKube -cmd 'delete configmap urls'
|
ExecKube -cmd 'delete configmap urls'
|
||||||
ExecKube -cmd 'delete configmap externalcfg'
|
ExecKube -cmd 'delete configmap externalcfg'
|
||||||
ExecKube -cmd 'delete configmap ocelot'
|
ExecKube -cmd 'delete configmap ocelot'
|
||||||
|
ExecKube -cmd 'delete -f ingress.yaml'
|
||||||
|
|
||||||
# start sql, rabbitmq, frontend deployments
|
# start sql, rabbitmq, frontend deployments
|
||||||
if ($deployInfrastructure) {
|
if ($deployInfrastructure) {
|
||||||
@ -204,5 +205,8 @@ ExecKube -cmd 'rollout resume deployments/apigwwm'
|
|||||||
ExecKube -cmd 'rollout resume deployments/apigwws'
|
ExecKube -cmd 'rollout resume deployments/apigwws'
|
||||||
ExecKube -cmd 'rollout resume deployments/ordering-signalrhub'
|
ExecKube -cmd 'rollout resume deployments/ordering-signalrhub'
|
||||||
|
|
||||||
|
Write-Host "Adding/Updating ingress resource..." -ForegroundColor Yellow
|
||||||
|
ExecKube -cmd 'apply -f ingress.yaml'
|
||||||
|
|
||||||
Write-Host "WebSPA is exposed at http://$externalDns, WebMVC at http://$externalDns/webmvc, WebStatus at http://$externalDns/webstatus" -ForegroundColor Yellow
|
Write-Host "WebSPA is exposed at http://$externalDns, WebMVC at http://$externalDns/webmvc, WebStatus at http://$externalDns/webstatus" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
18
k8s/helm-rbac.yaml
Normal file
18
k8s/helm-rbac.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: tiller
|
||||||
|
namespace: kube-system
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: tiller
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: cluster-admin
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: tiller
|
||||||
|
namespace: kube-system
|
@ -27,7 +27,7 @@ app: # app global settings
|
|||||||
catalog: catalog # service name for catalog api
|
catalog: catalog # service name for catalog api
|
||||||
ordering: ordering # service name for ordering api
|
ordering: ordering # service name for ordering api
|
||||||
orderingbackgroundtasks: orderingbackgroundtasks # service name for orderingbackgroundtasks
|
orderingbackgroundtasks: orderingbackgroundtasks # service name for orderingbackgroundtasks
|
||||||
orderingsignalrhub: orderingsignalrhub # service name for orderingsignalrhub
|
orderingsignalrhub: ordering-signalrhub # service name for orderingsignalrhub
|
||||||
identity: identity # service name for identity api
|
identity: identity # service name for identity api
|
||||||
mvc: webmvc # service name for web mvc
|
mvc: webmvc # service name for web mvc
|
||||||
spa: webspa # service name for web spa
|
spa: webspa # service name for web spa
|
||||||
|
@ -8,11 +8,19 @@ Param(
|
|||||||
[parameter(Mandatory=$false)][bool]$clean=$true,
|
[parameter(Mandatory=$false)][bool]$clean=$true,
|
||||||
[parameter(Mandatory=$false)][string]$aksName="",
|
[parameter(Mandatory=$false)][string]$aksName="",
|
||||||
[parameter(Mandatory=$false)][string]$aksRg="",
|
[parameter(Mandatory=$false)][string]$aksRg="",
|
||||||
[parameter(Mandatory=$false)][string]$imageTag="latest"
|
[parameter(Mandatory=$false)][string]$imageTag="latest",
|
||||||
)
|
[parameter(Mandatory=$false)][bool]$useLocalk8s=$false
|
||||||
|
)
|
||||||
|
|
||||||
$dns = $externalDns
|
$dns = $externalDns
|
||||||
|
|
||||||
|
$ingressValuesFile="ingress_values.yaml"
|
||||||
|
|
||||||
|
if ($ingressValuesFile) {
|
||||||
|
$ingressValuesFile="ingress_values_dockerk8s.yaml"
|
||||||
|
$dns="localhost"
|
||||||
|
}
|
||||||
|
|
||||||
if ($externalDns -eq "aks") {
|
if ($externalDns -eq "aks") {
|
||||||
if ([string]::IsNullOrEmpty($aksName) -or [string]::IsNullOrEmpty($aksRg)) {
|
if ([string]::IsNullOrEmpty($aksName) -or [string]::IsNullOrEmpty($aksRg)) {
|
||||||
Write-Host "Error: When using -dns aks, MUST set -aksName and -aksRg too." -ForegroundColor Red
|
Write-Host "Error: When using -dns aks, MUST set -aksName and -aksRg too." -ForegroundColor Red
|
||||||
@ -58,18 +66,18 @@ $charts = ("eshop-common", "apigwmm", "apigwms", "apigwwm", "apigwws", "basket-a
|
|||||||
if ($deployInfrastructure) {
|
if ($deployInfrastructure) {
|
||||||
foreach ($infra in $infras) {
|
foreach ($infra in $infras) {
|
||||||
Write-Host "Installing infrastructure: $infra" -ForegroundColor Green
|
Write-Host "Installing infrastructure: $infra" -ForegroundColor Green
|
||||||
helm install --values app.yaml --values inf.yaml --values ingress_values.yaml --set app.name=$appName --set inf.k8s.dns=$dns --name="$appName-$infra" $infra
|
helm install --values app.yaml --values inf.yaml --values $ingressValuesFile --set app.name=$appName --set inf.k8s.dns=$dns --name="$appName-$infra" $infra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($chart in $charts) {
|
foreach ($chart in $charts) {
|
||||||
Write-Host "Installing: $chart" -ForegroundColor Green
|
Write-Host "Installing: $chart" -ForegroundColor Green
|
||||||
if ($useCustomRegistry) {
|
if ($useCustomRegistry) {
|
||||||
helm install --set inf.registry.server=$registry --set inf.registry.login=$dockerUser --set inf.registry.pwd=$dockerPassword --set inf.registry.secretName=eshop-docker-scret --values app.yaml --values inf.yaml --values ingress_values.yaml --set app.name=$appName --set inf.k8s.dns=$dns --set image.tag=$imageTag --set image.pullPolicy=Always --name="$appName-$chart" $chart
|
helm install --set inf.registry.server=$registry --set inf.registry.login=$dockerUser --set inf.registry.pwd=$dockerPassword --set inf.registry.secretName=eshop-docker-scret --values app.yaml --values inf.yaml --values $ingressValuesFile --set app.name=$appName --set inf.k8s.dns=$dns --set image.tag=$imageTag --set image.pullPolicy=Always --name="$appName-$chart" $chart
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if ($chart -ne "eshop-common") { # eshop-common is ignored when no secret must be deployed
|
if ($chart -ne "eshop-common") { # eshop-common is ignored when no secret must be deployed
|
||||||
helm install --values app.yaml --values inf.yaml --values ingress_values.yaml --set app.name=$appName --set inf.k8s.dns=$dns --set image.tag=$imageTag --set image.pullPolicy=Always --name="$appName-$chart" $chart
|
helm install --values app.yaml --values inf.yaml --values $ingressValuesFile --set app.name=$appName --set inf.k8s.dns=$dns --set image.tag=$imageTag --set image.pullPolicy=Always --name="$appName-$chart" $chart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
k8s/helm/ingress_values_dockerk8s.yaml
Normal file
5
k8s/helm/ingress_values_dockerk8s.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ingress:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: "nginx"
|
||||||
|
ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
@ -15,4 +15,4 @@ data:
|
|||||||
all__InstrumentationKey: {{ .Values.inf.appinsights.key }}
|
all__InstrumentationKey: {{ .Values.inf.appinsights.key }}
|
||||||
all__UseAzureServiceBus: "{{ .Values.inf.eventbus.useAzure }}"
|
all__UseAzureServiceBus: "{{ .Values.inf.eventbus.useAzure }}"
|
||||||
signalr__StoreConnectionString: {{ .Values.inf.redis.keystore.constr }}
|
signalr__StoreConnectionString: {{ .Values.inf.redis.keystore.constr }}
|
||||||
urls__IdentityUrl: {{ $identity }}
|
urls__IdentityUrl: http://{{ $identity }}
|
@ -1,19 +0,0 @@
|
|||||||
kind: Service
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: ingress-nginx
|
|
||||||
namespace: ingress-nginx
|
|
||||||
labels:
|
|
||||||
app: ingress-nginx
|
|
||||||
spec:
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
type: LoadBalancer
|
|
||||||
selector:
|
|
||||||
app: ingress-nginx
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: http
|
|
||||||
- name: https
|
|
||||||
port: 443
|
|
||||||
targetPort: https
|
|
21
k8s/nginx-ingress/cloud-generic.yaml
Normal file
21
k8s/nginx-ingress/cloud-generic.yaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
spec:
|
||||||
|
externalTrafficPolicy: Local
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: https
|
BIN
k8s/nginx-ingress/cm.yaml
Normal file
BIN
k8s/nginx-ingress/cm.yaml
Normal file
Binary file not shown.
@ -1,11 +0,0 @@
|
|||||||
kind: ConfigMap
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: nginx-configuration
|
|
||||||
namespace: ingress-nginx
|
|
||||||
labels:
|
|
||||||
app: ingress-nginx
|
|
||||||
data:
|
|
||||||
ssl-redirect: "false"
|
|
||||||
proxy-buffer-size: "128k"
|
|
||||||
proxy-buffers: "4 256k"
|
|
@ -1,52 +0,0 @@
|
|||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: default-http-backend
|
|
||||||
labels:
|
|
||||||
app: default-http-backend
|
|
||||||
namespace: ingress-nginx
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: default-http-backend
|
|
||||||
spec:
|
|
||||||
terminationGracePeriodSeconds: 60
|
|
||||||
containers:
|
|
||||||
- name: default-http-backend
|
|
||||||
# Any image is permissable as long as:
|
|
||||||
# 1. It serves a 404 page at /
|
|
||||||
# 2. It serves 200 on a /healthz endpoint
|
|
||||||
image: gcr.io/google_containers/defaultbackend:1.4
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 8080
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
timeoutSeconds: 5
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 10m
|
|
||||||
memory: 20Mi
|
|
||||||
requests:
|
|
||||||
cpu: 10m
|
|
||||||
memory: 20Mi
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: default-http-backend
|
|
||||||
namespace: ingress-nginx
|
|
||||||
labels:
|
|
||||||
app: default-http-backend
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
selector:
|
|
||||||
app: default-http-backend
|
|
@ -0,0 +1,3 @@
|
|||||||
|
data:
|
||||||
|
mvc_e: http://10.0.75.1/webmvc
|
||||||
|
|
3
k8s/nginx-ingress/local-dockerk8s/mvc-cm-fix.yaml
Normal file
3
k8s/nginx-ingress/local-dockerk8s/mvc-cm-fix.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
data:
|
||||||
|
urls__IdentityUrl: http://10.0.75.1/identity
|
||||||
|
urls__mvc: http://10.0.75.1/webmvc
|
39
k8s/nginx-ingress/local-dockerk8s/mvc-fix.yaml
Normal file
39
k8s/nginx-ingress/local-dockerk8s/mvc-fix.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
kubernetes.io/ingress.class: nginx
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
labels:
|
||||||
|
app: webmvc
|
||||||
|
name: eshop-webmvc-loopback
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: webmvc
|
||||||
|
servicePort: http
|
||||||
|
path: /webmvc
|
||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
kubernetes.io/ingress.class: nginx
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
labels:
|
||||||
|
app: identity-api
|
||||||
|
name: eshop-identity-api-loopback
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: identity
|
||||||
|
servicePort: http
|
||||||
|
path: /identity
|
238
k8s/nginx-ingress/mandatory.yaml
Normal file
238
k8s/nginx-ingress/mandatory.yaml
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
kind: ConfigMap
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: nginx-configuration
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-serviceaccount
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-clusterrole
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- endpoints
|
||||||
|
- nodes
|
||||||
|
- pods
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- nodes
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- "extensions"
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- "extensions"
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-role
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- pods
|
||||||
|
- secrets
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
resourceNames:
|
||||||
|
# Defaults to "<election-id>-<ingress-class>"
|
||||||
|
# Here: "<ingress-controller-leader>-<nginx>"
|
||||||
|
# This has to be adapted if you change either parameter
|
||||||
|
# when launching the nginx-ingress-controller.
|
||||||
|
- "ingress-controller-leader-nginx"
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- endpoints
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-role-nisa-binding
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: nginx-ingress-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: nginx-ingress-serviceaccount
|
||||||
|
namespace: ingress-nginx
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-clusterrole-nisa-binding
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: nginx-ingress-clusterrole
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: nginx-ingress-serviceaccount
|
||||||
|
namespace: ingress-nginx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-controller
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
annotations:
|
||||||
|
prometheus.io/port: "10254"
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
spec:
|
||||||
|
serviceAccountName: nginx-ingress-serviceaccount
|
||||||
|
containers:
|
||||||
|
- name: nginx-ingress-controller
|
||||||
|
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.20.0
|
||||||
|
args:
|
||||||
|
- /nginx-ingress-controller
|
||||||
|
- --configmap=$(POD_NAMESPACE)/nginx-configuration
|
||||||
|
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
|
||||||
|
- --annotations-prefix=nginx.ingress.kubernetes.io
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
# www-data -> 33
|
||||||
|
runAsUser: 33
|
||||||
|
env:
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
- name: https
|
||||||
|
containerPort: 443
|
||||||
|
livenessProbe:
|
||||||
|
failureThreshold: 3
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 10254
|
||||||
|
scheme: HTTP
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
timeoutSeconds: 1
|
||||||
|
readinessProbe:
|
||||||
|
failureThreshold: 3
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 10254
|
||||||
|
scheme: HTTP
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
timeoutSeconds: 1
|
@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: ingress-nginx
|
|
@ -1,40 +0,0 @@
|
|||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: nginx-ingress-controller
|
|
||||||
namespace: ingress-nginx
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: ingress-nginx
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: ingress-nginx
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: nginx-ingress-controller
|
|
||||||
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.9.0
|
|
||||||
args:
|
|
||||||
- /nginx-ingress-controller
|
|
||||||
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
|
|
||||||
- --configmap=$(POD_NAMESPACE)/nginx-configuration
|
|
||||||
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
|
|
||||||
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
|
|
||||||
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
|
|
||||||
- --annotations-prefix=nginx.ingress.kubernetes.io
|
|
||||||
env:
|
|
||||||
- name: POD_NAME
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.name
|
|
||||||
- name: POD_NAMESPACE
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.namespace
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 80
|
|
||||||
- name: https
|
|
||||||
containerPort: 443
|
|
@ -1,7 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
'op': 'add',
|
|
||||||
'path': '/spec/template/spec/containers/0/args/-',
|
|
||||||
'value': '--publish-service=$(POD_NAMESPACE)/ingress-nginx'
|
|
||||||
}
|
|
||||||
]
|
|
22
k8s/nginx-ingress/service-nodeport.yaml
Normal file
22
k8s/nginx-ingress/service-nodeport.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: 443
|
||||||
|
protocol: TCP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/part-of: ingress-nginx
|
@ -1,5 +0,0 @@
|
|||||||
kind: ConfigMap
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: tcp-services
|
|
||||||
namespace: ingress-nginx
|
|
@ -1,5 +0,0 @@
|
|||||||
kind: ConfigMap
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: udp-services
|
|
||||||
namespace: ingress-nginx
|
|
@ -1,61 +0,0 @@
|
|||||||
apiVersion: extensions/v1beta1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: nginx-ingress-controller
|
|
||||||
namespace: ingress-nginx
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: ingress-nginx
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: ingress-nginx
|
|
||||||
annotations:
|
|
||||||
prometheus.io/port: '10254'
|
|
||||||
prometheus.io/scrape: 'true'
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: nginx-ingress-controller
|
|
||||||
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.9.0
|
|
||||||
args:
|
|
||||||
- /nginx-ingress-controller
|
|
||||||
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
|
|
||||||
- --configmap=$(POD_NAMESPACE)/nginx-configuration
|
|
||||||
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
|
|
||||||
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
|
|
||||||
- --annotations-prefix=nginx.ingress.kubernetes.io
|
|
||||||
env:
|
|
||||||
- name: POD_NAME
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.name
|
|
||||||
- name: POD_NAMESPACE
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.namespace
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 80
|
|
||||||
- name: https
|
|
||||||
containerPort: 443
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 3
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 10254
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 1
|
|
||||||
readinessProbe:
|
|
||||||
failureThreshold: 3
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 10254
|
|
||||||
scheme: HTTP
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 1
|
|
@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.App" />
|
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="Ocelot" Version="3.0.0" />
|
<PackageReference Include="Ocelot" Version="12.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CacheManager.Core;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -217,14 +217,16 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
|||||||
if (subscription.IsDynamic)
|
if (subscription.IsDynamic)
|
||||||
{
|
{
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
||||||
|
if (handler == null) continue;
|
||||||
dynamic eventData = JObject.Parse(message);
|
dynamic eventData = JObject.Parse(message);
|
||||||
await handler.Handle(eventData);
|
await handler.Handle(eventData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||||
|
if (handler == null) continue;
|
||||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||||
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
|
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
|
||||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
||||||
}
|
}
|
||||||
|
@ -163,14 +163,16 @@
|
|||||||
if (subscription.IsDynamic)
|
if (subscription.IsDynamic)
|
||||||
{
|
{
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
||||||
|
if (handler == null) continue;
|
||||||
dynamic eventData = JObject.Parse(message);
|
dynamic eventData = JObject.Parse(message);
|
||||||
await handler.Handle(eventData);
|
await handler.Handle(eventData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
|
||||||
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
|
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||||
|
if (handler == null) continue;
|
||||||
|
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||||
|
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
|
||||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
|||||||
public enum EventStateEnum
|
public enum EventStateEnum
|
||||||
{
|
{
|
||||||
NotPublished = 0,
|
NotPublished = 0,
|
||||||
Published = 1,
|
InProgress = 1,
|
||||||
PublishedFailed = 2
|
Published = 2,
|
||||||
|
PublishedFailed = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@ using System.Collections.Generic;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||||
|
using System.Linq;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
||||||
{
|
{
|
||||||
@ -11,7 +14,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
|||||||
private IntegrationEventLogEntry() { }
|
private IntegrationEventLogEntry() { }
|
||||||
public IntegrationEventLogEntry(IntegrationEvent @event)
|
public IntegrationEventLogEntry(IntegrationEvent @event)
|
||||||
{
|
{
|
||||||
EventId = @event.Id;
|
EventId = @event.Id;
|
||||||
CreationTime = @event.CreationDate;
|
CreationTime = @event.CreationDate;
|
||||||
EventTypeName = @event.GetType().FullName;
|
EventTypeName = @event.GetType().FullName;
|
||||||
Content = JsonConvert.SerializeObject(@event);
|
Content = JsonConvert.SerializeObject(@event);
|
||||||
@ -20,9 +23,19 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
|||||||
}
|
}
|
||||||
public Guid EventId { get; private set; }
|
public Guid EventId { get; private set; }
|
||||||
public string EventTypeName { get; private set; }
|
public string EventTypeName { get; private set; }
|
||||||
|
[NotMapped]
|
||||||
|
public string EventTypeShortName => EventTypeName.Split('.')?.Last();
|
||||||
|
[NotMapped]
|
||||||
|
public IntegrationEvent IntegrationEvent { get; private set; }
|
||||||
public EventStateEnum State { get; set; }
|
public EventStateEnum State { get; set; }
|
||||||
public int TimesSent { get; set; }
|
public int TimesSent { get; set; }
|
||||||
public DateTime CreationTime { get; private set; }
|
public DateTime CreationTime { get; private set; }
|
||||||
public string Content { get; private set; }
|
public string Content { get; private set; }
|
||||||
|
|
||||||
|
public IntegrationEventLogEntry DeserializeJsonContent(Type type)
|
||||||
|
{
|
||||||
|
IntegrationEvent = JsonConvert.DeserializeObject(Content, type) as IntegrationEvent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,10 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
|||||||
{
|
{
|
||||||
public interface IIntegrationEventLogService
|
public interface IIntegrationEventLogService
|
||||||
{
|
{
|
||||||
|
Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync();
|
||||||
Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction);
|
Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction);
|
||||||
Task MarkEventAsPublishedAsync(IntegrationEvent @event);
|
Task MarkEventAsPublishedAsync(Guid eventId);
|
||||||
|
Task MarkEventAsInProgressAsync(Guid eventId);
|
||||||
|
Task MarkEventAsFailedAsync(Guid eventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services
|
||||||
@ -12,6 +17,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
|||||||
{
|
{
|
||||||
private readonly IntegrationEventLogContext _integrationEventLogContext;
|
private readonly IntegrationEventLogContext _integrationEventLogContext;
|
||||||
private readonly DbConnection _dbConnection;
|
private readonly DbConnection _dbConnection;
|
||||||
|
private readonly List<Type> _eventTypes;
|
||||||
|
|
||||||
public IntegrationEventLogService(DbConnection dbConnection)
|
public IntegrationEventLogService(DbConnection dbConnection)
|
||||||
{
|
{
|
||||||
@ -21,6 +27,20 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
|||||||
.UseSqlServer(_dbConnection)
|
.UseSqlServer(_dbConnection)
|
||||||
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning))
|
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning))
|
||||||
.Options);
|
.Options);
|
||||||
|
|
||||||
|
_eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName)
|
||||||
|
.GetTypes()
|
||||||
|
.Where(t => t.Name.EndsWith(nameof(IntegrationEvent)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync()
|
||||||
|
{
|
||||||
|
return await _integrationEventLogContext.IntegrationEventLogs
|
||||||
|
.Where(e => e.State == EventStateEnum.NotPublished)
|
||||||
|
.OrderBy(o => o.CreationTime)
|
||||||
|
.Select(e => e.DeserializeJsonContent(_eventTypes.Find(t=> t.Name == e.EventTypeShortName)))
|
||||||
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction)
|
public Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction)
|
||||||
@ -38,11 +58,28 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
|||||||
return _integrationEventLogContext.SaveChangesAsync();
|
return _integrationEventLogContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task MarkEventAsPublishedAsync(IntegrationEvent @event)
|
public Task MarkEventAsPublishedAsync(Guid eventId)
|
||||||
{
|
{
|
||||||
var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == @event.Id);
|
return UpdateEventStatus(eventId, EventStateEnum.Published);
|
||||||
eventLogEntry.TimesSent++;
|
}
|
||||||
eventLogEntry.State = EventStateEnum.Published;
|
|
||||||
|
public Task MarkEventAsInProgressAsync(Guid eventId)
|
||||||
|
{
|
||||||
|
return UpdateEventStatus(eventId, EventStateEnum.InProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task MarkEventAsFailedAsync(Guid eventId)
|
||||||
|
{
|
||||||
|
return UpdateEventStatus(eventId, EventStateEnum.PublishedFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task UpdateEventStatus(Guid eventId, EventStateEnum status)
|
||||||
|
{
|
||||||
|
var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == eventId);
|
||||||
|
eventLogEntry.State = status;
|
||||||
|
|
||||||
|
if(status == EventStateEnum.InProgress)
|
||||||
|
eventLogEntry.TimesSent++;
|
||||||
|
|
||||||
_integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry);
|
_integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry);
|
||||||
|
|
||||||
|
@ -3,7 +3,5 @@
|
|||||||
public class BasketSettings
|
public class BasketSettings
|
||||||
{
|
{
|
||||||
public string ConnectionString { get; set; }
|
public string ConnectionString { get; set; }
|
||||||
|
|
||||||
public string EventBusConnection { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,16 @@ namespace Catalog.API.IntegrationEvents
|
|||||||
|
|
||||||
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
|
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
|
||||||
{
|
{
|
||||||
_eventBus.Publish(evt);
|
try
|
||||||
|
{
|
||||||
await _eventLogService.MarkEventAsPublishedAsync(evt);
|
await _eventLogService.MarkEventAsInProgressAsync(evt.Id);
|
||||||
|
_eventBus.Publish(evt);
|
||||||
|
await _eventLogService.MarkEventAsPublishedAsync(evt.Id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _eventLogService.MarkEventAsFailedAsync(evt.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt)
|
public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt)
|
||||||
|
@ -232,7 +232,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API
|
|||||||
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
|
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
|
||||||
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
||||||
|
|
||||||
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>();
|
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>();
|
||||||
|
|
||||||
|
@ -216,6 +216,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers
|
|||||||
// delete authentication cookie
|
// delete authentication cookie
|
||||||
await HttpContext.SignOutAsync();
|
await HttpContext.SignOutAsync();
|
||||||
|
|
||||||
|
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
|
||||||
|
|
||||||
// set this so UI rendering sees an anonymous user
|
// set this so UI rendering sees an anonymous user
|
||||||
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
|
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
|
||||||
|
@ -55,7 +55,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Extensions\" />
|
<Folder Include="Extensions\" />
|
||||||
<Folder Include="wwwroot\lib\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</environment>
|
</environment>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="navbar navbar-inverse navbar-fixed-top es-header">
|
<div class="navbar navbar-inverse fixed-top es-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="navbar-header col-sm-6 col-xs-8">
|
<div class="navbar-header col-sm-6 col-xs-8">
|
||||||
|
@ -8,28 +8,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"provider": "unpkg",
|
"provider": "unpkg",
|
||||||
"library": "bootstrap@3.3.7",
|
"library": "bootstrap@4.1.3",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/css/bootstrap.css",
|
"dist/css/bootstrap.css",
|
||||||
"dist/css/bootstrap.css.map",
|
"dist/css/bootstrap.css.map",
|
||||||
"dist/css/bootstrap.min.css",
|
"dist/css/bootstrap.min.css",
|
||||||
"dist/css/bootstrap.min.css.map",
|
"dist/css/bootstrap.min.css.map",
|
||||||
"dist/css/bootstrap-theme.css",
|
|
||||||
"dist/css/bootstrap-theme.css.map",
|
|
||||||
"dist/css/bootstrap-theme.min.css",
|
|
||||||
"dist/css/bootstrap-theme.min.css.map",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.eot",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.svg",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.ttf",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.woff",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.woff2",
|
|
||||||
"dist/js/bootstrap.js",
|
"dist/js/bootstrap.js",
|
||||||
"dist/js/bootstrap.min.js",
|
"dist/js/bootstrap.min.js"
|
||||||
"fonts/glyphicons-halflings-regular.eot",
|
|
||||||
"fonts/glyphicons-halflings-regular.svg",
|
|
||||||
"fonts/glyphicons-halflings-regular.ttf",
|
|
||||||
"fonts/glyphicons-halflings-regular.woff",
|
|
||||||
"fonts/glyphicons-halflings-regular.woff2"
|
|
||||||
],
|
],
|
||||||
"destination": "wwwroot/lib/bootstrap/"
|
"destination": "wwwroot/lib/bootstrap/"
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Ordering.API.Application.IntegrationEvents;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Behaviors
|
||||||
|
{
|
||||||
|
public class TransactionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
{
|
||||||
|
private readonly ILogger<TransactionBehaviour<TRequest, TResponse>> _logger;
|
||||||
|
private readonly OrderingContext _dbContext;
|
||||||
|
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||||
|
|
||||||
|
public TransactionBehaviour(OrderingContext dbContext,
|
||||||
|
IOrderingIntegrationEventService orderingIntegrationEventService,
|
||||||
|
ILogger<TransactionBehaviour<TRequest, TResponse>> logger)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext ?? throw new ArgumentException(nameof(OrderingContext));
|
||||||
|
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentException(nameof(orderingIntegrationEventService));
|
||||||
|
_logger = logger ?? throw new ArgumentException(nameof(ILogger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
|
||||||
|
{
|
||||||
|
TResponse response = default(TResponse);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var strategy = _dbContext.Database.CreateExecutionStrategy();
|
||||||
|
await strategy.ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Begin transaction {typeof(TRequest).Name}");
|
||||||
|
|
||||||
|
await _dbContext.BeginTransactionAsync();
|
||||||
|
|
||||||
|
response = await next();
|
||||||
|
|
||||||
|
await _dbContext.CommitTransactionAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation($"Committed transaction {typeof(TRequest).Name}");
|
||||||
|
|
||||||
|
await _orderingIntegrationEventService.PublishEventsThroughEventBusAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Rollback transaction executed {typeof(TRequest).Name}");
|
||||||
|
|
||||||
|
_dbContext.RollbackTransaction();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands
|
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands
|
||||||
{
|
{
|
||||||
using Domain.AggregatesModel.OrderAggregate;
|
using Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using global::Ordering.API.Application.IntegrationEvents;
|
||||||
|
using global::Ordering.API.Application.IntegrationEvents.Events;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services;
|
using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
||||||
@ -15,17 +17,26 @@
|
|||||||
private readonly IOrderRepository _orderRepository;
|
private readonly IOrderRepository _orderRepository;
|
||||||
private readonly IIdentityService _identityService;
|
private readonly IIdentityService _identityService;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||||
|
|
||||||
// Using DI to inject infrastructure persistence Repositories
|
// Using DI to inject infrastructure persistence Repositories
|
||||||
public CreateOrderCommandHandler(IMediator mediator, IOrderRepository orderRepository, IIdentityService identityService)
|
public CreateOrderCommandHandler(IMediator mediator,
|
||||||
|
IOrderingIntegrationEventService orderingIntegrationEventService,
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
IIdentityService identityService)
|
||||||
{
|
{
|
||||||
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
|
||||||
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
|
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
|
||||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||||
|
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
|
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Add Integration event to clean the basket
|
||||||
|
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
|
||||||
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
|
||||||
|
|
||||||
// Add/Update the Buyer AggregateRoot
|
// Add/Update the Buyer AggregateRoot
|
||||||
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
|
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
|
||||||
// methods and constructor so validations, invariants and business logic
|
// methods and constructor so validations, invariants and business logic
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
public class SetAwaitingValidationOrderStatusCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public int OrderNumber { get; private set; }
|
||||||
|
|
||||||
|
public SetAwaitingValidationOrderStatusCommand(int orderNumber)
|
||||||
|
{
|
||||||
|
OrderNumber = orderNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
// Regular CommandHandler
|
||||||
|
public class SetAwaitingValidationOrderStatusCommandHandler : IRequestHandler<SetAwaitingValidationOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
|
||||||
|
public SetAwaitingValidationOrderStatusCommandHandler(IOrderRepository orderRepository)
|
||||||
|
{
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler which processes the command when
|
||||||
|
/// graceperiod has finished
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> Handle(SetAwaitingValidationOrderStatusCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
|
||||||
|
if(orderToUpdate == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderToUpdate.SetAwaitingValidationStatus();
|
||||||
|
return await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Use for Idempotency in Command process
|
||||||
|
public class SetAwaitingValidationIdentifiedOrderStatusCommandHandler : IdentifiedCommandHandler<SetAwaitingValidationOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
public SetAwaitingValidationIdentifiedOrderStatusCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CreateResultForDuplicateRequest()
|
||||||
|
{
|
||||||
|
return true; // Ignore duplicate requests for processing order.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
public class SetPaidOrderStatusCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public int OrderNumber { get; private set; }
|
||||||
|
|
||||||
|
public SetPaidOrderStatusCommand(int orderNumber)
|
||||||
|
{
|
||||||
|
OrderNumber = orderNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
// Regular CommandHandler
|
||||||
|
public class SetPaidOrderStatusCommandHandler : IRequestHandler<SetPaidOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
|
||||||
|
public SetPaidOrderStatusCommandHandler(IOrderRepository orderRepository)
|
||||||
|
{
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler which processes the command when
|
||||||
|
/// Shipment service confirms the payment
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> Handle(SetPaidOrderStatusCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Simulate a work time for validating the payment
|
||||||
|
await Task.Delay(10000);
|
||||||
|
|
||||||
|
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
|
||||||
|
if(orderToUpdate == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderToUpdate.SetPaidStatus();
|
||||||
|
return await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Use for Idempotency in Command process
|
||||||
|
public class SetPaidIdentifiedOrderStatusCommandHandler : IdentifiedCommandHandler<SetPaidOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
public SetPaidIdentifiedOrderStatusCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CreateResultForDuplicateRequest()
|
||||||
|
{
|
||||||
|
return true; // Ignore duplicate requests for processing order.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
public class SetStockConfirmedOrderStatusCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public int OrderNumber { get; private set; }
|
||||||
|
|
||||||
|
public SetStockConfirmedOrderStatusCommand(int orderNumber)
|
||||||
|
{
|
||||||
|
OrderNumber = orderNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
// Regular CommandHandler
|
||||||
|
public class SetStockConfirmedOrderStatusCommandHandler : IRequestHandler<SetStockConfirmedOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
|
||||||
|
public SetStockConfirmedOrderStatusCommandHandler(IOrderRepository orderRepository)
|
||||||
|
{
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler which processes the command when
|
||||||
|
/// Stock service confirms the request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> Handle(SetStockConfirmedOrderStatusCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Simulate a work time for confirming the stock
|
||||||
|
await Task.Delay(10000);
|
||||||
|
|
||||||
|
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
|
||||||
|
if(orderToUpdate == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderToUpdate.SetStockConfirmedStatus();
|
||||||
|
return await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Use for Idempotency in Command process
|
||||||
|
public class SetStockConfirmedOrderStatusIdenfifiedCommandHandler : IdentifiedCommandHandler<SetStockConfirmedOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
public SetStockConfirmedOrderStatusIdenfifiedCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CreateResultForDuplicateRequest()
|
||||||
|
{
|
||||||
|
return true; // Ignore duplicate requests for processing order.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
public class SetStockRejectedOrderStatusCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public int OrderNumber { get; private set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public List<int> OrderStockItems { get; private set; }
|
||||||
|
|
||||||
|
public SetStockRejectedOrderStatusCommand(int orderNumber, List<int> orderStockItems)
|
||||||
|
{
|
||||||
|
OrderNumber = orderNumber;
|
||||||
|
OrderStockItems = orderStockItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ordering.API.Application.Commands
|
||||||
|
{
|
||||||
|
// Regular CommandHandler
|
||||||
|
public class SetStockRejectedOrderStatusCommandHandler : IRequestHandler<SetStockRejectedOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository;
|
||||||
|
|
||||||
|
public SetStockRejectedOrderStatusCommandHandler(IOrderRepository orderRepository)
|
||||||
|
{
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler which processes the command when
|
||||||
|
/// Stock service rejects the request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> Handle(SetStockRejectedOrderStatusCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Simulate a work time for rejecting the stock
|
||||||
|
await Task.Delay(10000);
|
||||||
|
|
||||||
|
var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
|
||||||
|
if(orderToUpdate == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderToUpdate.SetCancelledStatusWhenStockIsRejected(command.OrderStockItems);
|
||||||
|
|
||||||
|
return await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Use for Idempotency in Command process
|
||||||
|
public class SetStockRejectedOrderStatusIdenfifiedCommandHandler : IdentifiedCommandHandler<SetStockRejectedOrderStatusCommand, bool>
|
||||||
|
{
|
||||||
|
public SetStockRejectedOrderStatusIdenfifiedCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CreateResultForDuplicateRequest()
|
||||||
|
{
|
||||||
|
return true; // Ignore duplicate requests for processing order.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -41,7 +41,7 @@ namespace Ordering.API.Application.DomainEventHandlers.OrderCancelled
|
|||||||
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||||
|
|
||||||
var orderStatusChangedToCancelledIntegrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
var orderStatusChangedToCancelledIntegrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToCancelledIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToCancelledIntegrationEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
var orderStatusChangedToAwaitingValidationIntegrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent(
|
var orderStatusChangedToAwaitingValidationIntegrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent(
|
||||||
order.Id, order.OrderStatus.Name, buyer.Name, orderStockList);
|
order.Id, order.OrderStatus.Name, buyer.Name, orderStockList);
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToAwaitingValidationIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToAwaitingValidationIntegrationEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -51,7 +51,7 @@
|
|||||||
buyer.Name,
|
buyer.Name,
|
||||||
orderStockList);
|
orderStockList);
|
||||||
|
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToPaidIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToPaidIntegrationEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -41,7 +41,7 @@ namespace Ordering.API.Application.DomainEventHandlers.OrderShipped
|
|||||||
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||||
|
|
||||||
var orderStatusChangedToShippedIntegrationEvent = new OrderStatusChangedToShippedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
var orderStatusChangedToShippedIntegrationEvent = new OrderStatusChangedToShippedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToShippedIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToShippedIntegrationEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ namespace Ordering.API.Application.DomainEventHandlers.OrderStartedEvent
|
|||||||
.SaveEntitiesAsync();
|
.SaveEntitiesAsync();
|
||||||
|
|
||||||
var orderStatusChangedTosubmittedIntegrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(orderStartedEvent.Order.Id, orderStartedEvent.Order.OrderStatus.Name, buyer.Name);
|
var orderStatusChangedTosubmittedIntegrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(orderStartedEvent.Order.Id, orderStartedEvent.Order.OrderStatus.Name, buyer.Name);
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedTosubmittedIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedTosubmittedIntegrationEvent);
|
||||||
|
|
||||||
_logger.CreateLogger(nameof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler)).LogTrace($"Buyer {buyerUpdated.Id} and related payment method were validated or updated for orderId: {orderStartedEvent.Order.Id}.");
|
_logger.CreateLogger(nameof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler)).LogTrace($"Buyer {buyerUpdated.Id} and related payment method were validated or updated for orderId: {orderStartedEvent.Order.Id}.");
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||||
|
|
||||||
var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
|
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
using MediatR;
|
||||||
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Ordering.API.Application.Commands;
|
||||||
using Ordering.API.Application.IntegrationEvents.Events;
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -7,11 +9,11 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
|||||||
{
|
{
|
||||||
public class GracePeriodConfirmedIntegrationEventHandler : IIntegrationEventHandler<GracePeriodConfirmedIntegrationEvent>
|
public class GracePeriodConfirmedIntegrationEventHandler : IIntegrationEventHandler<GracePeriodConfirmedIntegrationEvent>
|
||||||
{
|
{
|
||||||
private readonly IOrderRepository _orderRepository;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
public GracePeriodConfirmedIntegrationEventHandler(IOrderRepository orderRepository)
|
public GracePeriodConfirmedIntegrationEventHandler(IMediator mediator)
|
||||||
{
|
{
|
||||||
_orderRepository = orderRepository;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -24,9 +26,8 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Handle(GracePeriodConfirmedIntegrationEvent @event)
|
public async Task Handle(GracePeriodConfirmedIntegrationEvent @event)
|
||||||
{
|
{
|
||||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
var command = new SetAwaitingValidationOrderStatusCommand(@event.OrderId);
|
||||||
orderToUpdate.SetAwaitingValidationStatus();
|
await _mediator.Send(command);
|
||||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||||
{
|
{
|
||||||
|
using MediatR;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Ordering.API.Application.Commands;
|
||||||
using Ordering.API.Application.IntegrationEvents.Events;
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
public class OrderPaymentFailedIntegrationEventHandler :
|
public class OrderPaymentFailedIntegrationEventHandler :
|
||||||
IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>
|
IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>
|
||||||
{
|
{
|
||||||
private readonly IOrderRepository _orderRepository;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
public OrderPaymentFailedIntegrationEventHandler(IOrderRepository orderRepository)
|
public OrderPaymentFailedIntegrationEventHandler(IMediator mediator)
|
||||||
{
|
{
|
||||||
_orderRepository = orderRepository;
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(OrderPaymentFailedIntegrationEvent @event)
|
public async Task Handle(OrderPaymentFailedIntegrationEvent @event)
|
||||||
{
|
{
|
||||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
var command = new CancelOrderCommand(@event.OrderId);
|
||||||
|
await _mediator.Send(command);
|
||||||
orderToUpdate.SetCancelledStatus();
|
|
||||||
|
|
||||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,27 @@
|
|||||||
namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||||
{
|
{
|
||||||
|
using MediatR;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using Ordering.API.Application.Commands;
|
||||||
using Ordering.API.Application.IntegrationEvents.Events;
|
using Ordering.API.Application.IntegrationEvents.Events;
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
public class OrderPaymentSuccededIntegrationEventHandler :
|
public class OrderPaymentSuccededIntegrationEventHandler :
|
||||||
IIntegrationEventHandler<OrderPaymentSuccededIntegrationEvent>
|
IIntegrationEventHandler<OrderPaymentSuccededIntegrationEvent>
|
||||||
{
|
{
|
||||||
private readonly IOrderRepository _orderRepository;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
public OrderPaymentSuccededIntegrationEventHandler(IOrderRepository orderRepository)
|
public OrderPaymentSuccededIntegrationEventHandler(IMediator mediator)
|
||||||
{
|
{
|
||||||
_orderRepository = orderRepository;
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(OrderPaymentSuccededIntegrationEvent @event)
|
public async Task Handle(OrderPaymentSuccededIntegrationEvent @event)
|
||||||
{
|
{
|
||||||
// Simulate a work time for validating the payment
|
var command = new SetPaidOrderStatusCommand(@event.OrderId);
|
||||||
await Task.Delay(10000);
|
await _mediator.Send(command);
|
||||||
|
|
||||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
|
||||||
|
|
||||||
orderToUpdate.SetPaidStatus();
|
|
||||||
|
|
||||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,27 +4,24 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Events;
|
using Events;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using Ordering.API.Application.Commands;
|
||||||
|
|
||||||
public class OrderStockConfirmedIntegrationEventHandler :
|
public class OrderStockConfirmedIntegrationEventHandler :
|
||||||
IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent>
|
IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent>
|
||||||
{
|
{
|
||||||
private readonly IOrderRepository _orderRepository;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
public OrderStockConfirmedIntegrationEventHandler(IOrderRepository orderRepository)
|
public OrderStockConfirmedIntegrationEventHandler(IMediator mediator)
|
||||||
{
|
{
|
||||||
_orderRepository = orderRepository;
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(OrderStockConfirmedIntegrationEvent @event)
|
public async Task Handle(OrderStockConfirmedIntegrationEvent @event)
|
||||||
{
|
{
|
||||||
// Simulate a work time for confirming the stock
|
var command = new SetStockConfirmedOrderStatusCommand(@event.OrderId);
|
||||||
await Task.Delay(10000);
|
await _mediator.Send(command);
|
||||||
|
|
||||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
|
||||||
|
|
||||||
orderToUpdate.SetStockConfirmedStatus();
|
|
||||||
|
|
||||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,27 +5,27 @@
|
|||||||
using Events;
|
using Events;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
|
using MediatR;
|
||||||
|
using Ordering.API.Application.Commands;
|
||||||
|
|
||||||
public class OrderStockRejectedIntegrationEventHandler : IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>
|
public class OrderStockRejectedIntegrationEventHandler : IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>
|
||||||
{
|
{
|
||||||
private readonly IOrderRepository _orderRepository;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
public OrderStockRejectedIntegrationEventHandler(IOrderRepository orderRepository)
|
public OrderStockRejectedIntegrationEventHandler(IMediator mediator)
|
||||||
{
|
{
|
||||||
_orderRepository = orderRepository;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(OrderStockRejectedIntegrationEvent @event)
|
public async Task Handle(OrderStockRejectedIntegrationEvent @event)
|
||||||
{
|
{
|
||||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
|
||||||
|
|
||||||
var orderStockRejectedItems = @event.OrderStockItems
|
var orderStockRejectedItems = @event.OrderStockItems
|
||||||
.FindAll(c => !c.HasStock)
|
.FindAll(c => !c.HasStock)
|
||||||
.Select(c => c.ProductId);
|
.Select(c => c.ProductId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
orderToUpdate.SetCancelledStatusWhenStockIsRejected(orderStockRejectedItems);
|
var command = new SetStockRejectedOrderStatusCommand(@event.OrderId, orderStockRejectedItems);
|
||||||
|
await _mediator.Send(command);
|
||||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,15 +11,13 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
|||||||
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
|
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ILoggerFactory _logger;
|
private readonly ILoggerFactory _logger;
|
||||||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
|
||||||
|
|
||||||
public UserCheckoutAcceptedIntegrationEventHandler(IMediator mediator,
|
public UserCheckoutAcceptedIntegrationEventHandler(IMediator mediator,
|
||||||
ILoggerFactory logger, IOrderingIntegrationEventService orderingIntegrationEventService)
|
ILoggerFactory logger)
|
||||||
{
|
{
|
||||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -34,11 +32,7 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
|||||||
public async Task Handle(UserCheckoutAcceptedIntegrationEvent eventMsg)
|
public async Task Handle(UserCheckoutAcceptedIntegrationEvent eventMsg)
|
||||||
{
|
{
|
||||||
var result = false;
|
var result = false;
|
||||||
|
|
||||||
// Send Integration event to clean basket once basket is converted to Order and before starting with the order creation process
|
|
||||||
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(eventMsg.UserId);
|
|
||||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStartedIntegrationEvent);
|
|
||||||
|
|
||||||
if (eventMsg.RequestId != Guid.Empty)
|
if (eventMsg.RequestId != Guid.Empty)
|
||||||
{
|
{
|
||||||
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items, eventMsg.UserId, eventMsg.UserName, eventMsg.City, eventMsg.Street,
|
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items, eventMsg.UserId, eventMsg.UserName, eventMsg.City, eventMsg.Street,
|
||||||
|
@ -5,6 +5,7 @@ namespace Ordering.API.Application.IntegrationEvents
|
|||||||
{
|
{
|
||||||
public interface IOrderingIntegrationEventService
|
public interface IOrderingIntegrationEventService
|
||||||
{
|
{
|
||||||
Task PublishThroughEventBusAsync(IntegrationEvent evt);
|
Task PublishEventsThroughEventBusAsync();
|
||||||
|
Task AddAndSaveEventAsync(IntegrationEvent evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||||
|
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
|
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities;
|
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure;
|
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure;
|
||||||
using System;
|
using System;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Ordering.API.Application.IntegrationEvents
|
namespace Ordering.API.Application.IntegrationEvents
|
||||||
@ -17,34 +19,42 @@ namespace Ordering.API.Application.IntegrationEvents
|
|||||||
private readonly Func<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory;
|
private readonly Func<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory;
|
||||||
private readonly IEventBus _eventBus;
|
private readonly IEventBus _eventBus;
|
||||||
private readonly OrderingContext _orderingContext;
|
private readonly OrderingContext _orderingContext;
|
||||||
|
private readonly IntegrationEventLogContext _eventLogContext;
|
||||||
private readonly IIntegrationEventLogService _eventLogService;
|
private readonly IIntegrationEventLogService _eventLogService;
|
||||||
|
|
||||||
public OrderingIntegrationEventService(IEventBus eventBus, OrderingContext orderingContext,
|
public OrderingIntegrationEventService(IEventBus eventBus,
|
||||||
Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory)
|
OrderingContext orderingContext,
|
||||||
|
IntegrationEventLogContext eventLogContext,
|
||||||
|
Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory)
|
||||||
{
|
{
|
||||||
_orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext));
|
_orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext));
|
||||||
|
_eventLogContext = eventLogContext ?? throw new ArgumentNullException(nameof(eventLogContext));
|
||||||
_integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory));
|
_integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory));
|
||||||
_eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
|
_eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
|
||||||
_eventLogService = _integrationEventLogServiceFactory(_orderingContext.Database.GetDbConnection());
|
_eventLogService = _integrationEventLogServiceFactory(_orderingContext.Database.GetDbConnection());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
|
public async Task PublishEventsThroughEventBusAsync()
|
||||||
{
|
{
|
||||||
await SaveEventAndOrderingContextChangesAsync(evt);
|
var pendindLogEvents = await _eventLogService.RetrieveEventLogsPendingToPublishAsync();
|
||||||
_eventBus.Publish(evt);
|
foreach (var logEvt in pendindLogEvents)
|
||||||
await _eventLogService.MarkEventAsPublishedAsync(evt);
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _eventLogService.MarkEventAsInProgressAsync(logEvt.EventId);
|
||||||
|
_eventBus.Publish(logEvt.IntegrationEvent);
|
||||||
|
await _eventLogService.MarkEventAsPublishedAsync(logEvt.EventId);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _eventLogService.MarkEventAsFailedAsync(logEvt.EventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt)
|
public async Task AddAndSaveEventAsync(IntegrationEvent evt)
|
||||||
{
|
{
|
||||||
//Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction():
|
await _eventLogService.SaveEventAsync(evt, _orderingContext.GetCurrentTransaction.GetDbTransaction());
|
||||||
//See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
|
|
||||||
await ResilientTransaction.New(_orderingContext)
|
|
||||||
.ExecuteAsync(async () => {
|
|
||||||
// Achieving atomicity between original ordering database operation and the IntegrationEventLog thanks to a local transaction
|
|
||||||
await _orderingContext.SaveChangesAsync();
|
|
||||||
await _eventLogService.SaveEventAsync(evt, _orderingContext.Database.CurrentTransaction.GetDbTransaction());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using Autofac;
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||||
|
using Ordering.API.Application.Behaviors;
|
||||||
using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent;
|
using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent;
|
||||||
using Ordering.API.Application.Validations;
|
using Ordering.API.Application.Validations;
|
||||||
using Ordering.API.Infrastructure.Behaviors;
|
using Ordering.API.Infrastructure.Behaviors;
|
||||||
@ -40,6 +41,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof
|
|||||||
|
|
||||||
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>));
|
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>));
|
||||||
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
|
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
|
||||||
|
builder.RegisterGeneric(typeof(TransactionBehaviour<,>)).As(typeof(IPipelineBehavior<,>));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ namespace Ordering.API.Infrastructure.Middlewares
|
|||||||
new Claim("emails", currentUserId),
|
new Claim("emails", currentUserId),
|
||||||
new Claim("name", "Test user"),
|
new Claim("name", "Test user"),
|
||||||
new Claim("nonce", Guid.NewGuid().ToString()),
|
new Claim("nonce", Guid.NewGuid().ToString()),
|
||||||
new Claim("ttp://schemas.microsoft.com/identity/claims/identityprovider", "ByPassAuthMiddleware"),
|
new Claim("http://schemas.microsoft.com/identity/claims/identityprovider", "ByPassAuthMiddleware"),
|
||||||
new Claim("nonce", Guid.NewGuid().ToString()),
|
new Claim("nonce", Guid.NewGuid().ToString()),
|
||||||
new Claim("sub", "1234"),
|
new Claim("sub", "1234"),
|
||||||
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname","User"),
|
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname","User"),
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
eventBus.Subscribe<OrderStockConfirmedIntegrationEvent, IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent>>();
|
eventBus.Subscribe<OrderStockConfirmedIntegrationEvent, IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent>>();
|
||||||
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
|
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
|
||||||
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>();
|
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>();
|
||||||
eventBus.Subscribe<OrderPaymentSuccededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSuccededIntegrationEvent>>();
|
eventBus.Subscribe<OrderPaymentSuccededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSuccededIntegrationEvent>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
|
|||||||
{
|
{
|
||||||
public class Address : ValueObject
|
public class Address : ValueObject
|
||||||
{
|
{
|
||||||
public String Street { get; }
|
public String Street { get; private set; }
|
||||||
public String City { get; }
|
public String City { get; private set; }
|
||||||
public String State { get; }
|
public String State { get; private set; }
|
||||||
public String Country { get; }
|
public String Country { get; private set; }
|
||||||
public String ZipCode { get; }
|
public String ZipCode { get; private set; }
|
||||||
|
|
||||||
private Address() { }
|
private Address() { }
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
|
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
|
||||||
using Ordering.Infrastructure;
|
using Ordering.Infrastructure;
|
||||||
using Ordering.Infrastructure.EntityConfigurations;
|
using Ordering.Infrastructure.EntityConfigurations;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Data;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -23,9 +25,12 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
|
|||||||
public DbSet<OrderStatus> OrderStatus { get; set; }
|
public DbSet<OrderStatus> OrderStatus { get; set; }
|
||||||
|
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
private IDbContextTransaction _currentTransaction;
|
||||||
|
|
||||||
private OrderingContext(DbContextOptions<OrderingContext> options) : base (options) { }
|
private OrderingContext(DbContextOptions<OrderingContext> options) : base (options) { }
|
||||||
|
|
||||||
|
public IDbContextTransaction GetCurrentTransaction => _currentTransaction;
|
||||||
|
|
||||||
public OrderingContext(DbContextOptions<OrderingContext> options, IMediator mediator) : base(options)
|
public OrderingContext(DbContextOptions<OrderingContext> options, IMediator mediator) : base(options)
|
||||||
{
|
{
|
||||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||||
@ -60,7 +65,50 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
|
|||||||
var result = await base.SaveChangesAsync();
|
var result = await base.SaveChangesAsync();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task BeginTransactionAsync()
|
||||||
|
{
|
||||||
|
_currentTransaction = _currentTransaction ?? await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CommitTransactionAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveChangesAsync();
|
||||||
|
_currentTransaction?.Commit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
RollbackTransaction();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_currentTransaction != null)
|
||||||
|
{
|
||||||
|
_currentTransaction.Dispose();
|
||||||
|
_currentTransaction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RollbackTransaction()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_currentTransaction?.Rollback();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_currentTransaction != null)
|
||||||
|
{
|
||||||
|
_currentTransaction.Dispose();
|
||||||
|
_currentTransaction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrderingContextDesignFactory : IDesignTimeDbContextFactory<OrderingContext>
|
public class OrderingContextDesignFactory : IDesignTimeDbContextFactory<OrderingContext>
|
||||||
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace UnitTest.Ordering.Application
|
namespace UnitTest.Ordering.Application
|
||||||
{
|
{
|
||||||
|
using global::Ordering.API.Application.IntegrationEvents;
|
||||||
using global::Ordering.API.Application.Models;
|
using global::Ordering.API.Application.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
@ -22,12 +23,14 @@ namespace UnitTest.Ordering.Application
|
|||||||
private readonly Mock<IOrderRepository> _orderRepositoryMock;
|
private readonly Mock<IOrderRepository> _orderRepositoryMock;
|
||||||
private readonly Mock<IIdentityService> _identityServiceMock;
|
private readonly Mock<IIdentityService> _identityServiceMock;
|
||||||
private readonly Mock<IMediator> _mediator;
|
private readonly Mock<IMediator> _mediator;
|
||||||
|
private readonly Mock<IOrderingIntegrationEventService> _orderingIntegrationEventService;
|
||||||
|
|
||||||
public NewOrderRequestHandlerTest()
|
public NewOrderRequestHandlerTest()
|
||||||
{
|
{
|
||||||
|
|
||||||
_orderRepositoryMock = new Mock<IOrderRepository>();
|
_orderRepositoryMock = new Mock<IOrderRepository>();
|
||||||
_identityServiceMock = new Mock<IIdentityService>();
|
_identityServiceMock = new Mock<IIdentityService>();
|
||||||
|
_orderingIntegrationEventService = new Mock<IOrderingIntegrationEventService>();
|
||||||
_mediator = new Mock<IMediator>();
|
_mediator = new Mock<IMediator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +51,7 @@ namespace UnitTest.Ordering.Application
|
|||||||
_identityServiceMock.Setup(svc => svc.GetUserIdentity()).Returns(buyerId);
|
_identityServiceMock.Setup(svc => svc.GetUserIdentity()).Returns(buyerId);
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
var handler = new CreateOrderCommandHandler(_mediator.Object, _orderRepositoryMock.Object, _identityServiceMock.Object);
|
var handler = new CreateOrderCommandHandler(_mediator.Object, _orderingIntegrationEventService.Object, _orderRepositoryMock.Object, _identityServiceMock.Object);
|
||||||
var cltToken = new System.Threading.CancellationToken();
|
var cltToken = new System.Threading.CancellationToken();
|
||||||
var result = await handler.Handle(fakeOrderCmd, cltToken);
|
var result = await handler.Handle(fakeOrderCmd, cltToken);
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card esh-campaigns-items">
|
<div class="card esh-campaigns-items">
|
||||||
<img class="card-img-top" src="@Model.PictureUri" alt="Card image cap">
|
<img class="card-img-top" src="@Model.PictureUri" alt="Card image cap">
|
||||||
<div class="card-block">
|
<div class="card-body">
|
||||||
<h4 class="card-title">@Model.Name</h4>
|
<h4 class="card-title">@Model.Name</h4>
|
||||||
<p class="card-text">@Model.Description</p>
|
<p class="card-text">@Model.Description</p>
|
||||||
<p class="card-text">
|
</div>
|
||||||
<small class="text-muted">
|
<div class="card-footer">
|
||||||
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
<small class="text-muted">
|
||||||
</small>
|
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
||||||
</p>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -27,24 +27,28 @@
|
|||||||
<div class="esh-campaigns-items" style="font-weight: 300;">
|
<div class="esh-campaigns-items" style="font-weight: 300;">
|
||||||
UPDATE USER LOCATION
|
UPDATE USER LOCATION
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
|
<form class="form-inline" asp-action="CreateNewUserLocation" method="post">
|
||||||
|
<label class="sr-only" for="longitudeInput">Name</label>
|
||||||
|
|
||||||
<form class="form-inline" asp-action="CreateNewUserLocation" method="post">
|
<div class="input-group mb-3">
|
||||||
<label class="sr-only" for="longitudeInput">Name</label>
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text" id="inputGroup-sizing-default">Lat</span>
|
||||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
|
</div>
|
||||||
<div class="input-group-addon">Lat</div>
|
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="latitudeInput" asp-for="Lat" placeholder="Latitude">
|
||||||
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="latitudeInput" asp-for="Lat" placeholder="Latitude">
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
|
<div class="input-group mb-3">
|
||||||
<div class="input-group-addon">Lon</div>
|
<div class="input-group-prepend">
|
||||||
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="longitudeInput" asp-for="Lon" placeholder="Longitude">
|
<span class="input-group-text" id="inputGroup-sizing-default">Lon</span>
|
||||||
</div>
|
</div>
|
||||||
|
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="longitudeInput" asp-for="Lon" placeholder="Longitude">
|
||||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0 col-md-2">
|
</div>
|
||||||
<input type="submit" value="Update" class="btn esh-campaigns-form-button" />
|
|
||||||
</div>
|
<div class="input-group mb-2 mr-sm-2 mb-sm-0 col-md-2">
|
||||||
</form>
|
<input type="submit" value="Update" class="btn esh-campaigns-form-button" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
@model CampaignItem
|
@model CampaignItem
|
||||||
|
|
||||||
<form asp-controller="Campaigns" asp-action="Details" asp-route-id="@Model.Id">
|
<form asp-controller="Campaigns" asp-action="Details" asp-route-id="@Model.Id">
|
||||||
<div class="card-block">
|
<div class="card">
|
||||||
|
<img class="card-img-top esh-campaigns-thumbnail" src="@Model.PictureUri" alt="@Model.Name">
|
||||||
|
<div class="card-body">
|
||||||
<h4 class="card-title esh-campaigns-name">@Model.Name</h4>
|
<h4 class="card-title esh-campaigns-name">@Model.Name</h4>
|
||||||
<img class="card-img-top esh-campaigns-thumbnail" src="@Model.PictureUri" alt="@Model.Name">
|
|
||||||
@if (ViewBag.IsCampaignDetailFunctionActive == true)
|
@if (ViewBag.IsCampaignDetailFunctionActive == true)
|
||||||
{
|
{
|
||||||
<input type="button" value="More Details" class="btn esh-campaigns-button" onClick="window.open('@Model.DetailsUri')">
|
<input type="button" value="More Details" class="btn esh-campaigns-button" onClick="window.open('@Model.DetailsUri')">
|
||||||
@ -11,11 +12,12 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<input class="esh-campaigns-button" type="submit" value="More details">
|
<input class="esh-campaigns-button" type="submit" value="More details">
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer esh-campaigns-card-footer-text">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
@await Html.PartialAsync("_OrderItems")
|
@await Html.PartialAsync("_OrderItems")
|
||||||
|
|
||||||
<section class="esh-orders_new-section">
|
<section class="esh-orders_new-section">
|
||||||
<div class="form-group">
|
<div class="form-group row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
@ -13,51 +13,51 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<section class="esh-orders_detail-section">
|
<section class="esh-orders_detail-section">
|
||||||
<article class="esh-orders_detail-titles row">
|
<article class="esh-orders_detail-titles row">
|
||||||
<section class="esh-orders_detail-title col-xs-3">Order number</section>
|
<section class="esh-orders_detail-title col-3">Order number</section>
|
||||||
<section class="esh-orders_detail-title col-xs-3">Date</section>
|
<section class="esh-orders_detail-title col-3">Date</section>
|
||||||
<section class="esh-orders_detail-title col-xs-3">Total</section>
|
<section class="esh-orders_detail-title col-3">Total</section>
|
||||||
<section class="esh-orders_detail-title col-xs-3">Status</section>
|
<section class="esh-orders_detail-title col-3">Status</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_detail-items row">
|
<article class="esh-orders_detail-items row">
|
||||||
<section class="esh-orders_detail-item col-xs-3">@Model.OrderNumber</section>
|
<section class="esh-orders_detail-item col-3">@Model.OrderNumber</section>
|
||||||
<section class="esh-orders_detail-item col-xs-3">@Model.Date</section>
|
<section class="esh-orders_detail-item col-3">@Model.Date</section>
|
||||||
<section class="esh-orders_detail-item col-xs-3">$@Model.Total</section>
|
<section class="esh-orders_detail-item col-3">$@Model.Total</section>
|
||||||
<section class="esh-orders_detail-title col-xs-3">@Model.Status</section>
|
<section class="esh-orders_detail-title col-3">@Model.Status</section>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="esh-orders_detail-section">
|
<section class="esh-orders_detail-section">
|
||||||
<article class="esh-orders_detail-titles row">
|
<article class="esh-orders_detail-titles row">
|
||||||
<section class="esh-orders_detail-title col-xs-12">Description</section>
|
<section class="esh-orders_detail-title col-12">Description</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_detail-items row">
|
<article class="esh-orders_detail-items row">
|
||||||
<section class="esh-orders_detail-item col-xs-12">@Model.Description</section>
|
<section class="esh-orders_detail-item col-12">@Model.Description</section>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="esh-orders_detail-section">
|
<section class="esh-orders_detail-section">
|
||||||
<article class="esh-orders_detail-titles row">
|
<article class="esh-orders_detail-titles row">
|
||||||
<section class="esh-orders_detail-title col-xs-12">Shiping address</section>
|
<section class="esh-orders_detail-title col-12">Shiping address</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_detail-items row">
|
<article class="esh-orders_detail-items row">
|
||||||
<section class="esh-orders_detail-item col-xs-12">@Model.Street</section>
|
<section class="esh-orders_detail-item col-12">@Model.Street</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_detail-items row">
|
<article class="esh-orders_detail-items row">
|
||||||
<section class="esh-orders_detail-item col-xs-12">@Model.City</section>
|
<section class="esh-orders_detail-item col-12">@Model.City</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_detail-items row">
|
<article class="esh-orders_detail-items row">
|
||||||
<section class="esh-orders_detail-item col-xs-12">@Model.Country</section>
|
<section class="esh-orders_detail-item col-12">@Model.Country</section>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="esh-orders_detail-section">
|
<section class="esh-orders_detail-section">
|
||||||
<article class="esh-orders_detail-titles row">
|
<article class="esh-orders_detail-titles row">
|
||||||
<section class="esh-orders_detail-title col-xs-12">ORDER DETAILS</section>
|
<section class="esh-orders_detail-title col-12">ORDER DETAILS</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@for (int i = 0; i < Model.OrderItems.Count; i++)
|
@for (int i = 0; i < Model.OrderItems.Count; i++)
|
||||||
@ -67,23 +67,23 @@
|
|||||||
<section class="esh-orders_detail-item col-md-4 hidden-md-down">
|
<section class="esh-orders_detail-item col-md-4 hidden-md-down">
|
||||||
<img class="esh-orders_detail-image" src="@item.PictureUrl">
|
<img class="esh-orders_detail-image" src="@item.PictureUrl">
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-4">@item.ProductName</section>
|
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-4">@item.ProductName</section>
|
||||||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-1">$ @item.UnitPrice.ToString("N2")</section>
|
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-1">$ @item.UnitPrice.ToString("N2")</section>
|
||||||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-1">@item.Units</section>
|
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-1">@item.Units</section>
|
||||||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section>
|
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="esh-orders_detail-section esh-orders_detail-section--right">
|
<section class="esh-orders_detail-section esh-orders_detail-section--right">
|
||||||
<article class="esh-orders_detail-titles esh-basket-titles--clean row">
|
<article class="esh-orders_detail-titles esh-basket-titles--clean row">
|
||||||
<section class="esh-orders_detail-title col-xs-9"></section>
|
<section class="esh-orders_detail-title col-9"></section>
|
||||||
<section class="esh-orders_detail-title col-xs-2">TOTAL</section>
|
<section class="esh-orders_detail-title col-2">TOTAL</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_detail-items row">
|
<article class="esh-orders_detail-items row">
|
||||||
<section class="esh-orders_detail-item col-xs-9"></section>
|
<section class="esh-orders_detail-item col-9"></section>
|
||||||
<section class="esh-orders_detail-item esh-orders_detail-item--mark col-xs-2">$ @Model.Total</section>
|
<section class="esh-orders_detail-item esh-orders_detail-item--mark col-2">$ @Model.Total</section>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,25 +14,25 @@ new Header() { Controller = "OrderManagement", Text = "Orders Management" } })
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<article class="esh-orders-titles row">
|
<article class="esh-orders-titles row">
|
||||||
<section class="esh-orders-title col-xs-2">Order number</section>
|
<section class="esh-orders-title col-2">Order number</section>
|
||||||
<section class="esh-orders-title col-xs-4">Date</section>
|
<section class="esh-orders-title col-4">Date</section>
|
||||||
<section class="esh-orders-title col-xs-2">Total</section>
|
<section class="esh-orders-title col-2">Total</section>
|
||||||
<section class="esh-orders-title col-xs-2">Status</section>
|
<section class="esh-orders-title col-2">Status</section>
|
||||||
<section class="esh-orders-title col-xs-2"></section>
|
<section class="esh-orders-title col-2"></section>
|
||||||
</article>
|
</article>
|
||||||
@if (Model != null && Model.Any())
|
@if (Model != null && Model.Any())
|
||||||
{
|
{
|
||||||
foreach (var item in Model)
|
foreach (var item in Model)
|
||||||
{
|
{
|
||||||
<article class="esh-orders-items row">
|
<article class="esh-orders-items row">
|
||||||
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
|
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
|
||||||
<section class="esh-orders-item col-xs-4">@Html.DisplayFor(modelItem => item.Date)</section>
|
<section class="esh-orders-item col-4">@Html.DisplayFor(modelItem => item.Date)</section>
|
||||||
<section class="esh-orders-item col-xs-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
|
<section class="esh-orders-item col-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
|
||||||
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
||||||
<section class="esh-orders-item col-xs-1">
|
<section class="esh-orders-item col-1">
|
||||||
<a class="esh-orders-link" asp-controller="Order" asp-action="Detail" asp-route-orderId="@item.OrderNumber">Detail</a>
|
<a class="esh-orders-link" asp-controller="Order" asp-action="Detail" asp-route-orderId="@item.OrderNumber">Detail</a>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-orders-item col-xs-1">
|
<section class="esh-orders-item col-1">
|
||||||
@if (item.Status.ToLower() == "submitted")
|
@if (item.Status.ToLower() == "submitted")
|
||||||
{
|
{
|
||||||
<a class="esh-orders-link" asp-controller="Order" asp-action="cancel" asp-route-orderId="@item.OrderNumber">Cancel</a>
|
<a class="esh-orders-link" asp-controller="Order" asp-action="cancel" asp-route-orderId="@item.OrderNumber">Cancel</a>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<section class="esh-orders_new-section">
|
<section class="esh-orders_new-section">
|
||||||
<article class="esh-orders_new-titles row">
|
<article class="esh-orders_new-titles row">
|
||||||
<section class="esh-orders_new-title col-xs-12">Order details</section>
|
<section class="esh-orders_new-title col-12">Order details</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@for (int i = 0; i < Model.OrderItems.Count; i++)
|
@for (int i = 0; i < Model.OrderItems.Count; i++)
|
||||||
@ -15,32 +15,32 @@
|
|||||||
<img class="esh-orders_new-image" src="@item.PictureUrl">
|
<img class="esh-orders_new-image" src="@item.PictureUrl">
|
||||||
<input type="hidden" value="@item.PictureUrl" name=@("orderitems[" + i + "].PictureUrl") />
|
<input type="hidden" value="@item.PictureUrl" name=@("orderitems[" + i + "].PictureUrl") />
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-orders_new-item esh-orders_new-item--middle col-xs-4">
|
<section class="esh-orders_new-item esh-orders_new-item--middle col-4">
|
||||||
@item.ProductName
|
@item.ProductName
|
||||||
<input type="hidden" value="@item.ProductName" name=@("orderitems[" + i + "].ProductName") />
|
<input type="hidden" value="@item.ProductName" name=@("orderitems[" + i + "].ProductName") />
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-orders_new-item esh-orders_new-item--middle col-xs-1">
|
<section class="esh-orders_new-item esh-orders_new-item--middle col-1">
|
||||||
$ @item.UnitPrice.ToString("N2")
|
$ @item.UnitPrice.ToString("N2")
|
||||||
<input type="hidden" value="@item.UnitPrice" name=@("orderitems[" + i + "].UnitPrice") />
|
<input type="hidden" value="@item.UnitPrice" name=@("orderitems[" + i + "].UnitPrice") />
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-orders_new-item esh-orders_new-item--middle col-xs-1">
|
<section class="esh-orders_new-item esh-orders_new-item--middle col-1">
|
||||||
@item.Units
|
@item.Units
|
||||||
<input type="hidden" value="@item.Units" name=@("orderitems[" + i + "].Units") />
|
<input type="hidden" value="@item.Units" name=@("orderitems[" + i + "].Units") />
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-orders_new-item esh-orders_new-item--middle col-xs-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section>
|
<section class="esh-orders_new-item esh-orders_new-item--middle col-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="esh-orders_new-section esh-orders_new-section--right">
|
<section class="esh-orders_new-section esh-orders_new-section--right">
|
||||||
<article class="esh-orders_new-titles row">
|
<article class="esh-orders_new-titles row">
|
||||||
<section class="esh-orders_new-title col-xs-9"></section>
|
<section class="esh-orders_new-title col-9"></section>
|
||||||
<section class="esh-orders_new-title col-xs-2">Total</section>
|
<section class="esh-orders_new-title col-2">Total</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders_new-items row">
|
<article class="esh-orders_new-items row">
|
||||||
<section class="esh-orders_new-item col-xs-9"></section>
|
<section class="esh-orders_new-item col-9"></section>
|
||||||
<section class="esh-orders_new-item esh-orders_new-item--mark col-xs-2">
|
<section class="esh-orders_new-item esh-orders_new-item--mark col-2">
|
||||||
$ @Model.Total.ToString("N2")
|
$ @Model.Total.ToString("N2")
|
||||||
<input type="hidden" value="@Model.Total" name="Total"/>
|
<input type="hidden" value="@Model.Total" name="Total"/>
|
||||||
</section>
|
</section>
|
||||||
|
@ -12,21 +12,21 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<article class="esh-orders-titles row">
|
<article class="esh-orders-titles row">
|
||||||
<section class="esh-orders-title col-xs-2">Order number</section>
|
<section class="esh-orders-title col-2">Order number</section>
|
||||||
<section class="esh-orders-title col-xs-4">Date</section>
|
<section class="esh-orders-title col-4">Date</section>
|
||||||
<section class="esh-orders-title col-xs-2">Total</section>
|
<section class="esh-orders-title col-2">Total</section>
|
||||||
<section class="esh-orders-title col-xs-2">Status</section>
|
<section class="esh-orders-title col-2">Status</section>
|
||||||
<section class="esh-orders-title col-xs-2"></section>
|
<section class="esh-orders-title col-2"></section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@foreach (var item in Model)
|
@foreach (var item in Model)
|
||||||
{
|
{
|
||||||
<article class="esh-orders-items row">
|
<article class="esh-orders-items row">
|
||||||
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
|
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
|
||||||
<section class="esh-orders-item col-xs-4">@Html.DisplayFor(modelItem => item.Date)</section>
|
<section class="esh-orders-item col-4">@Html.DisplayFor(modelItem => item.Date)</section>
|
||||||
<section class="esh-orders-item col-xs-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
|
<section class="esh-orders-item col-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
|
||||||
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
||||||
<section class="esh-orders-item col-xs-2">
|
<section class="esh-orders-item col-2">
|
||||||
<form asp-action="OrderProcess" id="orderForm+@item.OrderNumber" method="post">
|
<form asp-action="OrderProcess" id="orderForm+@item.OrderNumber" method="post">
|
||||||
<input type="hidden" name="orderId" value="@item.OrderNumber" />
|
<input type="hidden" name="orderId" value="@item.OrderNumber" />
|
||||||
<select name="actionCode" asp-items="@item.ActionCodeSelectList"
|
<select name="actionCode" asp-items="@item.ActionCodeSelectList"
|
||||||
|
@ -23,34 +23,29 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="esh-basket-title col-xs-3">Product</section>
|
<section class="esh-basket-title col-3">Product</section>
|
||||||
<section class="esh-basket-title col-xs-3 hidden-lg-down"></section>
|
<section class="esh-basket-title col-3 hidden-lg-down"></section>
|
||||||
<section class="esh-basket-title col-xs-2">Price</section>
|
<section class="esh-basket-title col-2">Price</section>
|
||||||
<section class="esh-basket-title col-xs-2">Quantity</section>
|
<section class="esh-basket-title col-2">Quantity</section>
|
||||||
<section class="esh-basket-title col-xs-2">Cost</section>
|
<section class="esh-basket-title col-2">Cost</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@for (int i = 0; i < Model.Items.Count; i++)
|
@for (int i = 0; i < Model.Items.Count; i++)
|
||||||
{
|
{
|
||||||
var item = Model.Items[i];
|
var item = Model.Items[i];
|
||||||
|
|
||||||
<article class="esh-basket-items row">
|
<article class="esh-basket-items row">
|
||||||
<div>
|
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
<img class="esh-basket-image" src="@item.PictureUrl" />
|
||||||
<img class="esh-basket-image" src="@item.PictureUrl" />
|
</section>
|
||||||
</section>
|
<section class="esh-basket-item esh-basket-item--middle col-3">@item.ProductName</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-3">@item.ProductName</section>
|
<section class="esh-basket-item esh-basket-item--middle col-2">$ @item.UnitPrice.ToString("N2")</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">$ @item.UnitPrice.ToString("N2")</section>
|
<section class="esh-basket-item esh-basket-item--middle col-2">
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">
|
<input type="hidden" name="@("quantities[" + i +"].Key")" value="@item.Id" />
|
||||||
<input type="hidden" name="@("quantities[" + i +"].Key")" value="@item.Id" />
|
<input type="number" class="esh-basket-input" min="1" name="@("quantities[" + i +"].Value")" value="@item.Quantity" />
|
||||||
<input type="number" class="esh-basket-input" min="1" name="@("quantities[" + i +"].Value")" value="@item.Quantity" />
|
</section>
|
||||||
</section>
|
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-2">$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")</section>
|
</article>
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<div class="esh-basket-items--border row">
|
<div class="esh-basket-items--border row">
|
||||||
@if (item.OldUnitPrice != 0)
|
@if (item.OldUnitPrice != 0)
|
||||||
@ -63,21 +58,21 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<article class="esh-basket-titles esh-basket-titles--clean row">
|
<article class="esh-basket-titles esh-basket-titles--clean row">
|
||||||
<section class="esh-basket-title col-xs-10"></section>
|
<section class="esh-basket-title col-10"></section>
|
||||||
<section class="esh-basket-title col-xs-2">Total</section>
|
<section class="esh-basket-title col-2">Total</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-basket-items row">
|
<article class="esh-basket-items row">
|
||||||
<section class="esh-basket-item col-xs-10"></section>
|
<section class="esh-basket-item col-10"></section>
|
||||||
<section class="esh-basket-item esh-basket-item--mark col-xs-2">$ @Model.Total()</section>
|
<section class="esh-basket-item esh-basket-item--mark col-2">$ @Model.Total()</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-basket-items row">
|
<article class="esh-basket-items row">
|
||||||
<section class="esh-basket-item col-xs-7"></section>
|
<section class="esh-basket-item col-7"></section>
|
||||||
<section class="esh-basket-item col-xs-2">
|
<section class="esh-basket-item col-2">
|
||||||
<button class="btn esh-basket-checkout" name="name" value="" type="submit">[ Update ]</button>
|
<button class="btn esh-basket-checkout" name="name" value="" type="submit">[ Update ]</button>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-basket-item col-xs-3">
|
<section class="esh-basket-item col-3">
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
class="btn esh-basket-checkout"
|
class="btn esh-basket-checkout"
|
||||||
value="[ Checkout ]" name="action" />
|
value="[ Checkout ]" name="action" />
|
||||||
|
@ -32,11 +32,11 @@
|
|||||||
</environment>
|
</environment>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="navbar navbar-light navbar-static-top">
|
<header class="esh-app-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<article class="row">
|
<article class="row">
|
||||||
|
|
||||||
<section class="col-lg-7 col-md-6 col-xs-12">
|
<section class="col-lg-7 col-md-6 col-12">
|
||||||
<a class="navbar-brand" routerLink="catalog">
|
<a class="navbar-brand" routerLink="catalog">
|
||||||
<a asp-area="" asp-controller="Catalog" asp-action="Index">
|
<a asp-area="" asp-controller="Catalog" asp-action="Index">
|
||||||
<img src="~/images/brand.png" />
|
<img src="~/images/brand.png" />
|
||||||
@ -96,24 +96,23 @@
|
|||||||
if ('@User.Identity.IsAuthenticated' === 'True') {
|
if ('@User.Identity.IsAuthenticated' === 'True') {
|
||||||
var timerId;
|
var timerId;
|
||||||
|
|
||||||
let connection = stablishConnection();
|
stablishConnection((conn) => registerNotificationHandlers(conn));
|
||||||
|
|
||||||
connection.start().then(function () {
|
|
||||||
console.log('User Registered to Signalr Hub');
|
|
||||||
});
|
|
||||||
|
|
||||||
registerNotificationHandlers(connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stablishConnection() {
|
function stablishConnection(cb) {
|
||||||
return new signalR.HubConnectionBuilder()
|
let connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl('@settings.Value.SignalrHubUrl/hub/notificationhub', {
|
.withUrl('@settings.Value.SignalrHubUrl/hub/notificationhub', {
|
||||||
transport: signalR.HttpTransportType.LongPolling,
|
transport: signalR.HttpTransportType.LongPolling,
|
||||||
accessTokenFactory: () => {
|
accessTokenFactory: () => {
|
||||||
return "Authorization", getToken();
|
return "Authorization", getToken();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
connection.start().then(function () {
|
||||||
|
console.log('User Registered to Signalr Hub');
|
||||||
|
cb(connection);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerNotificationHandlers(connection) {
|
function registerNotificationHandlers(connection) {
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Redis" Version="0.3.3" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Redis" Version="0.3.3" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Fabric.MSBuild" Version="1.6.5" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Fabric.MSBuild" Version="1.6.5" />
|
||||||
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="1.0.163" />
|
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="1.0.163" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.5.180" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||||
|
@ -8,28 +8,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"provider": "unpkg",
|
"provider": "unpkg",
|
||||||
"library": "bootstrap@3.3.7",
|
"library": "bootstrap@4.1.3",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/css/bootstrap.css",
|
"dist/css/bootstrap.css",
|
||||||
"dist/css/bootstrap.css.map",
|
"dist/css/bootstrap.css.map",
|
||||||
"dist/css/bootstrap.min.css",
|
"dist/css/bootstrap.min.css",
|
||||||
"dist/css/bootstrap.min.css.map",
|
"dist/css/bootstrap.min.css.map",
|
||||||
"dist/css/bootstrap-theme.css",
|
|
||||||
"dist/css/bootstrap-theme.css.map",
|
|
||||||
"dist/css/bootstrap-theme.min.css",
|
|
||||||
"dist/css/bootstrap-theme.min.css.map",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.eot",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.svg",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.ttf",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.woff",
|
|
||||||
"dist/fonts/glyphicons-halflings-regular.woff2",
|
|
||||||
"dist/js/bootstrap.js",
|
"dist/js/bootstrap.js",
|
||||||
"dist/js/bootstrap.min.js",
|
"dist/js/bootstrap.min.js"
|
||||||
"fonts/glyphicons-halflings-regular.eot",
|
|
||||||
"fonts/glyphicons-halflings-regular.svg",
|
|
||||||
"fonts/glyphicons-halflings-regular.ttf",
|
|
||||||
"fonts/glyphicons-halflings-regular.woff",
|
|
||||||
"fonts/glyphicons-halflings-regular.woff2"
|
|
||||||
],
|
],
|
||||||
"destination": "wwwroot/lib/bootstrap/"
|
"destination": "wwwroot/lib/bootstrap/"
|
||||||
},
|
},
|
||||||
|
@ -12,3 +12,7 @@
|
|||||||
width: 230px;
|
width: 230px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.esh-app-header {
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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{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-header{margin:15px;}
|
@ -20,4 +20,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-header{
|
||||||
|
margin:15px;
|
||||||
|
}
|
||||||
}
|
}
|
@ -73,6 +73,12 @@
|
|||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.esh-campaigns-card-footer-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.esh-campaigns-form-button {
|
.esh-campaigns-form-button {
|
||||||
background-color: #83D01B;
|
background-color: #83D01B;
|
||||||
border: none;
|
border: none;
|
||||||
@ -81,6 +87,7 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: all 0.35s;
|
transition: all 0.35s;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
margin-top: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.esh-campaigns-button.is-disabled {
|
.esh-campaigns-button.is-disabled {
|
||||||
|
@ -71,6 +71,7 @@
|
|||||||
margin-top: -1.5rem;
|
margin-top: -1.5rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
transition: all 0.35s;
|
transition: all 0.35s;
|
||||||
|
margin-bottom: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.esh-catalog-send:hover {
|
.esh-catalog-send:hover {
|
||||||
|
@ -1 +1 @@
|
|||||||
.esh-catalog-hero{background-image:url("../../images/main_banner.png");background-size:cover;height:260px;width:100%;}.esh-catalog-title{position:relative;top:74.28571px;}.esh-catalog-filters{background-color:#00a69c;height:65px;}.esh-catalog-filter{-webkit-appearance:none;background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;}.esh-catalog-filter option{background-color:#00a69c;}.esh-catalog-label{display:inline-block;position:relative;z-index:0;}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1;}.esh-catalog-label::after{background-image:url("../../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1;}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;margin-top:-1.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{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important;}@media screen and (max-width:1024px){.esh-catalog-item{width:50%;}}@media screen and (max-width:768px){.esh-catalog-item{width:100%;}}.esh-catalog-thumbnail{max-width:370px;width:100%;}.esh-catalog-button{background-color:#83d01b;border:0;color:#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{font-size:28px;font-weight:900;text-align:center;}.esh-catalog-price::before{content:'$';}
|
.esh-catalog-hero{background-image:url("../../images/main_banner.png");background-size:cover;height:260px;width:100%;}.esh-catalog-title{position:relative;top:74.28571px;}.esh-catalog-filters{background-color:#00a69c;height:65px;}.esh-catalog-filter{-webkit-appearance:none;background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;}.esh-catalog-filter option{background-color:#00a69c;}.esh-catalog-label{display:inline-block;position:relative;z-index:0;}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1;}.esh-catalog-label::after{background-image:url("../../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1;}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;margin-top:-1.5rem;padding:.5rem;transition:all .35s;margin-bottom:-6px;}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s;}.esh-catalog-items{margin-top:1rem;}.esh-catalog-item{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important;}@media screen and (max-width:1024px){.esh-catalog-item{width:50%;}}@media screen and (max-width:768px){.esh-catalog-item{width:100%;}}.esh-catalog-thumbnail{max-width:370px;width:100%;}.esh-catalog-button{background-color:#83d01b;border:0;color:#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{font-size:28px;font-weight:900;text-align:center;}.esh-catalog-price::before{content:'$';}
|
@ -80,6 +80,7 @@
|
|||||||
margin-top: -$filter-padding * 3;
|
margin-top: -$filter-padding * 3;
|
||||||
padding: $filter-padding;
|
padding: $filter-padding;
|
||||||
transition: all $animation-speed-default;
|
transition: all $animation-speed-default;
|
||||||
|
margin-bottom: -6px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $color-secondary-darker;
|
background-color: $color-secondary-darker;
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
||||||
"project": {
|
|
||||||
"name": "WebSPA"
|
|
||||||
},
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"root": "Client",
|
|
||||||
"outDir": "wwwroot",
|
|
||||||
"assets": [
|
|
||||||
"assets",
|
|
||||||
"favicon.ico"
|
|
||||||
],
|
|
||||||
"index": "index.html",
|
|
||||||
"main": "main.ts",
|
|
||||||
"polyfills": "polyfills.ts",
|
|
||||||
"test": "test.ts",
|
|
||||||
"tsconfig": "tsconfig.app.json",
|
|
||||||
"testTsconfig": "tsconfig.spec.json",
|
|
||||||
"prefix": "app",
|
|
||||||
"styles": [
|
|
||||||
"globals.scss",
|
|
||||||
"../node_modules/bootstrap/scss/bootstrap.scss",
|
|
||||||
"../node_modules/ng2-toastr/bundles/ng2-toastr.min.css"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
|
||||||
"environmentSource": "environments/environment.ts",
|
|
||||||
"environments": {
|
|
||||||
"dev": "environments/environment.ts",
|
|
||||||
"prod": "environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"e2e": {
|
|
||||||
"protractor": {
|
|
||||||
"config": "./protractor.conf.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": [
|
|
||||||
{
|
|
||||||
"project": "Client/tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"project": "Client/tsconfig.spec.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"project": "e2e/tsconfig.e2e.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"test": {
|
|
||||||
"karma": {
|
|
||||||
"config": "./karma.conf.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"styleExt": "scss",
|
|
||||||
"component": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,7 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
@import "~ngx-toastr/toastr-bs4-alert.scss";
|
||||||
|
|
||||||
@import './modules/variables';
|
@import './modules/variables';
|
||||||
|
|
||||||
$dist: './fonts/Montserrat-Regular';
|
$dist: './fonts/Montserrat-Regular';
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
<header class="navbar navbar-light navbar-static-top">
|
<header class="esh-app-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<article class="row">
|
<article class="row">
|
||||||
|
<section class="col-lg-7 col-md-6 col-12">
|
||||||
<section class="col-lg-7 col-md-6 col-xs-12">
|
|
||||||
<a class="navbar-brand" routerLink="catalog">
|
<a class="navbar-brand" routerLink="catalog">
|
||||||
<img src="assets/images/brand.png" />
|
<img src="assets/images/brand.png" />
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="col-lg-4 col-md-5 col-xs-12">
|
<section class="col-lg-4 col-md-5 col-12">
|
||||||
<esh-identity></esh-identity>
|
<esh-identity></esh-identity>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="col-lg-1 col-xs-12">
|
<section class="col-lg-1 col-12">
|
||||||
<esh-basket-status *ngIf="Authenticated"></esh-basket-status>
|
<esh-basket-status *ngIf="Authenticated"></esh-basket-status>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -32,7 +31,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="col-sm-6">
|
<section class="col-sm-6">
|
||||||
<img class="esh-app-footer-text hidden-xs" src="assets/images/main_footer_text.png" width="335" height="26" alt="footer text image" />
|
<img class="esh-app-footer-text hidden" src="assets/images/main_footer_text.png" width="335" height="26" alt="footer text image" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
padding-bottom: $padding;
|
padding-bottom: $padding;
|
||||||
padding-top: $padding;
|
padding-top: $padding;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
$height: 50px;
|
$height: 50px;
|
||||||
|
|
||||||
@ -18,6 +19,9 @@
|
|||||||
height: $height;
|
height: $height;
|
||||||
width: 230px;
|
width: 230px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
margin: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { Component, ViewEncapsulation, OnInit, ViewContainerRef } from '@angular/core';
|
import { Component, OnInit, ViewContainerRef } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { Subscription } from 'rxjs';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
|
||||||
|
|
||||||
import { DataService } from './shared/services/data.service';
|
|
||||||
import { SecurityService } from './shared/services/security.service';
|
import { SecurityService } from './shared/services/security.service';
|
||||||
import { ConfigurationService } from './shared/services/configuration.service';
|
import { ConfigurationService } from './shared/services/configuration.service';
|
||||||
import { SignalrService } from './shared/services/signalr.service';
|
import { SignalrService } from './shared/services/signalr.service';
|
||||||
import { ToastsManager } from 'ng2-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* App Component
|
* App Component
|
||||||
* Top Level Component
|
* Top Level Component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'esh-app',
|
selector: 'esh-app',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
@ -27,10 +24,11 @@ export class AppComponent implements OnInit {
|
|||||||
private securityService: SecurityService,
|
private securityService: SecurityService,
|
||||||
private configurationService: ConfigurationService,
|
private configurationService: ConfigurationService,
|
||||||
private signalrService: SignalrService,
|
private signalrService: SignalrService,
|
||||||
private toastr: ToastsManager,
|
private toastr: ToastrService,
|
||||||
vcr: ViewContainerRef
|
vcr: ViewContainerRef
|
||||||
) {
|
) {
|
||||||
this.toastr.setRootViewContainerRef(vcr);
|
// TODO: Set Taster Root (Overlay) container
|
||||||
|
//this.toastr.setRootViewContainerRef(vcr);
|
||||||
this.Authenticated = this.securityService.IsAuthorized;
|
this.Authenticated = this.securityService.IsAuthorized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,35 +1,32 @@
|
|||||||
import { NgModule, NgModuleFactoryLoader } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
// import { FormsModule } from '@angular/forms';
|
import { HttpClientModule } from "@angular/common/http";
|
||||||
import { HttpModule } from '@angular/http';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { routing } from './app.routes';
|
import { routing } from './app.routes';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
import { CatalogModule } from './catalog/catalog.module';
|
import { CatalogModule } from './catalog/catalog.module';
|
||||||
import { OrdersModule } from './orders/orders.module';
|
import { OrdersModule } from './orders/orders.module';
|
||||||
import { BasketModule } from './basket/basket.module';
|
import { BasketModule } from './basket/basket.module';
|
||||||
import { CampaignsModule } from './campaigns/campaigns.module';
|
import { CampaignsModule } from './campaigns/campaigns.module';
|
||||||
import { ToastModule } from 'ng2-toastr/ng2-toastr';
|
import { ToastrModule } from 'ngx-toastr';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
ToastModule.forRoot(),
|
ToastrModule.forRoot(),
|
||||||
routing,
|
routing,
|
||||||
HttpModule,
|
HttpClientModule,
|
||||||
// Only module that app module loads
|
// Only module that app module loads
|
||||||
SharedModule.forRoot(),
|
SharedModule.forRoot(),
|
||||||
CatalogModule,
|
CatalogModule,
|
||||||
OrdersModule,
|
OrdersModule,
|
||||||
BasketModule,
|
BasketModule,
|
||||||
CampaignsModule
|
CampaignsModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService
|
AppService
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { BasketService } from '../basket.service';
|
import { BasketService } from '../basket.service';
|
||||||
import { BasketWrapperService } from '../../shared/services/basket.wrapper.service';
|
import { BasketWrapperService } from '../../shared/services/basket.wrapper.service';
|
||||||
|
@ -9,11 +9,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="esh-basket-titles row">
|
<article class="esh-basket-titles row">
|
||||||
<section class="esh-basket-title col-xs-3">Product</section>
|
<section class="esh-basket-title col-3">Product</section>
|
||||||
<section class="esh-basket-title col-xs-3 hidden-lg-down"></section>
|
<section class="esh-basket-title col-3 hidden-lg-down"></section>
|
||||||
<section class="esh-basket-title col-xs-2">Price</section>
|
<section class="esh-basket-title col-2">Price</section>
|
||||||
<section class="esh-basket-title col-xs-2">Quantity</section>
|
<section class="esh-basket-title col-2">Quantity</section>
|
||||||
<section class="esh-basket-title col-xs-2">Cost</section>
|
<section class="esh-basket-title col-2">Cost</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div *ngFor="let item of basket?.items" class="esh-basket-items--border">
|
<div *ngFor="let item of basket?.items" class="esh-basket-items--border">
|
||||||
@ -22,9 +22,9 @@
|
|||||||
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
||||||
<img class="esh-basket-image" src="{{item.pictureUrl}}"/>
|
<img class="esh-basket-image" src="{{item.pictureUrl}}"/>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-3">{{item.productName}}</section>
|
<section class="esh-basket-item esh-basket-item--middle col-3">{{item.productName}}</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">$ {{item.unitPrice | number:'.2-2'}}</section>
|
<section class="esh-basket-item esh-basket-item--middle col-2">$ {{item.unitPrice | number:'.2-2'}}</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">
|
<section class="esh-basket-item esh-basket-item--middle col-2">
|
||||||
<input id="quantity"
|
<input id="quantity"
|
||||||
class="esh-basket-input"
|
class="esh-basket-input"
|
||||||
type="number"
|
type="number"
|
||||||
@ -32,7 +32,7 @@
|
|||||||
[(ngModel)]="item.quantity"
|
[(ngModel)]="item.quantity"
|
||||||
(change)="itemQuantityChanged(item)"/>
|
(change)="itemQuantityChanged(item)"/>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ {{(item.unitPrice * item.quantity) | number:'.2-2'}}</section>
|
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-2">$ {{(item.unitPrice * item.quantity) | number:'.2-2'}}</section>
|
||||||
</article>
|
</article>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="esh-basket-items-margin-left1 row">
|
<div class="esh-basket-items-margin-left1 row">
|
||||||
@ -43,21 +43,21 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<article class="esh-basket-titles esh-basket-titles--clean row">
|
<article class="esh-basket-titles esh-basket-titles--clean row">
|
||||||
<section class="esh-basket-title col-xs-9"></section>
|
<section class="esh-basket-title col-9"></section>
|
||||||
<section class="esh-basket-title col-xs-2">Total</section>
|
<section class="esh-basket-title col-2">Total</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-basket-items row">
|
<article class="esh-basket-items row">
|
||||||
<section class="esh-basket-item col-xs-9"></section>
|
<section class="esh-basket-item col-9"></section>
|
||||||
<section class="esh-basket-item esh-basket-item--mark col-xs-2">$ {{totalPrice | number:'.2-2'}}</section>
|
<section class="esh-basket-item esh-basket-item--mark col-2">$ {{totalPrice | number:'.2-2'}}</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-basket-items row">
|
<article class="esh-basket-items row">
|
||||||
<section class="esh-basket-item col-xs-7"></section>
|
<section class="esh-basket-item col-7"></section>
|
||||||
<section class="esh-basket-item col-xs-2">
|
<section class="esh-basket-item col-2">
|
||||||
<button class="btn esh-basket-checkout" (click)="update($event)">[ Update ]</button>
|
<button class="btn esh-basket-checkout" (click)="update($event)">[ Update ]</button>
|
||||||
</section>
|
</section>
|
||||||
<section class="esh-basket-item col-xs-3">
|
<section class="esh-basket-item col-3">
|
||||||
<div (click)="checkOut($event)" class="btn esh-basket-checkout">[ Checkout ]</div>
|
<div (click)="checkOut($event)" class="btn esh-basket-checkout">[ Checkout ]</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user