@ -1,38 +1,40 @@ | |||||
# This helm values file defines app-based settings | # This helm values file defines app-based settings | ||||
# Charts use those values, so this file **MUST** be included in all chart releases | # Charts use those values, so this file **MUST** be included in all chart releases | ||||
app: # app global settings | |||||
name: "my-eshop" # Override for custom app name | |||||
ingress: # ingress related settings | |||||
app: # app global settings | |||||
name: "my-eshop" # Override for custom app name | |||||
ingress: # ingress related settings | |||||
entries: | entries: | ||||
basket: basket-api # ingress entry for basket api | |||||
catalog: catalog-api # ingress entry for catalog api | |||||
ordering: ordering-api # ingress entry for ordering api | |||||
identity: identity # ingress entry for identity api | |||||
mvc: webmvc # ingress entry for web mvc | |||||
spa: "" # ingress entry for web spa | |||||
status: webstatus # ingress entry for web status | |||||
webshoppingapigw: webshoppingapigw # ingress entry for web shopping Agw | |||||
mobileshoppingapigw: mobileshoppingapigw # ingress entry for mobile shopping Agw | |||||
webshoppingagg: webshoppingagg # ingress entry for web shopping aggregator | |||||
mobileshoppingagg: mobileshoppingagg # ingress entry for mobile shopping aggregator | |||||
payment: payment-api # ingress entry for payment api | |||||
webhooks: webhooks-api # ingress entry for webhooks api | |||||
webhooksweb: webhooks-web # ingress entry for webhooks web demo client | |||||
basket: basket-api # ingress entry for basket api | |||||
catalog: catalog-api # ingress entry for catalog api | |||||
coupon: coupon-api # ingress entry for coupon api | |||||
ordering: ordering-api # ingress entry for ordering api | |||||
identity: identity # ingress entry for identity api | |||||
mvc: webmvc # ingress entry for web mvc | |||||
spa: "" # ingress entry for web spa | |||||
status: webstatus # ingress entry for web status | |||||
webshoppingapigw: webshoppingapigw # ingress entry for web shopping Agw | |||||
mobileshoppingapigw: mobileshoppingapigw # ingress entry for mobile shopping Agw | |||||
webshoppingagg: webshoppingagg # ingress entry for web shopping aggregator | |||||
mobileshoppingagg: mobileshoppingagg # ingress entry for mobile shopping aggregator | |||||
payment: payment-api # ingress entry for payment api | |||||
webhooks: webhooks-api # ingress entry for webhooks api | |||||
webhooksweb: webhooks-web # ingress entry for webhooks web demo client | |||||
svc: | svc: | ||||
basket: basket-api # service name for basket api | |||||
catalog: catalog-api # service name for catalog api | |||||
ordering: ordering-api # service name for ordering api | |||||
orderingbackgroundtasks: ordering-backgroundtasks # service name for orderingbackgroundtasks | |||||
orderingsignalrhub: ordering-signalrhub # service name for orderingsignalrhub | |||||
identity: identity-api # service name for identity api | |||||
mvc: webmvc # service name for web mvc | |||||
spa: webspa # service name for web spa | |||||
status: webstatus # service name for web status | |||||
webshoppingapigw: webshoppingapigw # service name for web shopping Agw | |||||
mobileshoppingapigw: mobileshoppingapigw # service name for mobile shopping Agw | |||||
webshoppingagg: webshoppingagg # service name for web shopping aggregator | |||||
mobileshoppingagg: mobileshoppingagg # service name for mobile shopping aggregator | |||||
payment: payment-api # service name for payment api | |||||
webhooks: webhooks-api # service name for webhooks api | |||||
webhooksweb: webhooks-client # service name for webhooks web | |||||
basket: basket-api # service name for basket api | |||||
catalog: catalog-api # service name for catalog api | |||||
coupon: coupon-api # service name for coupon api | |||||
ordering: ordering-api # service name for ordering api | |||||
orderingbackgroundtasks: ordering-backgroundtasks # service name for orderingbackgroundtasks | |||||
orderingsignalrhub: ordering-signalrhub # service name for orderingsignalrhub | |||||
identity: identity-api # service name for identity api | |||||
mvc: webmvc # service name for web mvc | |||||
spa: webspa # service name for web spa | |||||
status: webstatus # service name for web status | |||||
webshoppingapigw: webshoppingapigw # service name for web shopping Agw | |||||
mobileshoppingapigw: mobileshoppingapigw # service name for mobile shopping Agw | |||||
webshoppingagg: webshoppingagg # service name for web shopping aggregator | |||||
mobileshoppingagg: mobileshoppingagg # service name for mobile shopping aggregator | |||||
payment: payment-api # service name for payment api | |||||
webhooks: webhooks-api # service name for webhooks api | |||||
webhooksweb: webhooks-client # service name for webhooks web |
@ -0,0 +1,21 @@ | |||||
# Patterns to ignore when building packages. | |||||
# This supports shell glob matching, relative path matching, and | |||||
# negation (prefixed with !). Only one pattern per line. | |||||
.DS_Store | |||||
# Common VCS dirs | |||||
.git/ | |||||
.gitignore | |||||
.bzr/ | |||||
.bzrignore | |||||
.hg/ | |||||
.hgignore | |||||
.svn/ | |||||
# Common backup files | |||||
*.swp | |||||
*.bak | |||||
*.tmp | |||||
*~ | |||||
# Various IDEs | |||||
.project | |||||
.idea/ | |||||
*.tmproj |
@ -0,0 +1,5 @@ | |||||
apiVersion: v1 | |||||
appVersion: "1.0" | |||||
description: A Helm chart for Kubernetes | |||||
name: coupon-api | |||||
version: 0.1.0 |
@ -0,0 +1,9 @@ | |||||
eShop Coupon API installed. | |||||
---------------------------- | |||||
This API is not directly exposed outside cluster. If need to access it use: | |||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "coupon-api.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") | |||||
echo "Visit http://127.0.0.1:8080 to use your application" | |||||
kubectl port-forward $POD_NAME 8080:80 | |||||
@ -0,0 +1,32 @@ | |||||
{{/* vim: set filetype=mustache: */}} | |||||
{{/* | |||||
Expand the name of the chart. | |||||
*/}} | |||||
{{- define "coupon-api.name" -}} | |||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} | |||||
{{- end -}} | |||||
{{/* | |||||
Create a default fully qualified app name. | |||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). | |||||
If release name contains chart name it will be used as a full name. | |||||
*/}} | |||||
{{- define "coupon-api.fullname" -}} | |||||
{{- if .Values.fullnameOverride -}} | |||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} | |||||
{{- else -}} | |||||
{{- $name := default .Chart.Name .Values.nameOverride -}} | |||||
{{- if contains $name .Release.Name -}} | |||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}} | |||||
{{- else -}} | |||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{/* | |||||
Create chart name and version as used by the chart label. | |||||
*/}} | |||||
{{- define "coupon-api.chart" -}} | |||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} | |||||
{{- end -}} |
@ -0,0 +1,60 @@ | |||||
{{- define "suffix-name" -}} | |||||
{{- if .Values.app.name -}} | |||||
{{- .Values.app.name -}} | |||||
{{- else -}} | |||||
{{- .Release.Name -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- define "sql-name" -}} | |||||
{{- if .Values.inf.sql.host -}} | |||||
{{- .Values.inf.sql.host -}} | |||||
{{- else -}} | |||||
{{- printf "%s" "sql-data" -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- define "mongo-name" -}} | |||||
{{- if .Values.inf.mongo.host -}} | |||||
{{- .Values.inf.mongo.host -}} | |||||
{{- else -}} | |||||
{{- printf "%s" "nosql-data" -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- define "url-of" -}} | |||||
{{- $name := first .}} | |||||
{{- $ctx := last .}} | |||||
{{- if eq $name "" -}} | |||||
{{- $ctx.Values.inf.k8s.dns -}} | |||||
{{- else -}} | |||||
{{- printf "%s/%s" $ctx.Values.inf.k8s.dns $name -}} {{/*Value is just <dns>/<name> */}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- define "pathBase" -}} | |||||
{{- if .Values.inf.k8s.suffix -}} | |||||
{{- $suffix := include "suffix-name" . -}} | |||||
{{- printf "%s-%s" .Values.pathBase $suffix -}} | |||||
{{- else -}} | |||||
{{- .Values.pathBase -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- define "fqdn-image" -}} | |||||
{{- if .Values.inf.registry -}} | |||||
{{- printf "%s/%s" .Values.inf.registry.server .Values.image.repository -}} | |||||
{{- else -}} | |||||
{{- .Values.image.repository -}} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- define "protocol" -}} | |||||
{{- if .Values.inf.tls.enabled -}} | |||||
{{- printf "%s" "https" -}} | |||||
{{- else -}} | |||||
{{- printf "%s" "http" -}} | |||||
{{- end -}} | |||||
{{- end -}} |
@ -0,0 +1,21 @@ | |||||
{{- $name := include "coupon-api.fullname" . -}} | |||||
{{- $mongo := include "mongo-name" . -}} | |||||
{{- $webshoppingapigw := include "url-of" (list .Values.app.ingress.entries.webshoppingapigw .) -}} | |||||
{{- $protocol := include "protocol" . -}} | |||||
kind: ConfigMap | |||||
apiVersion: v1 | |||||
metadata: | |||||
name: "cfg-{{ $name }}" | |||||
labels: | |||||
app: {{ template "coupon-api.name" . }} | |||||
chart: {{ template "coupon-api.chart" .}} | |||||
release: {{ .Release.Name }} | |||||
heritage: {{ .Release.Service }} | |||||
data: | |||||
coupon__MongoDatabase: "CouponDb" | |||||
coupon__ConnectionString: mongodb://nosql-data | |||||
urls__IdentityUrl: http://{{ .Values.app.svc.identity }} | |||||
all__EventBusConnection: {{ .Values.inf.eventbus.constr }} | |||||
all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}" | |||||
all__UseAzureServiceBus: "{{ .Values.inf.eventbus.useAzure }}" |
@ -0,0 +1,99 @@ | |||||
{{- $name := include "coupon-api.fullname" . -}} | |||||
{{- $cfgname := printf "%s-%s" "cfg" $name -}} | |||||
apiVersion: apps/v1 | |||||
kind: Deployment | |||||
metadata: | |||||
name: {{ template "coupon-api.fullname" . }} | |||||
labels: | |||||
ufo: {{ $cfgname}} | |||||
app: {{ template "coupon-api.name" . }} | |||||
chart: {{ template "coupon-api.chart" . }} | |||||
release: {{ .Release.Name }} | |||||
heritage: {{ .Release.Service }} | |||||
spec: | |||||
replicas: {{ .Values.replicaCount }} | |||||
selector: | |||||
matchLabels: | |||||
app: {{ template "coupon-api.name" . }} | |||||
release: {{ .Release.Name }} | |||||
template: | |||||
metadata: | |||||
labels: | |||||
app: {{ template "coupon-api.name" . }} | |||||
release: {{ .Release.Name }} | |||||
{{ if .Values.inf.mesh.enabled -}} | |||||
annotations: | |||||
linkerd.io/inject: enabled | |||||
{{- end }} | |||||
spec: | |||||
{{ if .Values.inf.registry -}} | |||||
imagePullSecrets: | |||||
- name: {{ .Values.inf.registry.secretName }} | |||||
{{- end }} | |||||
containers: | |||||
- name: {{ .Chart.Name }} | |||||
{{ if .Values.probes -}} | |||||
{{- if .Values.probes.liveness -}} | |||||
livenessProbe: | |||||
httpGet: | |||||
port: {{ .Values.probes.liveness.port }} | |||||
path: {{ .Values.probes.liveness.path }} | |||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} | |||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- if .Values.probes -}} | |||||
{{- if .Values.probes.readiness }} | |||||
readinessProbe: | |||||
httpGet: | |||||
port: {{ .Values.probes.readiness.port }} | |||||
path: {{ .Values.probes.readiness.path }} | |||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} | |||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }} | |||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} | |||||
{{- end -}} | |||||
{{- end }} | |||||
image: "{{ template "fqdn-image" . }}:{{ .Values.image.tag }}" | |||||
imagePullPolicy: {{ .Values.image.pullPolicy }} | |||||
env: | |||||
- name: PATH_BASE | |||||
value: {{ include "pathBase" . }} | |||||
- name: k8sname | |||||
value: {{ .Values.clusterName }} | |||||
{{- if .Values.env.values -}} | |||||
{{- range .Values.env.values }} | |||||
- name: {{ .name }} | |||||
value: {{ .value | quote }} | |||||
{{- end -}} | |||||
{{- end -}} | |||||
{{- if .Values.env.configmap -}} | |||||
{{- range .Values.env.configmap }} | |||||
- name: {{ .name }} | |||||
valueFrom: | |||||
configMapKeyRef: | |||||
name: {{ $cfgname }} | |||||
key: {{ .key }} | |||||
{{- end -}} | |||||
{{- end }} | |||||
ports: | |||||
- name: http | |||||
containerPort: 80 | |||||
protocol: TCP | |||||
- name: grpc | |||||
containerPort: 81 | |||||
protocol: TCP | |||||
resources: | |||||
{{ toYaml .Values.resources | indent 12 }} | |||||
{{- with .Values.nodeSelector }} | |||||
nodeSelector: | |||||
{{ toYaml . | indent 8 }} | |||||
{{- end }} | |||||
{{- with .Values.affinity }} | |||||
affinity: | |||||
{{ toYaml . | indent 8 }} | |||||
{{- end }} | |||||
{{- with .Values.tolerations }} | |||||
tolerations: | |||||
{{ toYaml . | indent 8 }} | |||||
{{- end }} | |||||
@ -0,0 +1,23 @@ | |||||
apiVersion: v1 | |||||
kind: Service | |||||
metadata: | |||||
name: {{ .Values.app.svc.coupon }} | |||||
labels: | |||||
app: {{ template "coupon-api.name" . }} | |||||
chart: {{ template "coupon-api.chart" . }} | |||||
release: {{ .Release.Name }} | |||||
heritage: {{ .Release.Service }} | |||||
spec: | |||||
type: {{ .Values.service.type }} | |||||
ports: | |||||
- port: {{ .Values.service.port }} | |||||
targetPort: http | |||||
protocol: TCP | |||||
name: http | |||||
- port: {{ .Values.service.grpcPort }} | |||||
targetPort: grpc | |||||
protocol: TCP | |||||
name: grpc | |||||
selector: | |||||
app: {{ template "coupon-api.name" . }} | |||||
release: {{ .Release.Name }} |
@ -0,0 +1,61 @@ | |||||
replicaCount: 1 | |||||
clusterName: eshop-aks | |||||
pathBase: /coupon-api | |||||
image: | |||||
repository: eshop/coupon.api | |||||
tag: latest | |||||
pullPolicy: IfNotPresent | |||||
service: | |||||
type: ClusterIP | |||||
port: 80 | |||||
grpcPort: 81 | |||||
resources: {} | |||||
nodeSelector: {} | |||||
tolerations: [] | |||||
affinity: {} | |||||
# env defines the environment variables that will be declared in the pod | |||||
env: | |||||
urls: | |||||
# configmap declares variables which value is taken from the config map defined in template configmap.yaml (name is name of var and key the key in configmap). | |||||
configmap: | |||||
- name: ConnectionString | |||||
key: coupon__ConnectionString | |||||
- name: ApplicationInsights__InstrumentationKey | |||||
key: all__InstrumentationKey | |||||
- name: EventBusConnection | |||||
key: all__EventBusConnection | |||||
- name: AzureServiceBusEnabled | |||||
key: all__UseAzureServiceBus | |||||
- name: CouponMongoDatabase | |||||
key: coupon__MongoDatabase | |||||
- name: IdentityUrl | |||||
key: urls__IdentityUrl | |||||
# values define environment variables with a fixed value (no configmap involved) (name is name of var, and value is its value) | |||||
values: | |||||
- name: ASPNETCORE_ENVIRONMENT | |||||
value: Development | |||||
- name: OrchestratorType | |||||
value: "K8S" | |||||
- name: PORT | |||||
value: "80" | |||||
- name: GRPC_PORT | |||||
value: "81" | |||||
probes: | |||||
liveness: | |||||
path: /liveness | |||||
initialDelaySeconds: 10 | |||||
periodSeconds: 15 | |||||
port: 80 | |||||
readiness: | |||||
path: /hc | |||||
timeoutSeconds: 5 | |||||
initialDelaySeconds: 90 | |||||
periodSeconds: 60 | |||||
port: 80 |
@ -0,0 +1,30 @@ | |||||
namespace Coupon.API.Controllers; | |||||
[Authorize] | |||||
[ApiController] | |||||
[Route("api/v1/[controller]")] | |||||
public class CouponController : ControllerBase | |||||
{ | |||||
private readonly ICouponRepository _couponRepository; | |||||
public CouponController(ICouponRepository couponRepository) | |||||
{ | |||||
_couponRepository = couponRepository; | |||||
} | |||||
[HttpGet("{code}")] | |||||
[ProducesResponseType(StatusCodes.Status200OK)] | |||||
[ProducesResponseType(StatusCodes.Status400BadRequest)] | |||||
[ProducesResponseType(StatusCodes.Status404NotFound)] | |||||
public async Task<ActionResult<Infrastructure.Models.Coupon>> GetCouponByCodeAsync(string code) | |||||
{ | |||||
var coupon = await _couponRepository.FindCouponByCodeAsync(code); | |||||
if (coupon is null || coupon.Consumed) | |||||
{ | |||||
return NotFound(); | |||||
} | |||||
return coupon; | |||||
} | |||||
} |
@ -0,0 +1,63 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<UserSecretsId>1d5bc948-90f1-4906-a1f8-8edaa1ed9e2e</UserSecretsId> | |||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | |||||
<DockerfileContext>..\..\..\..</DockerfileContext> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\contentFiles\any\netstandard2.1\Core\Compression\Snappy\lib\win\snappy32.dll" /> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\contentFiles\any\netstandard2.1\Core\Compression\Snappy\lib\win\snappy64.dll" /> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\contentFiles\any\netstandard2.1\Core\Compression\Zstandard\lib\win\libzstd.dll" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.0.3" /> | |||||
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="6.0.1" /> | |||||
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="6.0.2" /> | |||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="6.0.2" /> | |||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.4" /> | |||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0" /> | |||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.20.0" /> | |||||
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="2.0.2" /> | |||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.3" /> | |||||
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.23" /> | |||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.0" /> | |||||
<PackageReference Include="MongoDB.Driver.Core" Version="2.13.3" /> | |||||
<PackageReference Include="Polly" Version="7.2.3" /> | |||||
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" /> | |||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" /> | |||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> | |||||
<PackageReference Include="Serilog.Sinks.Http" Version="7.2.0" /> | |||||
<PackageReference Include="Serilog.Sinks.Seq" Version="5.1.1" /> | |||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.0" /> | |||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.16.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> | |||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" /> | |||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" /> | |||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\build\../runtimes/win/native/snappy32.dll" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\build\../runtimes/win/native/snappy64.dll" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\build\../runtimes/win/native/snappy64.dll" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Content Remove="C:\Users\Siarhei_Sialitski\.nuget\packages\mongodb.driver.core\2.13.3\build\../runtimes/win/native/libzstd.dll" /> | |||||
</ItemGroup> | |||||
</Project> |
@ -0,0 +1,15 @@ | |||||
namespace Coupon.API | |||||
{ | |||||
public class CouponSettings | |||||
{ | |||||
public string ConnectionString { get; set; } | |||||
public string CouponMongoDatabase { get; set; } | |||||
public string EventBusConnection { get; set; } | |||||
public bool UseCustomizationData { get; set; } | |||||
public bool AzureStorageEnabled { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,62 @@ | |||||
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. | |||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base | |||||
WORKDIR /app | |||||
EXPOSE 80 | |||||
EXPOSE 443 | |||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build | |||||
WORKDIR /src | |||||
# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles | |||||
# to take advantage of Docker's build cache, to speed up local container builds | |||||
COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWebApps.sln" | |||||
COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" | |||||
COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" | |||||
COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" | |||||
COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" | |||||
COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" | |||||
COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" | |||||
COPY "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj" "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj" | |||||
COPY "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj" "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj" | |||||
COPY "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj" "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj" | |||||
COPY "Services/Basket/Basket.API/Basket.API.csproj" "Services/Basket/Basket.API/Basket.API.csproj" | |||||
COPY "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj" "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj" | |||||
COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" | |||||
COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" | |||||
COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" | |||||
COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" | |||||
COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" | |||||
COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" | |||||
COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" | |||||
COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" | |||||
COPY "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj" "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj" | |||||
COPY "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj" "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj" | |||||
COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" | |||||
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" | |||||
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" | |||||
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" | |||||
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" | |||||
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | |||||
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" | |||||
COPY "Web/WebMVC/WebMVC.csproj" "Web/WebMVC/WebMVC.csproj" | |||||
COPY "Web/WebSPA/WebSPA.csproj" "Web/WebSPA/WebSPA.csproj" | |||||
COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" | |||||
COPY "docker-compose.dcproj" "docker-compose.dcproj" | |||||
COPY "NuGet.config" "NuGet.config" | |||||
RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" | |||||
COPY . . | |||||
WORKDIR "/src/Services/Coupon/Coupon.API" | |||||
FROM build AS publish | |||||
RUN dotnet publish "Coupon.API.csproj" -c Release -o /app/publish | |||||
FROM base AS final | |||||
WORKDIR /app | |||||
COPY --from=publish /app/publish . | |||||
ENTRYPOINT ["dotnet", "Coupon.API.dll"] |
@ -0,0 +1,49 @@ | |||||
using System; | |||||
using Microsoft.Data.SqlClient; | |||||
using Coupon.API.IntegrationEvents.EventHandlers; | |||||
using Coupon.API.IntegrationEvents.Events; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Polly; | |||||
namespace Coupon.API.Extensions | |||||
{ | |||||
public static class IHostBuilderExtensions | |||||
{ | |||||
public static IHost SeedDatabaseStrategy<TContext>(this IHost host, Action<TContext> seeder) | |||||
{ | |||||
using (var scope = host.Services.CreateScope()) | |||||
{ | |||||
var context = scope.ServiceProvider.GetService<TContext>(); | |||||
var policy = Policy.Handle<SqlException>() | |||||
.WaitAndRetry(new TimeSpan[] | |||||
{ | |||||
TimeSpan.FromSeconds(3), | |||||
TimeSpan.FromSeconds(5), | |||||
TimeSpan.FromSeconds(8), | |||||
}); | |||||
policy.Execute(() => | |||||
{ | |||||
seeder.Invoke(context); | |||||
}); | |||||
} | |||||
return host; | |||||
} | |||||
public static IHost SubscribersIntegrationEvents(this IHost host) | |||||
{ | |||||
using (var scope = host.Services.CreateScope()) | |||||
{ | |||||
var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>(); | |||||
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>(); | |||||
} | |||||
return host; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,204 @@ | |||||
namespace Coupon.API.Extensions; | |||||
public static class IServiceCollectionExtensions | |||||
{ | |||||
public static IServiceCollection AddCouponRegister(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.AddTransient<ICouponRepository, CouponRepository>() | |||||
.AddTransient<IServiceBusPersisterConnection, DefaultServiceBusPersisterConnection>(service => | |||||
{ | |||||
var settings = service.GetRequiredService<IOptions<CouponSettings>>().Value; | |||||
var serviceBusConnection = settings.EventBusConnection; | |||||
return new DefaultServiceBusPersisterConnection(serviceBusConnection); | |||||
}) | |||||
.AddTransient<IRabbitMQPersistentConnection, DefaultRabbitMQPersistentConnection>(service => | |||||
{ | |||||
var factory = new ConnectionFactory() | |||||
{ | |||||
HostName = configuration["EventBusConnection"], | |||||
DispatchConsumersAsync = true | |||||
}; | |||||
var retryCount = 5; | |||||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | |||||
{ | |||||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||||
} | |||||
return new DefaultRabbitMQPersistentConnection(factory, service.GetService<ILogger<DefaultRabbitMQPersistentConnection>>(), retryCount); | |||||
}) | |||||
.AddTransient<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>() | |||||
.AddTransient<CouponContext>(); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.AddSwaggerGen(options => | |||||
{ | |||||
options.SwaggerDoc("v1", new OpenApiInfo | |||||
{ | |||||
Title = "eShopOnContainers - Coupon HTTP API", | |||||
Version = "v1", | |||||
Description = "The Coupon Service HTTP API" | |||||
}); | |||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||||
{ | |||||
Type = SecuritySchemeType.OAuth2, | |||||
Flows = new OpenApiOAuthFlows() | |||||
{ | |||||
Implicit = new OpenApiOAuthFlow() | |||||
{ | |||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||||
Scopes = new Dictionary<string, string>() | |||||
{ | |||||
{ "coupon", "Coupon API" } | |||||
} | |||||
} | |||||
} | |||||
}); | |||||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomSettings(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.Configure<CouponSettings>(configuration); | |||||
services.Configure<ApiBehaviorOptions>(options => | |||||
{ | |||||
options.InvalidModelStateResponseFactory = context => | |||||
{ | |||||
var problemDetails = new ValidationProblemDetails(context.ModelState) | |||||
{ | |||||
Instance = context.HttpContext.Request.Path, | |||||
Status = StatusCodes.Status400BadRequest, | |||||
Detail = "Please refer to the errors property for additional details." | |||||
}; | |||||
return new BadRequestObjectResult(problemDetails) | |||||
{ | |||||
ContentTypes = { "application/problem+json", "application/problem+xml" } | |||||
}; | |||||
}; | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
var subscriptionClientName = configuration["SubscriptionClientName"]; | |||||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||||
{ | |||||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp => | |||||
{ | |||||
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>(); | |||||
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>(); | |||||
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>(); | |||||
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | |||||
return new EventBusServiceBus(serviceBusPersisterConnection, logger, eventBusSubcriptionsManager, iLifetimeScope, subscriptionClientName); | |||||
}); | |||||
} | |||||
else | |||||
{ | |||||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => | |||||
{ | |||||
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>(); | |||||
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>(); | |||||
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>(); | |||||
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | |||||
var retryCount = 5; | |||||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | |||||
{ | |||||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||||
} | |||||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); | |||||
}); | |||||
} | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
var accountName = configuration.GetValue<string>("AzureStorageAccountName"); | |||||
var accountKey = configuration.GetValue<string>("AzureStorageAccountKey"); | |||||
var hcBuilder = services.AddHealthChecks(); | |||||
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddMongoDb( | |||||
configuration["ConnectionString"], | |||||
name: "CouponCollection-check", | |||||
tags: new string[] { "couponcollection" }); | |||||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||||
{ | |||||
hcBuilder.AddAzureServiceBusTopic( | |||||
configuration["EventBusConnection"], | |||||
topicName: "eshop_event_bus", | |||||
name: "coupon-servicebus-check", | |||||
tags: new string[] { "servicebus" }); | |||||
} | |||||
else | |||||
{ | |||||
hcBuilder.AddRabbitMQ( | |||||
$"amqp://{configuration["EventBusConnection"]}", | |||||
name: "coupon-rabbitmqbus-check", | |||||
tags: new string[] { "rabbitmqbus" }); | |||||
} | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomPolicies(this IServiceCollection services) | |||||
{ | |||||
services.AddCors(options => | |||||
{ | |||||
options.AddPolicy("CorsPolicy", | |||||
builder => builder.SetIsOriginAllowed((host) => true) | |||||
.AllowAnyMethod() | |||||
.AllowAnyHeader() | |||||
.AllowCredentials()); | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddAppInsights(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.AddApplicationInsightsTelemetry(configuration); | |||||
services.AddApplicationInsightsKubernetesEnricher(); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
return services.AddAuthentication(options => | |||||
{ | |||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
}).AddJwtBearer(options => | |||||
{ | |||||
options.Authority = configuration["IdentityUrl"]; | |||||
options.RequireHttpsMetadata = false; | |||||
options.Audience = "coupon"; | |||||
}).Services; | |||||
} | |||||
public static IServiceCollection AddCustomAuthorization(this IServiceCollection services) => services.AddAuthorization(); | |||||
} |
@ -0,0 +1,35 @@ | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using Microsoft.AspNetCore.Authorization; | |||||
using Microsoft.OpenApi.Models; | |||||
using Swashbuckle.AspNetCore.SwaggerGen; | |||||
namespace Coupon.API.Filters | |||||
{ | |||||
public class AuthorizeCheckOperationFilter : IOperationFilter | |||||
{ | |||||
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |||||
{ | |||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | |||||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any(); | |||||
if (!hasAuthorize) return; | |||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); | |||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); | |||||
var oAuthScheme = new OpenApiSecurityScheme | |||||
{ | |||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } | |||||
}; | |||||
operation.Security = new List<OpenApiSecurityRequirement> | |||||
{ | |||||
new OpenApiSecurityRequirement | |||||
{ | |||||
[oAuthScheme] = new [] { "CouponApi" } | |||||
} | |||||
}; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,16 @@ | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.AspNetCore.Mvc.Filters; | |||||
namespace Coupon.API.Filters | |||||
{ | |||||
public class ValidateModelAttribute : ActionFilterAttribute | |||||
{ | |||||
public override void OnActionExecuting(ActionExecutingContext context) | |||||
{ | |||||
if (!context.ModelState.IsValid) | |||||
{ | |||||
context.Result = new BadRequestObjectResult(context.ModelState); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,33 @@ | |||||
global using Coupon.API.Filters; | |||||
global using Coupon.API.Infrastructure.Repositories; | |||||
global using Coupon.API.Infrastructure.Models; | |||||
global using Autofac.Extensions.DependencyInjection; | |||||
global using Autofac; | |||||
global using Microsoft.AspNetCore.Authorization; | |||||
global using Microsoft.AspNetCore.Hosting; | |||||
global using Microsoft.AspNetCore.Http; | |||||
global using Microsoft.AspNetCore.Builder; | |||||
global using Microsoft.AspNetCore.Mvc.Filters; | |||||
global using Microsoft.AspNetCore.Mvc; | |||||
global using Microsoft.Extensions.Logging; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
global using Microsoft.Extensions.Configuration; | |||||
global using Microsoft.Extensions.DependencyInjection; | |||||
global using Microsoft.Extensions.Hosting; | |||||
global using Microsoft.Extensions.Options; | |||||
global using Polly; | |||||
global using Serilog.Context; | |||||
global using Serilog; | |||||
global using System.Collections.Generic; | |||||
global using System.IO; | |||||
global using System.Linq; | |||||
global using System.Threading.Tasks; | |||||
global using System; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | |||||
global using Microsoft.Extensions.Diagnostics.HealthChecks; | |||||
global using Microsoft.OpenApi.Models; | |||||
global using RabbitMQ.Client; | |||||
global using Microsoft.AspNetCore.Authentication.JwtBearer; |
@ -0,0 +1,52 @@ | |||||
namespace Coupon.API.Infrastructure | |||||
{ | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
using Coupon.API.Infrastructure.Models; | |||||
using Coupon.API.Infrastructure.Repositories; | |||||
public class CouponSeed | |||||
{ | |||||
public async Task SeedAsync(CouponContext context) | |||||
{ | |||||
if (context.Coupons.EstimatedDocumentCount() == 0) | |||||
{ | |||||
var coupons = new List<Coupon> | |||||
{ | |||||
new Coupon | |||||
{ | |||||
Code = "DISC-5", | |||||
Discount = 5 | |||||
}, | |||||
new Coupon | |||||
{ | |||||
Code = "DISC-10", | |||||
Discount = 10 | |||||
}, | |||||
new Coupon | |||||
{ | |||||
Code = "DISC-15", | |||||
Discount = 15 | |||||
}, | |||||
new Coupon | |||||
{ | |||||
Code = "DISC-20", | |||||
Discount = 20 | |||||
}, | |||||
new Coupon | |||||
{ | |||||
Code = "DISC-25", | |||||
Discount = 25 | |||||
}, | |||||
new Coupon | |||||
{ | |||||
Code = "DISC-30", | |||||
Discount = 30 | |||||
} | |||||
}; | |||||
await context.Coupons.InsertManyAsync(coupons); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,20 @@ | |||||
using MongoDB.Bson; | |||||
using MongoDB.Bson.Serialization.Attributes; | |||||
namespace Coupon.API.Infrastructure.Models | |||||
{ | |||||
public class Coupon | |||||
{ | |||||
[BsonIgnoreIfDefault] | |||||
[BsonRepresentation(BsonType.ObjectId)] | |||||
public string Id { get; set; } | |||||
public int Discount { get; set; } | |||||
public string Code { get; set; } | |||||
public bool Consumed { get; set; } | |||||
public int OrderId { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,25 @@ | |||||
namespace Coupon.API.Infrastructure.Repositories | |||||
{ | |||||
using Coupon.API.Infrastructure.Models; | |||||
using Microsoft.Extensions.Options; | |||||
using MongoDB.Driver; | |||||
public class CouponContext | |||||
{ | |||||
private readonly IMongoDatabase _database = null; | |||||
public CouponContext(IOptions<CouponSettings> settings) | |||||
{ | |||||
var client = new MongoClient(settings.Value.ConnectionString); | |||||
if (client is null) | |||||
{ | |||||
throw new MongoConfigurationException("Cannot connect to the database. The connection string is not valid or the database is not accessible"); | |||||
} | |||||
_database = client.GetDatabase(settings.Value.CouponMongoDatabase); | |||||
} | |||||
public IMongoCollection<Coupon> Coupons => _database.GetCollection<Coupon>("CouponCollection"); | |||||
} | |||||
} |
@ -0,0 +1,42 @@ | |||||
namespace Coupon.API.Infrastructure.Repositories | |||||
{ | |||||
using System.Threading.Tasks; | |||||
using Coupon.API.Infrastructure.Models; | |||||
using MongoDB.Driver; | |||||
public class CouponRepository : ICouponRepository | |||||
{ | |||||
private readonly CouponContext _couponContext; | |||||
public CouponRepository(CouponContext couponContext) | |||||
{ | |||||
_couponContext = couponContext; | |||||
} | |||||
public async Task UpdateCouponConsumedByCodeAsync(string code, int orderId) | |||||
{ | |||||
var filter = Builders<Coupon>.Filter.Eq("Code", code); | |||||
var update = Builders<Coupon>.Update | |||||
.Set(coupon => coupon.Consumed, true) | |||||
.Set(coupon => coupon.OrderId, orderId); | |||||
await _couponContext.Coupons.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = false }); | |||||
} | |||||
public async Task UpdateCouponReleasedByOrderIdAsync(int orderId) | |||||
{ | |||||
var filter = Builders<Coupon>.Filter.Eq("OrderId", orderId); | |||||
var update = Builders<Coupon>.Update | |||||
.Set(coupon => coupon.Consumed, false) | |||||
.Set(coupon => coupon.OrderId, 0); | |||||
await _couponContext.Coupons.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = false }); | |||||
} | |||||
public async Task<Coupon> FindCouponByCodeAsync(string code) | |||||
{ | |||||
var filter = Builders<Coupon>.Filter.Eq("Code", code); | |||||
return await _couponContext.Coupons.Find(filter).FirstOrDefaultAsync(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
namespace Coupon.API.Infrastructure.Repositories | |||||
{ | |||||
using System.Threading.Tasks; | |||||
using Coupon.API.Infrastructure.Models; | |||||
public interface ICouponRepository | |||||
{ | |||||
Task<Coupon> FindCouponByCodeAsync(string code); | |||||
Task UpdateCouponConsumedByCodeAsync(string code, int orderId); | |||||
Task UpdateCouponReleasedByOrderIdAsync(int orderId); | |||||
} | |||||
} |
@ -0,0 +1,56 @@ | |||||
using System.Threading.Tasks; | |||||
using Coupon.API.Infrastructure.Repositories; | |||||
using Coupon.API.IntegrationEvents.Events; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using Serilog; | |||||
using Serilog.Context; | |||||
namespace Coupon.API.IntegrationEvents.EventHandlers | |||||
{ | |||||
public class OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent> | |||||
{ | |||||
private readonly ICouponRepository _couponRepository; | |||||
private readonly IEventBus _eventBus; | |||||
public OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler(ICouponRepository couponRepository, IEventBus eventBus) | |||||
{ | |||||
_couponRepository = couponRepository; | |||||
_eventBus = eventBus; | |||||
} | |||||
public async Task Handle(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent @event) | |||||
{ | |||||
await Task.Delay(3000); | |||||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-Coupon.API")) | |||||
{ | |||||
Log.Information("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, "Coupon.API", @event); | |||||
var couponIntegrationEvent = await ProcessIntegrationEventAsync(@event); | |||||
Log.Information("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", couponIntegrationEvent.Id, "Coupon.API", couponIntegrationEvent); | |||||
_eventBus.Publish(couponIntegrationEvent); | |||||
} | |||||
} | |||||
private async Task<IntegrationEvent> ProcessIntegrationEventAsync(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent integrationEvent) | |||||
{ | |||||
var coupon = await _couponRepository.FindCouponByCodeAsync(integrationEvent.Code); | |||||
Log.Information("----- Coupon \"{CouponCode}\": {@Coupon}", integrationEvent.Code, coupon); | |||||
if (coupon == null || coupon.Consumed) | |||||
{ | |||||
return new OrderCouponRejectedIntegrationEvent(integrationEvent.OrderId, coupon.Code); | |||||
} | |||||
Log.Information("Consumed coupon: {DiscountCode}", integrationEvent.Code); | |||||
await _couponRepository.UpdateCouponConsumedByCodeAsync(integrationEvent.Code, integrationEvent.OrderId); | |||||
return new OrderCouponConfirmedIntegrationEvent(integrationEvent.OrderId, coupon.Discount); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,22 @@ | |||||
using System.Threading.Tasks; | |||||
using Coupon.API.Infrastructure.Repositories; | |||||
using Coupon.API.IntegrationEvents.Events; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
namespace Coupon.API.IntegrationEvents.EventHandlers | |||||
{ | |||||
public class OrderStatusChangedToCancelledIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent> | |||||
{ | |||||
private readonly ICouponRepository _couponRepository; | |||||
public OrderStatusChangedToCancelledIntegrationEventHandler(ICouponRepository couponRepository) | |||||
{ | |||||
_couponRepository = couponRepository; | |||||
} | |||||
public async Task Handle(OrderStatusChangedToCancelledIntegrationEvent @event) | |||||
{ | |||||
await _couponRepository.UpdateCouponReleasedByOrderIdAsync(@event.OrderId); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
namespace Coupon.API.IntegrationEvents.Events; | |||||
public record OrderCouponConfirmedIntegrationEvent : IntegrationEvent | |||||
{ | |||||
public int OrderId { get; } | |||||
public int Discount { get; } | |||||
public OrderCouponConfirmedIntegrationEvent(int orderId, int discount) | |||||
{ | |||||
OrderId = orderId; | |||||
Discount = discount; | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
namespace Coupon.API.IntegrationEvents.Events; | |||||
public record OrderCouponRejectedIntegrationEvent : IntegrationEvent | |||||
{ | |||||
public int OrderId { get; } | |||||
public string Code { get; } | |||||
public OrderCouponRejectedIntegrationEvent(int orderId, string code) | |||||
{ | |||||
OrderId = orderId; | |||||
Code = code; | |||||
} | |||||
} |
@ -0,0 +1,18 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Coupon.API.IntegrationEvents.Events; | |||||
public record OrderStatusChangedToAwaitingCouponValidationIntegrationEvent : IntegrationEvent | |||||
{ | |||||
[JsonProperty] | |||||
public int OrderId { get; private set; } | |||||
[JsonProperty] | |||||
public string OrderStatus { get; private set; } | |||||
[JsonProperty] | |||||
public string BuyerName { get; private set; } | |||||
[JsonProperty] | |||||
public string Code { get; private set; } | |||||
} |
@ -0,0 +1,18 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Coupon.API.IntegrationEvents.Events; | |||||
public record OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent | |||||
{ | |||||
[JsonProperty] | |||||
public int OrderId { get; private set; } | |||||
[JsonProperty] | |||||
public string OrderStatus { get; private set; } | |||||
[JsonProperty] | |||||
public string BuyerName { get; private set; } | |||||
[JsonProperty] | |||||
public string DiscountCode { get; private set; } | |||||
} |
@ -0,0 +1,53 @@ | |||||
using System.IO; | |||||
using Autofac.Extensions.DependencyInjection; | |||||
using Coupon.API.Extensions; | |||||
using Coupon.API.Infrastructure; | |||||
using Coupon.API.Infrastructure.Repositories; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.Extensions.Configuration; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Serilog; | |||||
using Serilog.Events; | |||||
namespace Coupon.API | |||||
{ | |||||
public class Program | |||||
{ | |||||
public static void Main(string[] args) => | |||||
CreateHostBuilder(args) | |||||
.Build() | |||||
.SeedDatabaseStrategy<CouponContext>(context => new CouponSeed().SeedAsync(context).Wait()) | |||||
.SubscribersIntegrationEvents() | |||||
.Run(); | |||||
public static IHostBuilder CreateHostBuilder(string[] args) => | |||||
Host.CreateDefaultBuilder(args) | |||||
.UseServiceProviderFactory(new AutofacServiceProviderFactory()) | |||||
.ConfigureAppConfiguration((host, builder) => | |||||
{ | |||||
builder.SetBasePath(Directory.GetCurrentDirectory()) | |||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) | |||||
.AddJsonFile($"appsettings.{host.HostingEnvironment.EnvironmentName}.json", optional: false, reloadOnChange: true) | |||||
.AddEnvironmentVariables(); | |||||
var config = builder.Build(); | |||||
if (config.GetValue("UseVault", false)) | |||||
{ | |||||
builder.AddAzureKeyVault($"https://{config["Vault:Name"]}.vault.azure.net/", config["Vault:ClientId"], config["Vault:ClientSecret"]); | |||||
} | |||||
}) | |||||
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>()) | |||||
.UseSerilog((host, builder) => | |||||
{ | |||||
builder.MinimumLevel.Verbose() | |||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) | |||||
.Enrich.WithProperty("ApplicationContext", host.HostingEnvironment.ApplicationName) | |||||
.Enrich.FromLogContext() | |||||
.WriteTo.Console() | |||||
.WriteTo.Seq(string.IsNullOrWhiteSpace(host.Configuration["Serilog:SeqServerUrl"]) ? "http://seq" : host.Configuration["Serilog:SeqServerUrl"]) | |||||
.WriteTo.Http(string.IsNullOrWhiteSpace(host.Configuration["Serilog:LogstashUrl"]) ? "http://logstash:8080" : host.Configuration["Serilog:LogstashUrl"]) | |||||
.ReadFrom.Configuration(host.Configuration); | |||||
}); | |||||
} | |||||
} |
@ -0,0 +1,12 @@ | |||||
{ | |||||
"profiles": { | |||||
"Coupon.API": { | |||||
"commandName": "Project", | |||||
"launchBrowser": true, | |||||
"environmentVariables": { | |||||
"ASPNETCORE_ENVIRONMENT": "Development" | |||||
}, | |||||
"applicationUrl": "https://localhost:49935;http://localhost:49936" | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,94 @@ | |||||
using Coupon.API.Extensions; | |||||
using Coupon.API.Filters; | |||||
using Coupon.API.IntegrationEvents.EventHandlers; | |||||
using Coupon.API.IntegrationEvents.Events; | |||||
using HealthChecks.UI.Client; | |||||
using Microsoft.AspNetCore.Builder; | |||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.Extensions.Configuration; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Serilog; | |||||
namespace Coupon.API | |||||
{ | |||||
public class Startup | |||||
{ | |||||
public Startup(IConfiguration configuration) | |||||
{ | |||||
Configuration = configuration; | |||||
} | |||||
public IConfiguration Configuration { get; } | |||||
public void ConfigureServices(IServiceCollection services) | |||||
{ | |||||
services.AddControllers(options => options.Filters.Add<ValidateModelAttribute>()); | |||||
services.AddCustomSettings(Configuration) | |||||
.AddCouponRegister(Configuration) | |||||
.AddCustomPolicies() | |||||
.AddAppInsights(Configuration) | |||||
.AddEventBus(Configuration) | |||||
.AddCustomAuthentication(Configuration) | |||||
.AddCustomAuthorization() | |||||
.AddSwagger(Configuration) | |||||
.AddCustomHealthCheck(Configuration); | |||||
services.AddTransient<IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>(); | |||||
services.AddTransient<IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>, OrderStatusChangedToCancelledIntegrationEventHandler>(); | |||||
} | |||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |||||
{ | |||||
if (env.IsDevelopment()) | |||||
{ | |||||
app.UseDeveloperExceptionPage(); | |||||
} | |||||
var pathBase = Configuration["PATH_BASE"]; | |||||
if (!string.IsNullOrEmpty(pathBase)) | |||||
{ | |||||
app.UsePathBase(pathBase); | |||||
} | |||||
app.UseSwagger() | |||||
.UseSwaggerUI(options => | |||||
{ | |||||
options.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Coupon.API V1"); | |||||
options.OAuthClientId("couponswaggerui"); | |||||
options.OAuthAppName("eShop-Learn.Coupon.API Swagger UI"); | |||||
}) | |||||
.UseCors("CorsPolicy") | |||||
.UseRouting() | |||||
.UseAuthentication() | |||||
.UseAuthorization() | |||||
.UseEndpoints(endpoints => | |||||
{ | |||||
endpoints.MapControllers(); | |||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions | |||||
{ | |||||
Predicate = _ => true, | |||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||||
}); | |||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||||
{ | |||||
Predicate = r => r.Name.Contains("self") | |||||
}); | |||||
}); | |||||
ConfigureEventBus(app); | |||||
} | |||||
private void ConfigureEventBus(IApplicationBuilder app) | |||||
{ | |||||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | |||||
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>>(); | |||||
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>>(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,18 @@ | |||||
{ | |||||
"ConnectionString": "mongodb://localhost:27017", | |||||
"CouponMongoDatabase": "CouponDb", | |||||
"Serilog": { | |||||
"MinimumLevel": { | |||||
"Default": "Debug", | |||||
"Override": { | |||||
"Microsoft": "Warning", | |||||
"Microsoft.eShopOnContainers": "Debug", | |||||
"System": "Warning" | |||||
} | |||||
} | |||||
}, | |||||
"IdentityUrlExternal": "http://localhost:5105", | |||||
"IdentityUrl": "http://localhost:5105", | |||||
"AzureServiceBusEnabled": false, | |||||
"EventBusConnection": "localhost" | |||||
} |
@ -0,0 +1,28 @@ | |||||
{ | |||||
"ConnectionString": null, | |||||
"CouponMongoDatabase": "CouponDb", | |||||
"UseCustomizationData": false, | |||||
"Serilog": { | |||||
"SeqServerUrl": null, | |||||
"LogstashUrl": null, | |||||
"MinimumLevel": { | |||||
"Default": "Information", | |||||
"Override": { | |||||
"Microsoft": "Warning", | |||||
"Microsoft.eShopOnContainers": "Information", | |||||
"System": "Warning" | |||||
} | |||||
} | |||||
}, | |||||
"SubscriptionClientName": "Coupon", | |||||
"ApplicationInsights": { | |||||
"InstrumentationKey": "" | |||||
}, | |||||
"EventBusRetryCount": 5, | |||||
"UseVault": false, | |||||
"Vault": { | |||||
"Name": "eshop", | |||||
"ClientId": "your-client-id", | |||||
"ClientSecret": "your-client-secret" | |||||
} | |||||
} |
@ -0,0 +1,5 @@ | |||||
export interface ICoupon { | |||||
discount: number; | |||||
code: string; | |||||
message: string | |||||
} |