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
|
||||
|
||||
> ### 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.
|
||||
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.
|
||||
> 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.
|
||||
> <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
|
||||
> 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>
|
||||
<img src="img/eShopOnContainers-Architecture-With-Azure-API-Management.png">
|
||||
@ -56,9 +56,9 @@ The architecture proposes a microservice oriented architecture implementation wi
|
||||
<p>
|
||||
|
||||
> ### 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> 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
|
||||
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)
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
- (*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\azure\service.yaml
|
||||
kubectl apply -f nginx-ingress\patch-service-without-rbac.yaml
|
||||
kubectl apply -f nginx-ingress\cloud-generic.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
|
||||
kubectl apply -f nginx-ingress\namespace.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
|
||||
kubectl apply -f nginx-ingress\mandatory.yaml
|
||||
|
||||
|
||||
|
||||
|
@ -113,6 +113,7 @@ ExecKube -cmd 'delete configmap internalurls'
|
||||
ExecKube -cmd 'delete configmap urls'
|
||||
ExecKube -cmd 'delete configmap externalcfg'
|
||||
ExecKube -cmd 'delete configmap ocelot'
|
||||
ExecKube -cmd 'delete -f ingress.yaml'
|
||||
|
||||
# start sql, rabbitmq, frontend deployments
|
||||
if ($deployInfrastructure) {
|
||||
@ -204,5 +205,8 @@ ExecKube -cmd 'rollout resume deployments/apigwwm'
|
||||
ExecKube -cmd 'rollout resume deployments/apigwws'
|
||||
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
|
||||
|
||||
|
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
|
||||
ordering: ordering # service name for ordering api
|
||||
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
|
||||
mvc: webmvc # service name for web mvc
|
||||
spa: webspa # service name for web spa
|
||||
|
@ -8,11 +8,19 @@ Param(
|
||||
[parameter(Mandatory=$false)][bool]$clean=$true,
|
||||
[parameter(Mandatory=$false)][string]$aksName="",
|
||||
[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
|
||||
|
||||
$ingressValuesFile="ingress_values.yaml"
|
||||
|
||||
if ($ingressValuesFile) {
|
||||
$ingressValuesFile="ingress_values_dockerk8s.yaml"
|
||||
$dns="localhost"
|
||||
}
|
||||
|
||||
if ($externalDns -eq "aks") {
|
||||
if ([string]::IsNullOrEmpty($aksName) -or [string]::IsNullOrEmpty($aksRg)) {
|
||||
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) {
|
||||
foreach ($infra in $infras) {
|
||||
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) {
|
||||
Write-Host "Installing: $chart" -ForegroundColor Green
|
||||
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 {
|
||||
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__UseAzureServiceBus: "{{ .Values.inf.eventbus.useAzure }}"
|
||||
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>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Ocelot" Version="3.0.0" />
|
||||
<PackageReference Include="Ocelot" Version="12.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,11 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CacheManager.Core;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -217,14 +217,16 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
||||
if (subscription.IsDynamic)
|
||||
{
|
||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
||||
if (handler == null) continue;
|
||||
dynamic eventData = JObject.Parse(message);
|
||||
await handler.Handle(eventData);
|
||||
}
|
||||
else
|
||||
{
|
||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||
if (handler == null) continue;
|
||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
|
||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
||||
}
|
||||
|
@ -163,14 +163,16 @@
|
||||
if (subscription.IsDynamic)
|
||||
{
|
||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
||||
if (handler == null) continue;
|
||||
dynamic eventData = JObject.Parse(message);
|
||||
await handler.Handle(eventData);
|
||||
}
|
||||
else
|
||||
{
|
||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
|
||||
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);
|
||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
||||
}
|
||||
|
@ -7,7 +7,8 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
||||
public enum EventStateEnum
|
||||
{
|
||||
NotPublished = 0,
|
||||
Published = 1,
|
||||
PublishedFailed = 2
|
||||
InProgress = 1,
|
||||
Published = 2,
|
||||
PublishedFailed = 3
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
using System.Linq;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
||||
{
|
||||
@ -11,7 +14,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
||||
private IntegrationEventLogEntry() { }
|
||||
public IntegrationEventLogEntry(IntegrationEvent @event)
|
||||
{
|
||||
EventId = @event.Id;
|
||||
EventId = @event.Id;
|
||||
CreationTime = @event.CreationDate;
|
||||
EventTypeName = @event.GetType().FullName;
|
||||
Content = JsonConvert.SerializeObject(@event);
|
||||
@ -20,9 +23,19 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
||||
}
|
||||
public Guid EventId { 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 int TimesSent { get; set; }
|
||||
public DateTime CreationTime { 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
|
||||
{
|
||||
Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync();
|
||||
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.Diagnostics;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services
|
||||
@ -12,6 +17,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
||||
{
|
||||
private readonly IntegrationEventLogContext _integrationEventLogContext;
|
||||
private readonly DbConnection _dbConnection;
|
||||
private readonly List<Type> _eventTypes;
|
||||
|
||||
public IntegrationEventLogService(DbConnection dbConnection)
|
||||
{
|
||||
@ -21,6 +27,20 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
||||
.UseSqlServer(_dbConnection)
|
||||
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning))
|
||||
.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)
|
||||
@ -38,11 +58,28 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi
|
||||
return _integrationEventLogContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public Task MarkEventAsPublishedAsync(IntegrationEvent @event)
|
||||
public Task MarkEventAsPublishedAsync(Guid eventId)
|
||||
{
|
||||
var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == @event.Id);
|
||||
eventLogEntry.TimesSent++;
|
||||
eventLogEntry.State = EventStateEnum.Published;
|
||||
return UpdateEventStatus(eventId, 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);
|
||||
|
||||
|
@ -3,7 +3,5 @@
|
||||
public class BasketSettings
|
||||
{
|
||||
public string ConnectionString { get; set; }
|
||||
|
||||
public string EventBusConnection { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -29,9 +29,16 @@ namespace Catalog.API.IntegrationEvents
|
||||
|
||||
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
|
||||
{
|
||||
_eventBus.Publish(evt);
|
||||
|
||||
await _eventLogService.MarkEventAsPublishedAsync(evt);
|
||||
try
|
||||
{
|
||||
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)
|
||||
|
@ -232,7 +232,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API
|
||||
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
|
||||
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
||||
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
||||
|
||||
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>();
|
||||
|
||||
|
@ -216,6 +216,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers
|
||||
// delete authentication cookie
|
||||
await HttpContext.SignOutAsync();
|
||||
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
|
||||
|
||||
// set this so UI rendering sees an anonymous user
|
||||
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
|
||||
|
@ -55,7 +55,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Extensions\" />
|
||||
<Folder Include="wwwroot\lib\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -18,7 +18,7 @@
|
||||
</environment>
|
||||
</head>
|
||||
<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="row">
|
||||
<div class="navbar-header col-sm-6 col-xs-8">
|
||||
|
@ -8,28 +8,14 @@
|
||||
},
|
||||
{
|
||||
"provider": "unpkg",
|
||||
"library": "bootstrap@3.3.7",
|
||||
"library": "bootstrap@4.1.3",
|
||||
"files": [
|
||||
"dist/css/bootstrap.css",
|
||||
"dist/css/bootstrap.css.map",
|
||||
"dist/css/bootstrap.min.css",
|
||||
"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.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"
|
||||
"dist/js/bootstrap.min.js"
|
||||
],
|
||||
"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
|
||||
{
|
||||
using Domain.AggregatesModel.OrderAggregate;
|
||||
using global::Ordering.API.Application.IntegrationEvents;
|
||||
using global::Ordering.API.Application.IntegrationEvents.Events;
|
||||
using MediatR;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
|
||||
@ -15,17 +17,26 @@
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly IIdentityService _identityService;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||
|
||||
// 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));
|
||||
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
|
||||
}
|
||||
|
||||
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
|
||||
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
|
||||
// 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 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(
|
||||
order.Id, order.OrderStatus.Name, buyer.Name, orderStockList);
|
||||
await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToAwaitingValidationIntegrationEvent);
|
||||
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToAwaitingValidationIntegrationEvent);
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@
|
||||
buyer.Name,
|
||||
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 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();
|
||||
|
||||
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}.");
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
||||
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||
|
||||
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 Ordering.API.Application.Commands;
|
||||
using Ordering.API.Application.IntegrationEvents.Events;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -7,11 +9,11 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||
{
|
||||
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>
|
||||
@ -24,9 +26,8 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||
/// <returns></returns>
|
||||
public async Task Handle(GracePeriodConfirmedIntegrationEvent @event)
|
||||
{
|
||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
||||
orderToUpdate.SetAwaitingValidationStatus();
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
var command = new SetAwaitingValidationOrderStatusCommand(@event.OrderId);
|
||||
await _mediator.Send(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,27 @@
|
||||
namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||
{
|
||||
using MediatR;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||
using Ordering.API.Application.Commands;
|
||||
using Ordering.API.Application.IntegrationEvents.Events;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class OrderPaymentFailedIntegrationEventHandler :
|
||||
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)
|
||||
{
|
||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
||||
|
||||
orderToUpdate.SetCancelledStatus();
|
||||
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
var command = new CancelOrderCommand(@event.OrderId);
|
||||
await _mediator.Send(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,27 @@
|
||||
namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||
{
|
||||
using MediatR;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||
using Ordering.API.Application.Commands;
|
||||
using Ordering.API.Application.IntegrationEvents.Events;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class OrderPaymentSuccededIntegrationEventHandler :
|
||||
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)
|
||||
{
|
||||
// Simulate a work time for validating the payment
|
||||
await Task.Delay(10000);
|
||||
|
||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
||||
|
||||
orderToUpdate.SetPaidStatus();
|
||||
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
var command = new SetPaidOrderStatusCommand(@event.OrderId);
|
||||
await _mediator.Send(command);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,27 +4,24 @@
|
||||
using System.Threading.Tasks;
|
||||
using Events;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||
using MediatR;
|
||||
using System;
|
||||
using Ordering.API.Application.Commands;
|
||||
|
||||
public class OrderStockConfirmedIntegrationEventHandler :
|
||||
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)
|
||||
{
|
||||
// Simulate a work time for confirming the stock
|
||||
await Task.Delay(10000);
|
||||
|
||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
||||
|
||||
orderToUpdate.SetStockConfirmedStatus();
|
||||
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
var command = new SetStockConfirmedOrderStatusCommand(@event.OrderId);
|
||||
await _mediator.Send(command);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,27 +5,27 @@
|
||||
using Events;
|
||||
using System.Linq;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||
using MediatR;
|
||||
using Ordering.API.Application.Commands;
|
||||
|
||||
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)
|
||||
{
|
||||
var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId);
|
||||
|
||||
var orderStockRejectedItems = @event.OrderStockItems
|
||||
.FindAll(c => !c.HasStock)
|
||||
.Select(c => c.ProductId);
|
||||
.Select(c => c.ProductId)
|
||||
.ToList();
|
||||
|
||||
orderToUpdate.SetCancelledStatusWhenStockIsRejected(orderStockRejectedItems);
|
||||
|
||||
await _orderRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
var command = new SetStockRejectedOrderStatusCommand(@event.OrderId, orderStockRejectedItems);
|
||||
await _mediator.Send(command);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,15 +11,13 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILoggerFactory _logger;
|
||||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||
private readonly ILoggerFactory _logger;
|
||||
|
||||
public UserCheckoutAcceptedIntegrationEventHandler(IMediator mediator,
|
||||
ILoggerFactory logger, IOrderingIntegrationEventService orderingIntegrationEventService)
|
||||
ILoggerFactory logger)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -34,11 +32,7 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling
|
||||
public async Task Handle(UserCheckoutAcceptedIntegrationEvent eventMsg)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
Task PublishThroughEventBusAsync(IntegrationEvent evt);
|
||||
Task PublishEventsThroughEventBusAsync();
|
||||
Task AddAndSaveEventAsync(IntegrationEvent evt);
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,14 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure;
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ordering.API.Application.IntegrationEvents
|
||||
@ -17,34 +19,42 @@ namespace Ordering.API.Application.IntegrationEvents
|
||||
private readonly Func<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory;
|
||||
private readonly IEventBus _eventBus;
|
||||
private readonly OrderingContext _orderingContext;
|
||||
private readonly IntegrationEventLogContext _eventLogContext;
|
||||
private readonly IIntegrationEventLogService _eventLogService;
|
||||
|
||||
public OrderingIntegrationEventService(IEventBus eventBus, OrderingContext orderingContext,
|
||||
Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory)
|
||||
public OrderingIntegrationEventService(IEventBus eventBus,
|
||||
OrderingContext orderingContext,
|
||||
IntegrationEventLogContext eventLogContext,
|
||||
Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory)
|
||||
{
|
||||
_orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext));
|
||||
_eventLogContext = eventLogContext ?? throw new ArgumentNullException(nameof(eventLogContext));
|
||||
_integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory));
|
||||
_eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
|
||||
_eventLogService = _integrationEventLogServiceFactory(_orderingContext.Database.GetDbConnection());
|
||||
}
|
||||
|
||||
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
|
||||
public async Task PublishEventsThroughEventBusAsync()
|
||||
{
|
||||
await SaveEventAndOrderingContextChangesAsync(evt);
|
||||
_eventBus.Publish(evt);
|
||||
await _eventLogService.MarkEventAsPublishedAsync(evt);
|
||||
var pendindLogEvents = await _eventLogService.RetrieveEventLogsPendingToPublishAsync();
|
||||
foreach (var logEvt in pendindLogEvents)
|
||||
{
|
||||
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():
|
||||
//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());
|
||||
});
|
||||
await _eventLogService.SaveEventAsync(evt, _orderingContext.GetCurrentTransaction.GetDbTransaction());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Autofac;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||
using Ordering.API.Application.Behaviors;
|
||||
using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent;
|
||||
using Ordering.API.Application.Validations;
|
||||
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(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("name", "Test user"),
|
||||
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("sub", "1234"),
|
||||
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname","User"),
|
||||
|
@ -113,7 +113,7 @@
|
||||
eventBus.Subscribe<OrderStockConfirmedIntegrationEvent, IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent>>();
|
||||
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
|
||||
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 String Street { get; }
|
||||
public String City { get; }
|
||||
public String State { get; }
|
||||
public String Country { get; }
|
||||
public String ZipCode { get; }
|
||||
public String Street { get; private set; }
|
||||
public String City { get; private set; }
|
||||
public String State { get; private set; }
|
||||
public String Country { get; private set; }
|
||||
public String ZipCode { get; private set; }
|
||||
|
||||
private Address() { }
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
|
||||
using Ordering.Infrastructure;
|
||||
using Ordering.Infrastructure.EntityConfigurations;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -23,9 +25,12 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
|
||||
public DbSet<OrderStatus> OrderStatus { get; set; }
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
private IDbContextTransaction _currentTransaction;
|
||||
|
||||
private OrderingContext(DbContextOptions<OrderingContext> options) : base (options) { }
|
||||
|
||||
public IDbContextTransaction GetCurrentTransaction => _currentTransaction;
|
||||
|
||||
public OrderingContext(DbContextOptions<OrderingContext> options, IMediator mediator) : base(options)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
@ -60,7 +65,50 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
|
||||
var result = await base.SaveChangesAsync();
|
||||
|
||||
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>
|
||||
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace UnitTest.Ordering.Application
|
||||
{
|
||||
using global::Ordering.API.Application.IntegrationEvents;
|
||||
using global::Ordering.API.Application.Models;
|
||||
using MediatR;
|
||||
using System.Collections;
|
||||
@ -22,12 +23,14 @@ namespace UnitTest.Ordering.Application
|
||||
private readonly Mock<IOrderRepository> _orderRepositoryMock;
|
||||
private readonly Mock<IIdentityService> _identityServiceMock;
|
||||
private readonly Mock<IMediator> _mediator;
|
||||
private readonly Mock<IOrderingIntegrationEventService> _orderingIntegrationEventService;
|
||||
|
||||
public NewOrderRequestHandlerTest()
|
||||
{
|
||||
|
||||
_orderRepositoryMock = new Mock<IOrderRepository>();
|
||||
_identityServiceMock = new Mock<IIdentityService>();
|
||||
_orderingIntegrationEventService = new Mock<IOrderingIntegrationEventService>();
|
||||
_mediator = new Mock<IMediator>();
|
||||
}
|
||||
|
||||
@ -48,7 +51,7 @@ namespace UnitTest.Ordering.Application
|
||||
_identityServiceMock.Setup(svc => svc.GetUserIdentity()).Returns(buyerId);
|
||||
|
||||
//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 result = await handler.Handle(fakeOrderCmd, cltToken);
|
||||
|
||||
|
@ -15,14 +15,14 @@
|
||||
<div class="container">
|
||||
<div class="card esh-campaigns-items">
|
||||
<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>
|
||||
<p class="card-text">@Model.Description</p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">
|
||||
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
||||
</small>
|
||||
</p>
|
||||
<p class="card-text">@Model.Description</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small class="text-muted">
|
||||
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -27,24 +27,28 @@
|
||||
<div class="esh-campaigns-items" style="font-weight: 300;">
|
||||
UPDATE USER LOCATION
|
||||
</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">
|
||||
<label class="sr-only" for="longitudeInput">Name</label>
|
||||
|
||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
|
||||
<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">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="inputGroup-sizing-default">Lat</span>
|
||||
</div>
|
||||
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="latitudeInput" asp-for="Lat" placeholder="Latitude">
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
|
||||
<div class="input-group-addon">Lon</div>
|
||||
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="longitudeInput" asp-for="Lon" placeholder="Longitude">
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0 col-md-2">
|
||||
<input type="submit" value="Update" class="btn esh-campaigns-form-button" />
|
||||
</div>
|
||||
</form>
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="inputGroup-sizing-default">Lon</span>
|
||||
</div>
|
||||
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="longitudeInput" asp-for="Lon" placeholder="Longitude">
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-2 mr-sm-2 mb-sm-0 col-md-2">
|
||||
<input type="submit" value="Update" class="btn esh-campaigns-form-button" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -1,9 +1,10 @@
|
||||
@model CampaignItem
|
||||
|
||||
<form asp-controller="Campaigns" asp-action="Details" asp-route-id="@Model.Id">
|
||||
<div class="card-block">
|
||||
<form asp-controller="Campaigns" asp-action="Details" asp-route-id="@Model.Id">
|
||||
<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>
|
||||
<img class="card-img-top esh-campaigns-thumbnail" src="@Model.PictureUri" alt="@Model.Name">
|
||||
@if (ViewBag.IsCampaignDetailFunctionActive == true)
|
||||
{
|
||||
<input type="button" value="More Details" class="btn esh-campaigns-button" onClick="window.open('@Model.DetailsUri')">
|
||||
@ -11,11 +12,12 @@
|
||||
else
|
||||
{
|
||||
<input class="esh-campaigns-button" type="submit" value="More details">
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="card-footer esh-campaigns-card-footer-text">
|
||||
<small class="text-muted">
|
||||
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -87,7 +87,7 @@
|
||||
@await Html.PartialAsync("_OrderItems")
|
||||
|
||||
<section class="esh-orders_new-section">
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
|
@ -13,51 +13,51 @@
|
||||
<div class="container">
|
||||
<section class="esh-orders_detail-section">
|
||||
<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-xs-3">Date</section>
|
||||
<section class="esh-orders_detail-title col-xs-3">Total</section>
|
||||
<section class="esh-orders_detail-title col-xs-3">Status</section>
|
||||
<section class="esh-orders_detail-title col-3">Order number</section>
|
||||
<section class="esh-orders_detail-title col-3">Date</section>
|
||||
<section class="esh-orders_detail-title col-3">Total</section>
|
||||
<section class="esh-orders_detail-title col-3">Status</section>
|
||||
</article>
|
||||
|
||||
<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-xs-3">@Model.Date</section>
|
||||
<section class="esh-orders_detail-item col-xs-3">$@Model.Total</section>
|
||||
<section class="esh-orders_detail-title col-xs-3">@Model.Status</section>
|
||||
<section class="esh-orders_detail-item col-3">@Model.OrderNumber</section>
|
||||
<section class="esh-orders_detail-item col-3">@Model.Date</section>
|
||||
<section class="esh-orders_detail-item col-3">$@Model.Total</section>
|
||||
<section class="esh-orders_detail-title col-3">@Model.Status</section>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="esh-orders_detail-section">
|
||||
<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 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>
|
||||
</section>
|
||||
|
||||
<section class="esh-orders_detail-section">
|
||||
<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 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 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 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>
|
||||
</section>
|
||||
|
||||
<section class="esh-orders_detail-section">
|
||||
<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>
|
||||
|
||||
@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">
|
||||
<img class="esh-orders_detail-image" src="@item.PictureUrl">
|
||||
</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-xs-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-xs-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</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-1">$ @item.UnitPrice.ToString("N2")</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-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section>
|
||||
</article>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="esh-orders_detail-section esh-orders_detail-section--right">
|
||||
<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-xs-2">TOTAL</section>
|
||||
<section class="esh-orders_detail-title col-9"></section>
|
||||
<section class="esh-orders_detail-title col-2">TOTAL</section>
|
||||
</article>
|
||||
|
||||
<article class="esh-orders_detail-items row">
|
||||
<section class="esh-orders_detail-item col-xs-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 col-9"></section>
|
||||
<section class="esh-orders_detail-item esh-orders_detail-item--mark col-2">$ @Model.Total</section>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -14,25 +14,25 @@ new Header() { Controller = "OrderManagement", Text = "Orders Management" } })
|
||||
|
||||
<div class="container">
|
||||
<article class="esh-orders-titles row">
|
||||
<section class="esh-orders-title col-xs-2">Order number</section>
|
||||
<section class="esh-orders-title col-xs-4">Date</section>
|
||||
<section class="esh-orders-title col-xs-2">Total</section>
|
||||
<section class="esh-orders-title col-xs-2">Status</section>
|
||||
<section class="esh-orders-title col-xs-2"></section>
|
||||
<section class="esh-orders-title col-2">Order number</section>
|
||||
<section class="esh-orders-title col-4">Date</section>
|
||||
<section class="esh-orders-title col-2">Total</section>
|
||||
<section class="esh-orders-title col-2">Status</section>
|
||||
<section class="esh-orders-title col-2"></section>
|
||||
</article>
|
||||
@if (Model != null && Model.Any())
|
||||
{
|
||||
foreach (var item in Model)
|
||||
{
|
||||
<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-xs-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-xs-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
||||
<section class="esh-orders-item col-xs-1">
|
||||
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
|
||||
<section class="esh-orders-item col-4">@Html.DisplayFor(modelItem => item.Date)</section>
|
||||
<section class="esh-orders-item col-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
|
||||
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
||||
<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>
|
||||
</section>
|
||||
<section class="esh-orders-item col-xs-1">
|
||||
<section class="esh-orders-item col-1">
|
||||
@if (item.Status.ToLower() == "submitted")
|
||||
{
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@for (int i = 0; i < Model.OrderItems.Count; i++)
|
||||
@ -15,32 +15,32 @@
|
||||
<img class="esh-orders_new-image" src="@item.PictureUrl">
|
||||
<input type="hidden" value="@item.PictureUrl" name=@("orderitems[" + i + "].PictureUrl") />
|
||||
</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
|
||||
<input type="hidden" value="@item.ProductName" name=@("orderitems[" + i + "].ProductName") />
|
||||
</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")
|
||||
<input type="hidden" value="@item.UnitPrice" name=@("orderitems[" + i + "].UnitPrice") />
|
||||
</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
|
||||
<input type="hidden" value="@item.Units" name=@("orderitems[" + i + "].Units") />
|
||||
</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>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="esh-orders_new-section esh-orders_new-section--right">
|
||||
<article class="esh-orders_new-titles row">
|
||||
<section class="esh-orders_new-title col-xs-9"></section>
|
||||
<section class="esh-orders_new-title col-xs-2">Total</section>
|
||||
<section class="esh-orders_new-title col-9"></section>
|
||||
<section class="esh-orders_new-title col-2">Total</section>
|
||||
</article>
|
||||
|
||||
<article class="esh-orders_new-items row">
|
||||
<section class="esh-orders_new-item col-xs-9"></section>
|
||||
<section class="esh-orders_new-item esh-orders_new-item--mark col-xs-2">
|
||||
<section class="esh-orders_new-item col-9"></section>
|
||||
<section class="esh-orders_new-item esh-orders_new-item--mark col-2">
|
||||
$ @Model.Total.ToString("N2")
|
||||
<input type="hidden" value="@Model.Total" name="Total"/>
|
||||
</section>
|
||||
|
@ -12,21 +12,21 @@
|
||||
|
||||
<div class="container">
|
||||
<article class="esh-orders-titles row">
|
||||
<section class="esh-orders-title col-xs-2">Order number</section>
|
||||
<section class="esh-orders-title col-xs-4">Date</section>
|
||||
<section class="esh-orders-title col-xs-2">Total</section>
|
||||
<section class="esh-orders-title col-xs-2">Status</section>
|
||||
<section class="esh-orders-title col-xs-2"></section>
|
||||
<section class="esh-orders-title col-2">Order number</section>
|
||||
<section class="esh-orders-title col-4">Date</section>
|
||||
<section class="esh-orders-title col-2">Total</section>
|
||||
<section class="esh-orders-title col-2">Status</section>
|
||||
<section class="esh-orders-title col-2"></section>
|
||||
</article>
|
||||
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<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-xs-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-xs-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
||||
<section class="esh-orders-item col-xs-2">
|
||||
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
|
||||
<section class="esh-orders-item col-4">@Html.DisplayFor(modelItem => item.Date)</section>
|
||||
<section class="esh-orders-item col-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
|
||||
<section class="esh-orders-item col-2">@Html.DisplayFor(modelItem => item.Status)</section>
|
||||
<section class="esh-orders-item col-2">
|
||||
<form asp-action="OrderProcess" id="orderForm+@item.OrderNumber" method="post">
|
||||
<input type="hidden" name="orderId" value="@item.OrderNumber" />
|
||||
<select name="actionCode" asp-items="@item.ActionCodeSelectList"
|
||||
|
@ -23,34 +23,29 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<section class="esh-basket-title col-xs-3">Product</section>
|
||||
<section class="esh-basket-title col-xs-3 hidden-lg-down"></section>
|
||||
<section class="esh-basket-title col-xs-2">Price</section>
|
||||
<section class="esh-basket-title col-xs-2">Quantity</section>
|
||||
<section class="esh-basket-title col-xs-2">Cost</section>
|
||||
<section class="esh-basket-title col-3">Product</section>
|
||||
<section class="esh-basket-title col-3 hidden-lg-down"></section>
|
||||
<section class="esh-basket-title col-2">Price</section>
|
||||
<section class="esh-basket-title col-2">Quantity</section>
|
||||
<section class="esh-basket-title col-2">Cost</section>
|
||||
</article>
|
||||
|
||||
@for (int i = 0; i < Model.Items.Count; i++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
|
||||
<article class="esh-basket-items row">
|
||||
<div>
|
||||
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
||||
<img class="esh-basket-image" src="@item.PictureUrl" />
|
||||
</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-xs-2">$ @item.UnitPrice.ToString("N2")</section>
|
||||
<section class="esh-basket-item esh-basket-item--middle col-xs-2">
|
||||
<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" />
|
||||
</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>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
</article>
|
||||
<article class="esh-basket-items row">
|
||||
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
|
||||
<img class="esh-basket-image" src="@item.PictureUrl" />
|
||||
</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-2">$ @item.UnitPrice.ToString("N2")</section>
|
||||
<section class="esh-basket-item esh-basket-item--middle col-2">
|
||||
<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" />
|
||||
</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>
|
||||
</article>
|
||||
|
||||
<div class="esh-basket-items--border row">
|
||||
@if (item.OldUnitPrice != 0)
|
||||
@ -63,21 +58,21 @@
|
||||
|
||||
<div class="container">
|
||||
<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-xs-2">Total</section>
|
||||
<section class="esh-basket-title col-10"></section>
|
||||
<section class="esh-basket-title col-2">Total</section>
|
||||
</article>
|
||||
|
||||
<article class="esh-basket-items row">
|
||||
<section class="esh-basket-item col-xs-10"></section>
|
||||
<section class="esh-basket-item esh-basket-item--mark col-xs-2">$ @Model.Total()</section>
|
||||
<section class="esh-basket-item col-10"></section>
|
||||
<section class="esh-basket-item esh-basket-item--mark col-2">$ @Model.Total()</section>
|
||||
</article>
|
||||
|
||||
<article class="esh-basket-items row">
|
||||
<section class="esh-basket-item col-xs-7"></section>
|
||||
<section class="esh-basket-item col-xs-2">
|
||||
<section class="esh-basket-item col-7"></section>
|
||||
<section class="esh-basket-item col-2">
|
||||
<button class="btn esh-basket-checkout" name="name" value="" type="submit">[ Update ]</button>
|
||||
</section>
|
||||
<section class="esh-basket-item col-xs-3">
|
||||
<section class="esh-basket-item col-3">
|
||||
<input type="submit"
|
||||
class="btn esh-basket-checkout"
|
||||
value="[ Checkout ]" name="action" />
|
||||
|
@ -32,11 +32,11 @@
|
||||
</environment>
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar navbar-light navbar-static-top">
|
||||
<header class="esh-app-header">
|
||||
<div class="container">
|
||||
<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 asp-area="" asp-controller="Catalog" asp-action="Index">
|
||||
<img src="~/images/brand.png" />
|
||||
@ -96,24 +96,23 @@
|
||||
if ('@User.Identity.IsAuthenticated' === 'True') {
|
||||
var timerId;
|
||||
|
||||
let connection = stablishConnection();
|
||||
|
||||
connection.start().then(function () {
|
||||
console.log('User Registered to Signalr Hub');
|
||||
});
|
||||
|
||||
registerNotificationHandlers(connection);
|
||||
stablishConnection((conn) => registerNotificationHandlers(conn));
|
||||
}
|
||||
|
||||
function stablishConnection() {
|
||||
return new signalR.HubConnectionBuilder()
|
||||
function stablishConnection(cb) {
|
||||
let connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl('@settings.Value.SignalrHubUrl/hub/notificationhub', {
|
||||
transport: signalR.HttpTransportType.LongPolling,
|
||||
accessTokenFactory: () => {
|
||||
return "Authorization", getToken();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
.build();
|
||||
|
||||
connection.start().then(function () {
|
||||
console.log('User Registered to Signalr Hub');
|
||||
cb(connection);
|
||||
});
|
||||
}
|
||||
|
||||
function registerNotificationHandlers(connection) {
|
||||
|
@ -30,6 +30,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Redis" Version="0.3.3" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Fabric.MSBuild" Version="1.6.5" />
|
||||
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="1.0.163" />
|
||||
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.5.180" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||
|
@ -8,28 +8,14 @@
|
||||
},
|
||||
{
|
||||
"provider": "unpkg",
|
||||
"library": "bootstrap@3.3.7",
|
||||
"library": "bootstrap@4.1.3",
|
||||
"files": [
|
||||
"dist/css/bootstrap.css",
|
||||
"dist/css/bootstrap.css.map",
|
||||
"dist/css/bootstrap.min.css",
|
||||
"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.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"
|
||||
"dist/js/bootstrap.min.js"
|
||||
],
|
||||
"destination": "wwwroot/lib/bootstrap/"
|
||||
},
|
||||
|
@ -12,3 +12,7 @@
|
||||
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%;
|
||||
}
|
||||
|
||||
.esh-campaigns-card-footer-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.esh-campaigns-form-button {
|
||||
background-color: #83D01B;
|
||||
border: none;
|
||||
@ -81,6 +87,7 @@
|
||||
font-size: 1rem;
|
||||
transition: all 0.35s;
|
||||
width: 80%;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.esh-campaigns-button.is-disabled {
|
||||
|
@ -71,6 +71,7 @@
|
||||
margin-top: -1.5rem;
|
||||
padding: 0.5rem;
|
||||
transition: all 0.35s;
|
||||
margin-bottom: -6px;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: $filter-padding;
|
||||
transition: all $animation-speed-default;
|
||||
margin-bottom: -6px;
|
||||
|
||||
&:hover {
|
||||
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 */
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "~ngx-toastr/toastr-bs4-alert.scss";
|
||||
|
||||
@import './modules/variables';
|
||||
|
||||
$dist: './fonts/Montserrat-Regular';
|
||||
|
@ -1,18 +1,17 @@
|
||||
<header class="navbar navbar-light navbar-static-top">
|
||||
<header class="esh-app-header">
|
||||
<div class="container">
|
||||
<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">
|
||||
<img src="assets/images/brand.png" />
|
||||
</a>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section class="col-lg-1 col-xs-12">
|
||||
<section class="col-lg-1 col-12">
|
||||
<esh-basket-status *ngIf="Authenticated"></esh-basket-status>
|
||||
</section>
|
||||
|
||||
@ -32,7 +31,7 @@
|
||||
</section>
|
||||
|
||||
<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>
|
||||
|
||||
</article>
|
||||
|
@ -11,6 +11,7 @@
|
||||
padding-bottom: $padding;
|
||||
padding-top: $padding;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
|
||||
$height: 50px;
|
||||
|
||||
@ -18,6 +19,9 @@
|
||||
height: $height;
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
margin: 15px;
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,16 @@
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Component, ViewEncapsulation, OnInit, ViewContainerRef } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Component, OnInit, ViewContainerRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { DataService } from './shared/services/data.service';
|
||||
import { SecurityService } from './shared/services/security.service';
|
||||
import { ConfigurationService } from './shared/services/configuration.service';
|
||||
import { SignalrService } from './shared/services/signalr.service';
|
||||
import { ToastsManager } from 'ng2-toastr';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
/*
|
||||
* App Component
|
||||
* Top Level Component
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'esh-app',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
@ -27,10 +24,11 @@ export class AppComponent implements OnInit {
|
||||
private securityService: SecurityService,
|
||||
private configurationService: ConfigurationService,
|
||||
private signalrService: SignalrService,
|
||||
private toastr: ToastsManager,
|
||||
private toastr: ToastrService,
|
||||
vcr: ViewContainerRef
|
||||
) {
|
||||
this.toastr.setRootViewContainerRef(vcr);
|
||||
// TODO: Set Taster Root (Overlay) container
|
||||
//this.toastr.setRootViewContainerRef(vcr);
|
||||
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 { BrowserModule } from '@angular/platform-browser';
|
||||
// import { FormsModule } from '@angular/forms';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { HttpClientModule } from "@angular/common/http";
|
||||
|
||||
import { routing } from './app.routes';
|
||||
import { routing } from './app.routes';
|
||||
import { AppService } from './app.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { CatalogModule } from './catalog/catalog.module';
|
||||
import { OrdersModule } from './orders/orders.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { CatalogModule } from './catalog/catalog.module';
|
||||
import { OrdersModule } from './orders/orders.module';
|
||||
import { BasketModule } from './basket/basket.module';
|
||||
import { CampaignsModule } from './campaigns/campaigns.module';
|
||||
import { ToastModule } from 'ng2-toastr/ng2-toastr';
|
||||
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
ToastModule.forRoot(),
|
||||
ToastrModule.forRoot(),
|
||||
routing,
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
// Only module that app module loads
|
||||
SharedModule.forRoot(),
|
||||
CatalogModule,
|
||||
OrdersModule,
|
||||
BasketModule,
|
||||
CampaignsModule
|
||||
CampaignsModule
|
||||
],
|
||||
providers: [
|
||||
AppService
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { BasketService } from '../basket.service';
|
||||
import { BasketWrapperService } from '../../shared/services/basket.wrapper.service';
|
||||
|
@ -9,11 +9,11 @@
|
||||
</div>
|
||||
|
||||
<article class="esh-basket-titles row">
|
||||
<section class="esh-basket-title col-xs-3">Product</section>
|
||||
<section class="esh-basket-title col-xs-3 hidden-lg-down"></section>
|
||||
<section class="esh-basket-title col-xs-2">Price</section>
|
||||
<section class="esh-basket-title col-xs-2">Quantity</section>
|
||||
<section class="esh-basket-title col-xs-2">Cost</section>
|
||||
<section class="esh-basket-title col-3">Product</section>
|
||||
<section class="esh-basket-title col-3 hidden-lg-down"></section>
|
||||
<section class="esh-basket-title col-2">Price</section>
|
||||
<section class="esh-basket-title col-2">Quantity</section>
|
||||
<section class="esh-basket-title col-2">Cost</section>
|
||||
</article>
|
||||
|
||||
<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">
|
||||
<img class="esh-basket-image" src="{{item.pictureUrl}}"/>
|
||||
</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-xs-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-3">{{item.productName}}</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-2">
|
||||
<input id="quantity"
|
||||
class="esh-basket-input"
|
||||
type="number"
|
||||
@ -32,7 +32,7 @@
|
||||
[(ngModel)]="item.quantity"
|
||||
(change)="itemQuantityChanged(item)"/>
|
||||
</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>
|
||||
<br/>
|
||||
<div class="esh-basket-items-margin-left1 row">
|
||||
@ -43,21 +43,21 @@
|
||||
|
||||
<div class="container">
|
||||
<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-xs-2">Total</section>
|
||||
<section class="esh-basket-title col-9"></section>
|
||||
<section class="esh-basket-title col-2">Total</section>
|
||||
</article>
|
||||
|
||||
<article class="esh-basket-items row">
|
||||
<section class="esh-basket-item col-xs-9"></section>
|
||||
<section class="esh-basket-item esh-basket-item--mark col-xs-2">$ {{totalPrice | number:'.2-2'}}</section>
|
||||
<section class="esh-basket-item col-9"></section>
|
||||
<section class="esh-basket-item esh-basket-item--mark col-2">$ {{totalPrice | number:'.2-2'}}</section>
|
||||
</article>
|
||||
|
||||
<article class="esh-basket-items row">
|
||||
<section class="esh-basket-item col-xs-7"></section>
|
||||
<section class="esh-basket-item col-xs-2">
|
||||
<section class="esh-basket-item col-7"></section>
|
||||
<section class="esh-basket-item col-2">
|
||||
<button class="btn esh-basket-checkout" (click)="update($event)">[ Update ]</button>
|
||||
</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>
|
||||
</section>
|
||||
</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