diff --git a/k8s/deploy-ingress-azure.ps1 b/k8s/deploy-ingress-azure.ps1 index f93cf437b..d0ff702df 100644 --- a/k8s/deploy-ingress-azure.ps1 +++ b/k8s/deploy-ingress-azure.ps1 @@ -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 \ No newline at end of file +kubectl apply -f nginx-ingress\cloud-generic.yaml \ No newline at end of file diff --git a/k8s/deploy-ingress-dockerlocal.ps1 b/k8s/deploy-ingress-dockerlocal.ps1 new file mode 100644 index 000000000..04ffad763 --- /dev/null +++ b/k8s/deploy-ingress-dockerlocal.ps1 @@ -0,0 +1,2 @@ +kubectl apply -f nginx-ingress\cm.yaml +kubectl apply -f nginx-ingress\cloud-generic.yaml \ No newline at end of file diff --git a/k8s/deploy-ingress.ps1 b/k8s/deploy-ingress.ps1 index 694361bfa..37abcbee2 100644 --- a/k8s/deploy-ingress.ps1 +++ b/k8s/deploy-ingress.ps1 @@ -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 diff --git a/k8s/deploy.ps1 b/k8s/deploy.ps1 index f0905096a..abeb12aed 100644 --- a/k8s/deploy.ps1 +++ b/k8s/deploy.ps1 @@ -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 diff --git a/k8s/helm-rbac.yaml b/k8s/helm-rbac.yaml new file mode 100644 index 000000000..b6180329a --- /dev/null +++ b/k8s/helm-rbac.yaml @@ -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 \ No newline at end of file diff --git a/k8s/helm/deploy-all.ps1 b/k8s/helm/deploy-all.ps1 index 1239cc7af..08313cdbb 100644 --- a/k8s/helm/deploy-all.ps1 +++ b/k8s/helm/deploy-all.ps1 @@ -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 } } } diff --git a/k8s/helm/ingress_values_dockerk8s.yaml b/k8s/helm/ingress_values_dockerk8s.yaml new file mode 100644 index 000000000..75597aac9 --- /dev/null +++ b/k8s/helm/ingress_values_dockerk8s.yaml @@ -0,0 +1,5 @@ +ingress: + annotations: + kubernetes.io/ingress.class: "nginx" + ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/ssl-redirect: "false" diff --git a/k8s/nginx-ingress/azure/service.yaml b/k8s/nginx-ingress/azure/service.yaml deleted file mode 100644 index 8d2f71505..000000000 --- a/k8s/nginx-ingress/azure/service.yaml +++ /dev/null @@ -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 diff --git a/k8s/nginx-ingress/cloud-generic.yaml b/k8s/nginx-ingress/cloud-generic.yaml new file mode 100644 index 000000000..945441ab8 --- /dev/null +++ b/k8s/nginx-ingress/cloud-generic.yaml @@ -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 \ No newline at end of file diff --git a/k8s/nginx-ingress/cm.yaml b/k8s/nginx-ingress/cm.yaml new file mode 100644 index 000000000..7818fd15b Binary files /dev/null and b/k8s/nginx-ingress/cm.yaml differ diff --git a/k8s/nginx-ingress/configmap.yaml b/k8s/nginx-ingress/configmap.yaml deleted file mode 100644 index 6703fc38e..000000000 --- a/k8s/nginx-ingress/configmap.yaml +++ /dev/null @@ -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" diff --git a/k8s/nginx-ingress/default-backend.yaml b/k8s/nginx-ingress/default-backend.yaml deleted file mode 100644 index 64f6f58ad..000000000 --- a/k8s/nginx-ingress/default-backend.yaml +++ /dev/null @@ -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 diff --git a/k8s/nginx-ingress/local-dockerk8s/identityapi-cm-fix.yaml b/k8s/nginx-ingress/local-dockerk8s/identityapi-cm-fix.yaml new file mode 100644 index 000000000..3a3fcf5a5 --- /dev/null +++ b/k8s/nginx-ingress/local-dockerk8s/identityapi-cm-fix.yaml @@ -0,0 +1,3 @@ +data: + mvc_e: http://10.0.75.1/webmvc + \ No newline at end of file diff --git a/k8s/nginx-ingress/local-dockerk8s/mvc-cm-fix.yaml b/k8s/nginx-ingress/local-dockerk8s/mvc-cm-fix.yaml new file mode 100644 index 000000000..1475deec1 --- /dev/null +++ b/k8s/nginx-ingress/local-dockerk8s/mvc-cm-fix.yaml @@ -0,0 +1,3 @@ +data: + urls__IdentityUrl: http://10.0.75.1/identity + urls__mvc: http://10.0.75.1/webmvc diff --git a/k8s/nginx-ingress/local-dockerk8s/mvc-fix.yaml b/k8s/nginx-ingress/local-dockerk8s/mvc-fix.yaml new file mode 100644 index 000000000..b9ecd4cba --- /dev/null +++ b/k8s/nginx-ingress/local-dockerk8s/mvc-fix.yaml @@ -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 \ No newline at end of file diff --git a/k8s/nginx-ingress/mandatory.yaml b/k8s/nginx-ingress/mandatory.yaml new file mode 100644 index 000000000..56b1cc3b5 --- /dev/null +++ b/k8s/nginx-ingress/mandatory.yaml @@ -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 "-" + # Here: "-" + # 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 diff --git a/k8s/nginx-ingress/namespace.yaml b/k8s/nginx-ingress/namespace.yaml deleted file mode 100644 index 6878f0be8..000000000 --- a/k8s/nginx-ingress/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: ingress-nginx diff --git a/k8s/nginx-ingress/patch-service-without-rbac.yaml b/k8s/nginx-ingress/patch-service-without-rbac.yaml deleted file mode 100644 index 919efc389..000000000 --- a/k8s/nginx-ingress/patch-service-without-rbac.yaml +++ /dev/null @@ -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 diff --git a/k8s/nginx-ingress/publish-service-patch.yaml b/k8s/nginx-ingress/publish-service-patch.yaml deleted file mode 100644 index f8f52f772..000000000 --- a/k8s/nginx-ingress/publish-service-patch.yaml +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - 'op': 'add', - 'path': '/spec/template/spec/containers/0/args/-', - 'value': '--publish-service=$(POD_NAMESPACE)/ingress-nginx' - } -] diff --git a/k8s/nginx-ingress/service-nodeport.yaml b/k8s/nginx-ingress/service-nodeport.yaml new file mode 100644 index 000000000..dd82ed3ed --- /dev/null +++ b/k8s/nginx-ingress/service-nodeport.yaml @@ -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 diff --git a/k8s/nginx-ingress/tcp-services-configmap.yaml b/k8s/nginx-ingress/tcp-services-configmap.yaml deleted file mode 100644 index a963085d3..000000000 --- a/k8s/nginx-ingress/tcp-services-configmap.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: ConfigMap -apiVersion: v1 -metadata: - name: tcp-services - namespace: ingress-nginx diff --git a/k8s/nginx-ingress/udp-services-configmap.yaml b/k8s/nginx-ingress/udp-services-configmap.yaml deleted file mode 100644 index 1870931a2..000000000 --- a/k8s/nginx-ingress/udp-services-configmap.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: ConfigMap -apiVersion: v1 -metadata: - name: udp-services - namespace: ingress-nginx diff --git a/k8s/nginx-ingress/without-rbac.yaml b/k8s/nginx-ingress/without-rbac.yaml deleted file mode 100644 index 1c46b73eb..000000000 --- a/k8s/nginx-ingress/without-rbac.yaml +++ /dev/null @@ -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 diff --git a/src/ApiGateways/ApiGw-Base/OcelotApiGw.csproj b/src/ApiGateways/ApiGw-Base/OcelotApiGw.csproj index d3b1a049b..821755d9d 100644 --- a/src/ApiGateways/ApiGw-Base/OcelotApiGw.csproj +++ b/src/ApiGateways/ApiGw-Base/OcelotApiGw.csproj @@ -10,6 +10,6 @@ - + diff --git a/src/ApiGateways/ApiGw-Base/Startup.cs b/src/ApiGateways/ApiGw-Base/Startup.cs index f6a36b59e..da7cd25f4 100644 --- a/src/ApiGateways/ApiGw-Base/Startup.cs +++ b/src/ApiGateways/ApiGw-Base/Startup.cs @@ -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; diff --git a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs index e01a7aaa8..ef09911fe 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events { @@ -10,7 +11,17 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events CreationDate = DateTime.UtcNow; } - public Guid Id { get; } - public DateTime CreationDate { get; } + [JsonConstructor] + public IntegrationEvent(Guid id, DateTime createDate) + { + Id = id; + CreationDate = createDate; + } + + [JsonProperty] + public Guid Id { get; private set; } + + [JsonProperty] + public DateTime CreationDate { get; private set; } } } diff --git a/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs b/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs index 88be8cf96..e789081f3 100644 --- a/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs +++ b/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs @@ -1,10 +1,8 @@ -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus { @@ -37,8 +35,13 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus where TH : IIntegrationEventHandler { var eventName = GetEventKey(); + DoAddSubscription(typeof(TH), eventName, isDynamic: false); - _eventTypes.Add(typeof(T)); + + if (!_eventTypes.Contains(typeof(T))) + { + _eventTypes.Add(typeof(T)); + } } private void DoAddSubscription(Type handlerType, string eventName, bool isDynamic) diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index 49a417635..a3b6437ef 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -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 }); } diff --git a/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs b/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs index 2cd86669b..d16eb4625 100644 --- a/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs +++ b/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs @@ -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 }); } diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/EventStateEnum.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/EventStateEnum.cs index 3efb78e74..079cf7d7e 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/EventStateEnum.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/EventStateEnum.cs @@ -7,7 +7,8 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF public enum EventStateEnum { NotPublished = 0, - Published = 1, - PublishedFailed = 2 + InProgress = 1, + Published = 2, + PublishedFailed = 3 } } diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs index 3cab9e500..e5c3bc9ad 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs @@ -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; + } } } diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs index ed1f74616..6167d8ae8 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs @@ -9,7 +9,10 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Servi { public interface IIntegrationEventLogService { + Task> RetrieveEventLogsPendingToPublishAsync(); Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction); - Task MarkEventAsPublishedAsync(IntegrationEvent @event); + Task MarkEventAsPublishedAsync(Guid eventId); + Task MarkEventAsInProgressAsync(Guid eventId); + Task MarkEventAsFailedAsync(Guid eventId); } } diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs index a12309482..2712c5e1c 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs @@ -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 _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> 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) + { + return UpdateEventStatus(eventId, EventStateEnum.Published); + } + + public Task MarkEventAsInProgressAsync(Guid eventId) { - var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == @event.Id); - eventLogEntry.TimesSent++; - eventLogEntry.State = EventStateEnum.Published; + 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); diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs index 1b82251e3..8c550bf27 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs @@ -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) diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 408f870af..5f0080c44 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -232,7 +232,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) { services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); + sp => (DbConnection c) => new IntegrationEventLogService(c)); services.AddTransient(); diff --git a/src/Services/Identity/Identity.API/Controllers/AccountController.cs b/src/Services/Identity/Identity.API/Controllers/AccountController.cs index 79e9c247e..7a1fea312 100644 --- a/src/Services/Identity/Identity.API/Controllers/AccountController.cs +++ b/src/Services/Identity/Identity.API/Controllers/AccountController.cs @@ -1,4 +1,9 @@ -using IdentityModel; +using System; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using IdentityModel; using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Services; @@ -11,11 +16,6 @@ using Microsoft.eShopOnContainers.Services.Identity.API.Models; using Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels; using Microsoft.eShopOnContainers.Services.Identity.API.Services; using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers { @@ -79,9 +79,16 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers if (ModelState.IsValid) { var user = await _loginService.FindByUsername(model.Email); + if (await _loginService.ValidateCredentials(user, model.Password)) { - AuthenticationProperties props = null; + var props = new AuthenticationProperties + { + ExpiresUtc = DateTimeOffset.UtcNow.AddHours(2), + AllowRefresh = true, + RedirectUri = model.ReturnUrl + }; + if (model.RememberMe) { props = new AuthenticationProperties @@ -91,8 +98,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers }; }; - await _loginService.SignIn(user); - + await _loginService.SignInAsync(user, props); + // make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint if (_interaction.IsValidReturnUrl(model.ReturnUrl)) { @@ -113,7 +120,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers return View(vm); } - async Task BuildLoginViewModelAsync(string returnUrl, AuthorizationRequest context) + private async Task BuildLoginViewModelAsync(string returnUrl, AuthorizationRequest context) { var allowLocal = true; if (context?.ClientId != null) @@ -132,7 +139,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers }; } - async Task BuildLoginViewModelAsync(LoginViewModel model) + private async Task BuildLoginViewModelAsync(LoginViewModel model) { var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); var vm = await BuildLoginViewModelAsync(model.ReturnUrl, context); @@ -193,7 +200,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers try { - + // hack: try/catch to handle social providers that throw await HttpContext.SignOutAsync(idp, new AuthenticationProperties { @@ -209,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()); diff --git a/src/Services/Identity/Identity.API/Services/EFLoginService.cs b/src/Services/Identity/Identity.API/Services/EFLoginService.cs index 63c4d4b7e..f3a9d5a03 100644 --- a/src/Services/Identity/Identity.API/Services/EFLoginService.cs +++ b/src/Services/Identity/Identity.API/Services/EFLoginService.cs @@ -1,15 +1,17 @@ -using Microsoft.AspNetCore.Identity; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; using Microsoft.eShopOnContainers.Services.Identity.API.Models; -using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Identity.API.Services { public class EFLoginService : ILoginService { - UserManager _userManager; - SignInManager _signInManager; + private UserManager _userManager; + private SignInManager _signInManager; - public EFLoginService(UserManager userManager, SignInManager signInManager) { + public EFLoginService(UserManager userManager, SignInManager signInManager) + { _userManager = userManager; _signInManager = signInManager; } @@ -24,8 +26,14 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Services return await _userManager.CheckPasswordAsync(user, password); } - public Task SignIn(ApplicationUser user) { + public Task SignIn(ApplicationUser user) + { return _signInManager.SignInAsync(user, true); } + + public Task SignInAsync(ApplicationUser user, AuthenticationProperties properties, string authenticationMethod = null) + { + return _signInManager.SignInAsync(user, properties, authenticationMethod); + } } } diff --git a/src/Services/Identity/Identity.API/Services/ILoginService.cs b/src/Services/Identity/Identity.API/Services/ILoginService.cs index 7bff7f272..8a977205b 100644 --- a/src/Services/Identity/Identity.API/Services/ILoginService.cs +++ b/src/Services/Identity/Identity.API/Services/ILoginService.cs @@ -1,11 +1,16 @@ using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; namespace Microsoft.eShopOnContainers.Services.Identity.API.Services { public interface ILoginService { Task ValidateCredentials(T user, string password); + Task FindByUsername(string user); + Task SignIn(T user); + + Task SignInAsync(T user, AuthenticationProperties properties, string authenticationMethod = null); } } diff --git a/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs b/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs new file mode 100644 index 000000000..6f9aed9e5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs @@ -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 : IPipelineBehavior + { + private readonly ILogger> _logger; + private readonly OrderingContext _dbContext; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public TransactionBehaviour(OrderingContext dbContext, + IOrderingIntegrationEventService orderingIntegrationEventService, + ILogger> 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 Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate 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; + } + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs index e5f154c0c..9a3035d5c 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs @@ -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 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 diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommand.cs new file mode 100644 index 000000000..2007b95c6 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommand.cs @@ -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 + { + + [DataMember] + public int OrderNumber { get; private set; } + + public SetAwaitingValidationOrderStatusCommand(int orderNumber) + { + OrderNumber = orderNumber; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs new file mode 100644 index 000000000..cee307ca2 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs @@ -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 + { + private readonly IOrderRepository _orderRepository; + + public SetAwaitingValidationOrderStatusCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// graceperiod has finished + /// + /// + /// + public async Task 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 + { + public SetAwaitingValidationIdentifiedOrderStatusCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommand.cs new file mode 100644 index 000000000..12bab9ac5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommand.cs @@ -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 + { + + [DataMember] + public int OrderNumber { get; private set; } + + public SetPaidOrderStatusCommand(int orderNumber) + { + OrderNumber = orderNumber; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs new file mode 100644 index 000000000..211e568cb --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs @@ -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 + { + private readonly IOrderRepository _orderRepository; + + public SetPaidOrderStatusCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// Shipment service confirms the payment + /// + /// + /// + public async Task 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 + { + public SetPaidIdentifiedOrderStatusCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommand.cs new file mode 100644 index 000000000..74f002e21 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommand.cs @@ -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 + { + + [DataMember] + public int OrderNumber { get; private set; } + + public SetStockConfirmedOrderStatusCommand(int orderNumber) + { + OrderNumber = orderNumber; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs new file mode 100644 index 000000000..4e1bc6185 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs @@ -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 + { + private readonly IOrderRepository _orderRepository; + + public SetStockConfirmedOrderStatusCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// Stock service confirms the request + /// + /// + /// + public async Task 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 + { + public SetStockConfirmedOrderStatusIdenfifiedCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommand.cs new file mode 100644 index 000000000..c2293c0aa --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommand.cs @@ -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 + { + + [DataMember] + public int OrderNumber { get; private set; } + + [DataMember] + public List OrderStockItems { get; private set; } + + public SetStockRejectedOrderStatusCommand(int orderNumber, List orderStockItems) + { + OrderNumber = orderNumber; + OrderStockItems = orderStockItems; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs new file mode 100644 index 000000000..2b7a72526 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs @@ -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 + { + private readonly IOrderRepository _orderRepository; + + public SetStockRejectedOrderStatusCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// Stock service rejects the request + /// + /// + /// + public async Task 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 + { + public SetStockRejectedOrderStatusIdenfifiedCommandHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs index f8a7b06e5..32967f6a7 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs @@ -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); } } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs index 60efead1b..e1c54af4f 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs @@ -46,7 +46,7 @@ var orderStatusChangedToAwaitingValidationIntegrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent( order.Id, order.OrderStatus.Name, buyer.Name, orderStockList); - await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToAwaitingValidationIntegrationEvent); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToAwaitingValidationIntegrationEvent); } } } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs index 59c1e4708..d3dca202f 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs @@ -51,7 +51,7 @@ buyer.Name, orderStockList); - await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToPaidIntegrationEvent); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToPaidIntegrationEvent); } } } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs index 0bf4cabcd..3be83a2ae 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs @@ -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); } } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs index 0a8366893..99b2a21a0 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs @@ -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}."); } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs index 910d764cf..e910964e8 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs @@ -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); } } } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs index c51619ff6..f8dcc6edb 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs @@ -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 { - private readonly IOrderRepository _orderRepository; + private readonly IMediator _mediator; - public GracePeriodConfirmedIntegrationEventHandler(IOrderRepository orderRepository) + public GracePeriodConfirmedIntegrationEventHandler(IMediator mediator) { - _orderRepository = orderRepository; + _mediator = mediator; } /// @@ -24,9 +26,8 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling /// 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); } } } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs index 259b7ec34..5f4fc28e1 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs @@ -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 { - 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); } } } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs index 1dbe59e10..6c201d77e 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs @@ -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 { - 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); } } } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs index c08554066..c5561508b 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs @@ -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 { - 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); } } } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs index c70eba187..af7d98f74 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs @@ -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 { - 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); - - orderToUpdate.SetCancelledStatusWhenStockIsRejected(orderStockRejectedItems); + .Select(c => c.ProductId) + .ToList(); - await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + var command = new SetStockRejectedOrderStatusCommand(@event.OrderId, orderStockRejectedItems); + await _mediator.Send(command); } } } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs index f46c5683c..33f327c6b 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs @@ -11,15 +11,13 @@ namespace Ordering.API.Application.IntegrationEvents.EventHandling public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler { 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)); } /// @@ -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, diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs index 373bafaa5..05e8f0e4f 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs @@ -5,6 +5,7 @@ namespace Ordering.API.Application.IntegrationEvents { public interface IOrderingIntegrationEventService { - Task PublishThroughEventBusAsync(IntegrationEvent evt); + Task PublishEventsThroughEventBusAsync(); + Task AddAndSaveEventAsync(IntegrationEvent evt); } } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs index b3c0201b5..9c1bd4e1b 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs @@ -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 _integrationEventLogServiceFactory; private readonly IEventBus _eventBus; private readonly OrderingContext _orderingContext; + private readonly IntegrationEventLogContext _eventLogContext; private readonly IIntegrationEventLogService _eventLogService; - public OrderingIntegrationEventService(IEventBus eventBus, OrderingContext orderingContext, - Func integrationEventLogServiceFactory) + public OrderingIntegrationEventService(IEventBus eventBus, + OrderingContext orderingContext, + IntegrationEventLogContext eventLogContext, + Func 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()); } } } diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs index e720c7b76..99a413f9f 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -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<,>)); + } } } diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 42567641a..f729e6666 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -113,7 +113,7 @@ eventBus.Subscribe>(); eventBus.Subscribe>(); eventBus.Subscribe>(); - eventBus.Subscribe>(); + eventBus.Subscribe>(); } diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 564acdfb4..ac9ec608f 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -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 { get; set; } private readonly IMediator _mediator; + private IDbContextTransaction _currentTransaction; private OrderingContext(DbContextOptions options) : base (options) { } + public IDbContextTransaction GetCurrentTransaction => _currentTransaction; + public OrderingContext(DbContextOptions 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 diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs index 7fe02017b..80a0deb25 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs @@ -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 _orderRepositoryMock; private readonly Mock _identityServiceMock; private readonly Mock _mediator; + private readonly Mock _orderingIntegrationEventService; public NewOrderRequestHandlerTest() { _orderRepositoryMock = new Mock(); _identityServiceMock = new Mock(); + _orderingIntegrationEventService = new Mock(); _mediator = new Mock(); } @@ -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);