├── .github
└── workflows
│ ├── ci-images.yml
│ ├── deploy-monolith-api.yml
│ └── push-images.yml
├── .gitignore
├── README.md
├── deploy
├── README.md
├── api-management
│ ├── README.md
│ └── policies
│ │ ├── README.md
│ │ ├── circuit-breaker.xml
│ │ ├── migrate-to-app-services-products.xml
│ │ ├── migrate-to-kubernetes-products.xml
│ │ ├── route-traffic-based-on-gateway.xml
│ │ ├── single-backend.xml
│ │ └── traffic-load-balancer.xml
├── arm
│ ├── README.md
│ ├── api-management
│ │ ├── circuit-breaker.json
│ │ └── load-balancer.json
│ ├── cluster
│ │ └── armdeploy.json
│ └── monolith
│ │ └── armdeploy.json
└── kubernetes
│ ├── README.md
│ ├── deploy-orders.yaml
│ ├── deploy-products.yaml
│ └── deploy-shipments.yaml
├── docs
├── contoso-tomorrow.md
├── meet-contoso.md
├── migrating-to-kubernetes.md
└── migrating-to-web-app-for-containers.md
├── media
├── api-management-gateway.png
├── contoso-future.png
├── contoso-phase-I.png
├── contoso-phase-II-internals.png
├── contoso-phase-II.png
├── contoso-today.png
└── contoso.jpg
├── openapi
├── README.md
├── orders.json
├── products.json
├── shipment_webhook.json
└── shipments.json
├── product-catalog.csv
└── src
├── microservices
├── .dockerignore
├── Demo.Microservices.Compose.dcproj
├── Demo.Microservices.Orders.API
│ ├── Contracts
│ │ └── v1
│ │ │ ├── Address.cs
│ │ │ ├── Customer.cs
│ │ │ ├── Order.cs
│ │ │ ├── OrderConfirmation.cs
│ │ │ ├── OrderLine.cs
│ │ │ ├── Product.cs
│ │ │ ├── ShipmentInformation.cs
│ │ │ └── ShipmentStatus.cs
│ ├── Controllers
│ │ ├── HealthController.cs
│ │ └── OrdersController.cs
│ ├── Data
│ │ ├── Contracts
│ │ │ └── v1
│ │ │ │ └── OrderTableEntity.cs
│ │ └── Providers
│ │ │ └── TableStorageAccessor.cs
│ ├── Demo.Microservices.Orders.API.csproj
│ ├── Dockerfile
│ ├── Docs
│ │ └── Open-Api.xml
│ ├── Extensions
│ │ ├── IApplicationBuilderExtensions.cs
│ │ └── IServiceCollectionExtensions.cs
│ ├── Managers
│ │ └── OrderManager.cs
│ ├── Program.cs
│ ├── Repositories
│ │ ├── InMemory
│ │ │ ├── InMemoryOrderRepository.cs
│ │ │ └── OrderTableRepository.cs
│ │ └── Interfaces
│ │ │ └── IOrderRepository.cs
│ ├── Services
│ │ ├── Interfaces
│ │ │ └── IShipmentService.cs
│ │ └── ShipmentService.cs
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── Demo.Microservices.Products.API
│ ├── Contracts
│ │ └── v1
│ │ │ └── Product.cs
│ ├── Controllers
│ │ ├── HealthController.cs
│ │ └── ProductsController.cs
│ ├── Data
│ │ ├── Contracts
│ │ │ └── v1
│ │ │ │ └── ProductTableEntity.cs
│ │ └── Providers
│ │ │ └── TableStorageAccessor.cs
│ ├── Demo.Microservices.Products.API.csproj
│ ├── Dockerfile
│ ├── Docs
│ │ └── Open-Api.xml
│ ├── Extensions
│ │ ├── IApplicationBuilderExtensions.cs
│ │ └── IServiceCollectionExtensions.cs
│ ├── Program.cs
│ ├── Repositories
│ │ ├── InMemory
│ │ │ ├── InMemoryProductRepository.cs
│ │ │ └── ProductTableRepository.cs
│ │ └── Interfaces
│ │ │ └── IProductRepository.cs
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── Demo.Microservices.Shipments.API
│ ├── Controllers
│ │ ├── HealthController.cs
│ │ └── ShipmentsController.cs
│ ├── Demo.Microservices.Shipments.API.csproj
│ ├── Dockerfile
│ ├── Docs
│ │ └── Open-Api.xml
│ ├── Extensions
│ │ ├── IApplicationBuilderExtensions.cs
│ │ └── IServiceCollectionExtensions.cs
│ ├── OpenAPI
│ │ └── OpenApiCategories.cs
│ ├── Program.cs
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── Demo.Microservices.Shipments.Contracts
│ ├── Demo.Microservices.Shipments.Contracts.csproj
│ └── v1
│ │ ├── Address.cs
│ │ ├── ShipmentInformation.cs
│ │ ├── ShipmentStatus.cs
│ │ └── ShipmentStatusUpdate.cs
├── Demo.Microservices.Shipments.Data
│ ├── Contracts
│ │ └── v1
│ │ │ └── ShipmentTableEntity.cs
│ ├── Demo.Microservices.Shipments.Data.csproj
│ ├── Exceptions
│ │ └── ShipmentNotFoundException.cs
│ ├── Providers
│ │ └── TableStorageAccessor.cs
│ └── Repositories
│ │ ├── InMemory
│ │ ├── InMemoryShipmentRepository.cs
│ │ └── ShipmentsTableRepository.cs
│ │ └── Interfaces
│ │ └── IShipmentRepository.cs
├── Demo.Microservices.Shipments.Webhooks
│ ├── .gitignore
│ ├── Demo.Microservices.Shipments.Webhooks.csproj
│ ├── Startup.cs
│ ├── StatusUpdateFunction.cs
│ └── host.json
├── Demo.Microservices.sln
├── api-gateway.conf
├── docker-compose.override.yml
└── docker-compose.yml
└── monolith
├── .dockerignore
├── Demo.Monolith.API
├── .config
│ └── dotnet-tools.json
├── Contracts
│ └── v1
│ │ ├── Address.cs
│ │ ├── Customer.cs
│ │ ├── Order.cs
│ │ ├── OrderConfirmation.cs
│ │ ├── OrderLine.cs
│ │ ├── Product.cs
│ │ ├── ShipmentInformation.cs
│ │ ├── ShipmentStatus.cs
│ │ └── ShipmentStatusUpdate.cs
├── Controllers
│ ├── HealthController.cs
│ ├── OrdersController.cs
│ ├── ProductsController.cs
│ └── ShipmentsController.cs
├── Data
│ ├── Contracts
│ │ └── v1
│ │ │ ├── OrderTableEntity.cs
│ │ │ ├── ProductTableEntity.cs
│ │ │ └── ShipmentTableEntity.cs
│ └── Providers
│ │ └── TableStorageAccessor.cs
├── Demo.Monolith.API.csproj
├── Dockerfile
├── Docs
│ └── Open-Api.xml
├── Exceptions
│ └── ShipmentNotFoundException.cs
├── Extensions
│ ├── IApplicationBuilderExtensions.cs
│ └── IServiceCollectionExtensions.cs
├── Managers
│ └── OrderManager.cs
├── OpenAPI
│ └── OpenApiCategories.cs
├── Program.cs
├── Repositories
│ ├── InMemory
│ │ ├── InMemoryOrderRepository.cs
│ │ ├── InMemoryProductRepository.cs
│ │ ├── InMemoryShipmentRepository.cs
│ │ ├── OrderTableRepository.cs
│ │ ├── ProductTableRepository.cs
│ │ └── ShipmentsTableRepository.cs
│ └── Interfaces
│ │ ├── IOrderRepository.cs
│ │ ├── IProductRepository.cs
│ │ └── IShipmentRepository.cs
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
├── Demo.sln
└── UpgradeReport.sarif
/.github/workflows/ci-images.yml:
--------------------------------------------------------------------------------
1 | name: CI (Runtime)
2 | on:
3 | pull_request:
4 | paths:
5 | - '.github/workflows/ci-images.yml'
6 | - 'src/**'
7 |
8 | env:
9 | IMAGE_NAME: ghcr.io/tomkerkhove/k8s-event-grid-bridge
10 | IMAGE_TAG: experimental
11 |
12 | jobs:
13 | solution_microservices:
14 | name: Solution (Microservices)
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Build Solution
19 | run: dotnet build ./src/microservices/Demo.Microservices.sln
20 | solution_monolith:
21 | name: Solution (Monolith)
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v2
25 | - name: Build Solution
26 | run: dotnet build ./src/monolith/Demo.sln
27 | monolith:
28 | name: Build Monolith (Docker)
29 | runs-on: ubuntu-latest
30 | env:
31 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-monolith
32 | steps:
33 | - uses: actions/checkout@v2
34 | - name: Build the Docker image
35 | run: docker build ./src/monolith/ --file ./src/monolith/Demo.Monolith.API/Dockerfile
36 | microservices_orders:
37 | name: Build Order Microservice (Docker)
38 | runs-on: ubuntu-latest
39 | env:
40 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-services-order
41 | steps:
42 | - uses: actions/checkout@v2
43 | - name: Build the Docker image
44 | run: docker build ./src/microservices --file ./src/microservices/Demo.Microservices.Orders.API/Dockerfile
45 | microservices_shipments:
46 | name: Build Shipments Microservice (Docker)
47 | runs-on: ubuntu-latest
48 | env:
49 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-services-shipment
50 | steps:
51 | - uses: actions/checkout@v2
52 | - name: Build the Docker image
53 | run: docker build ./src/microservices --file ./src/microservices/Demo.Microservices.Shipments.API/Dockerfile
54 | microservices_products:
55 | name: Build Products Microservice (Docker)
56 | runs-on: ubuntu-latest
57 | env:
58 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-services-product
59 | steps:
60 | - uses: actions/checkout@v2
61 | - name: Build the Docker image
62 | run: docker build ./src/microservices --file ./src/microservices/Demo.Microservices.Products.API/Dockerfile
--------------------------------------------------------------------------------
/.github/workflows/deploy-monolith-api.yml:
--------------------------------------------------------------------------------
1 | name: Ship Monolith API to Azure Web App
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | env:
8 | AZURE_WEBAPP_NAME: contoso-monolith-api
9 | AZURE_WEBAPP_PACKAGE_PATH: Demo.Monolith.API\publish
10 | CONFIGURATION: Release
11 | DOTNET_CORE_VERSION: 7.x
12 | WORKING_DIRECTORY: src/monolith/Demo.Monolith.API
13 | jobs:
14 | build:
15 | runs-on: windows-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Setup .NET SDK
19 | uses: actions/setup-dotnet@v3
20 | with:
21 | dotnet-version: ${{ env.DOTNET_CORE_VERSION }}
22 | - name: Restore
23 | run: dotnet restore "${{ env.WORKING_DIRECTORY }}"
24 | - name: Build
25 | run: dotnet build "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-restore
26 | - name: Test
27 | run: dotnet test "${{ env.WORKING_DIRECTORY }}" --no-build
28 | - name: Publish
29 | run: dotnet publish "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-build --output "${{ env.AZURE_WEBAPP_PACKAGE_PATH }}"
30 | - name: Publish Artifacts
31 | uses: actions/upload-artifact@v3
32 | with:
33 | name: webapp
34 | path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
35 | deploy:
36 | runs-on: windows-latest
37 | needs: build
38 | steps:
39 | - name: Login to Azure
40 | uses: azure/login@v1
41 | with:
42 | creds: ${{ secrets.AZURE_CREDENTIALS }}
43 | - name: Download artifact from build job
44 | uses: actions/download-artifact@v3
45 | with:
46 | name: webapp
47 | path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
48 | - name: Deploy to Azure WebApp
49 | uses: azure/webapps-deploy@v2
50 | with:
51 | app-name: ${{ env.AZURE_WEBAPP_NAME }}
52 | package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
53 |
--------------------------------------------------------------------------------
/.github/workflows/push-images.yml:
--------------------------------------------------------------------------------
1 | name: Push Docker Images to GitHub Container Registry
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | imageTag:
6 | description: 'Tag to push images with'
7 | required: true
8 | type: string
9 | release:
10 | types: [published]
11 | push:
12 | paths:
13 | - '.github/workflows/ci-images.yml'
14 | - 'src/**'
15 | branches:
16 | - main
17 | env:
18 | IMAGE_TAG: latest
19 | jobs:
20 | monolith:
21 | name: Push Monolith to GHCR
22 | runs-on: ubuntu-latest
23 | env:
24 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-monolith
25 | steps:
26 | - uses: actions/checkout@v2
27 | - name: Determine image tag from Release (${{ github.ref_name }})
28 | if: ${{ github.event_name == 'release' }}
29 | run: echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
30 | - name: Determine image tag from manual trigger (${{ inputs.imageTag }})
31 | if: ${{ github.event_name == 'workflow_dispatch' }}
32 | run: echo "IMAGE_TAG=${{ inputs.imageTag }}" >> $GITHUB_ENV
33 | - name: Docker Login
34 | uses: docker/login-action@v1.8.0
35 | with:
36 | registry: ghcr.io
37 | username: tomkerkhove
38 | password: ${{ secrets.GITHUB_TOKEN }}
39 | - name: Build the Docker image
40 | run: docker build ./src/monolith/ --file ./src/monolith/Demo.Monolith.API/Dockerfile --tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
41 | - name: Push the Docker image
42 | run: docker push ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
43 | microservices_orders:
44 | name: Push Order Microservice to GHCR
45 | runs-on: ubuntu-latest
46 | env:
47 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-services-order
48 | steps:
49 | - uses: actions/checkout@v2
50 | - name: Determine image tag from Release (${{ github.ref_name }})
51 | if: ${{ github.event_name == 'release' }}
52 | run: echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
53 | - name: Determine image tag from manual trigger (${{ inputs.imageTag }})
54 | if: ${{ github.event_name == 'workflow_dispatch' }}
55 | run: echo "IMAGE_TAG=${{ inputs.imageTag }}" >> $GITHUB_ENV
56 | - name: Docker Login
57 | uses: docker/login-action@v1.8.0
58 | with:
59 | registry: ghcr.io
60 | username: tomkerkhove
61 | password: ${{ secrets.GITHUB_TOKEN }}
62 | - name: Build the Docker image
63 | run: docker build ./src/microservices --file ./src/microservices/Demo.Microservices.Orders.API/Dockerfile --tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
64 | - name: Push the Docker image
65 | run: docker push ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
66 | microservices_shipments:
67 | name: Push Shipments Microservice to GHCR
68 | runs-on: ubuntu-latest
69 | env:
70 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-services-shipment
71 | steps:
72 | - uses: actions/checkout@v2
73 | - name: Determine image tag from Release (${{ github.ref_name }})
74 | if: ${{ github.event_name == 'release' }}
75 | run: echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
76 | - name: Determine image tag from manual trigger (${{ inputs.imageTag }})
77 | if: ${{ github.event_name == 'workflow_dispatch' }}
78 | run: echo "IMAGE_TAG=${{ inputs.imageTag }}" >> $GITHUB_ENV
79 | - name: Docker Login
80 | uses: docker/login-action@v1.8.0
81 | with:
82 | registry: ghcr.io
83 | username: tomkerkhove
84 | password: ${{ secrets.GITHUB_TOKEN }}
85 | - name: Build the Docker image
86 | run: docker build ./src/microservices --file ./src/microservices/Demo.Microservices.Shipments.API/Dockerfile --tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
87 | - name: Push the Docker image
88 | run: docker push ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
89 | microservices_products:
90 | name: Push Products Microservice to GHCR
91 | runs-on: ubuntu-latest
92 | env:
93 | IMAGE_NAME: ghcr.io/tomkerkhove/microservices-with-api-management-services-product
94 | steps:
95 | - uses: actions/checkout@v2
96 | - name: Determine image tag from Release (${{ github.ref_name }})
97 | if: ${{ github.event_name == 'release' }}
98 | run: echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
99 | - name: Determine image tag from manual trigger (${{ inputs.imageTag }})
100 | if: ${{ github.event_name == 'workflow_dispatch' }}
101 | run: echo "IMAGE_TAG=${{ inputs.imageTag }}" >> $GITHUB_ENV
102 | - name: Docker Login
103 | uses: docker/login-action@v1.8.0
104 | with:
105 | registry: ghcr.io
106 | username: tomkerkhove
107 | password: ${{ secrets.GITHUB_TOKEN }}
108 | - name: Build the Docker image
109 | run: docker build ./src/microservices --file ./src/microservices/Demo.Microservices.Products.API/Dockerfile --tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
110 | - name: Push the Docker image
111 | run: docker push ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Microservices with Azure API Management
2 | Contoso, a fictuous company, provides industry-leading APIs to their customers for purchasing Microsoft Products.
3 |
4 | Learn about their journey where they decompose a monolith into multiple smaller microservices and how they've migrated without downtime by managing API traffic with Azure API Management.
5 |
6 | - [Meet Contoso & Why they are transitioning to microservices](./docs/meet-contoso.md)
7 | - [Migrating to Azure Web App for Containers](./docs/migrating-to-web-app-for-containers.md)
8 | - [Migrating to Azure Kubernetes Service](./docs/migrating-to-kubernetes.md)
9 | - [Contoso Tomorrow](./docs/contoso-tomorrow.md)
10 |
11 | Curious? [Run it yourself](/deploy) and give it a try!
12 |
13 | 
14 |
--------------------------------------------------------------------------------
/deploy/README.md:
--------------------------------------------------------------------------------
1 | # Deployment
2 |
3 | This folder contains all artifacts to deploy to your own version of Azure API Management with microservices in Kubernetes.
--------------------------------------------------------------------------------
/deploy/api-management/README.md:
--------------------------------------------------------------------------------
1 | # Azure API Management
2 |
3 | This folder contains all artifacts to deploy to your own Azure API Management resource:
4 |
5 | - **Policies** are used to route traffic to multiple endpoints with an A/B routing principle
6 | - **Developer Portal** contains all the resources to run the Developer Portal yourself.
7 | - More information on how to host it yourself can be found on [GitHub](https://github.com/Azure/api-management-developer-portal)
8 | - *Note - This is based on the public preview and might be broken when going GA*
--------------------------------------------------------------------------------
/deploy/api-management/policies/README.md:
--------------------------------------------------------------------------------
1 | # Azure API Management policies
2 |
3 | Policies used to influence traffic routing across 2 versions in a 50/50 fashion.
4 |
5 | For production workloads you should use custom properties to more easily change the upstream URLs without having to redeploy the policies.
--------------------------------------------------------------------------------
/deploy/api-management/policies/circuit-breaker.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | @(context.LastError.Source)
16 |
17 |
18 | @(context.LastError.Reason)
19 |
20 |
21 | @(context.LastError.Message)
22 |
23 |
24 | @(context.LastError.Scope)
25 |
26 |
27 | @(context.LastError.Section)
28 |
29 |
30 | @(context.LastError.Path)
31 |
32 |
33 | @(context.LastError.PolicyId)
34 |
35 |
36 |
--------------------------------------------------------------------------------
/deploy/api-management/policies/migrate-to-app-services-products.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | @{return Guid.NewGuid().ToString();}
22 |
23 | A gateway-related error occurred while processing the request.
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Legacy
38 |
39 |
40 |
41 |
42 | Experiment (App Services with Containers)
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/deploy/api-management/policies/migrate-to-kubernetes-products.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | @{return Guid.NewGuid().ToString();}
21 |
22 | A gateway-related error occurred while processing the request.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Experiment (App Services with Containers)
36 |
37 |
38 |
39 |
40 | Experiment (Kubernetes)
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/deploy/api-management/policies/route-traffic-based-on-gateway.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | @(context.Deployment.Gateway.Id)
18 |
19 |
20 | @(context.Deployment.Gateway.IsManaged.ToString())
21 |
22 |
23 | @(context.Deployment.Gateway.InstanceId)
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/deploy/api-management/policies/single-backend.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | @(context.Variables.GetValueOrDefault("upstreamUrl"))
12 |
13 |
14 | @(context.Request.Url.ToString())
15 |
16 |
17 | @(context.Deployment.Region)
18 |
19 |
20 | @(context.Deployment.ServiceName)
21 |
22 |
23 | @(context.Deployment.Gateway.Id)
24 |
25 |
26 | @(context.Deployment.Gateway.IsManaged.ToString())
27 |
28 |
29 | @(context.Deployment.Gateway.InstanceId)
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/deploy/api-management/policies/traffic-load-balancer.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/deploy/arm/README.md:
--------------------------------------------------------------------------------
1 | # ARM Templates
2 |
3 | This folder contains all ARM templates that allow you to deploy the Azure infrastructure yourself.
4 |
5 | The ARM templates are exported ARM templates from the Azure portal and are provided as-is.
--------------------------------------------------------------------------------
/deploy/arm/api-management/circuit-breaker.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "apimServiceName": {
6 | "type": "string"
7 | }
8 | },
9 | "resources": [
10 | {
11 | "type": "Microsoft.ApiManagement/service/backends",
12 | "apiVersion": "2023-05-01-preview",
13 | "name": "[concat(parameters('apimServiceName'), '/bacon-api-with-circuit-breaker')]",
14 | "properties": {
15 | "protocol": "http",
16 | "url": "https://bacon-api.wonderfulbeach-99861d1c.westeurope.azurecontainerapps.io",
17 | "circuitBreaker": {
18 | "rules": [
19 | {
20 | "failureCondition": {
21 | "count": "5",
22 | "interval": "PT1M",
23 | "statusCodeRanges": [
24 | {
25 | "min": "500",
26 | "max": "599"
27 | }
28 | ]
29 | },
30 | "name": "failed-requests",
31 | "tripDuration": "PT1M"
32 | }
33 | ]
34 | }
35 | }
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/deploy/arm/api-management/load-balancer.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "apimServiceName": {
6 | "type": "string"
7 | }
8 | },
9 | "resources": [
10 | {
11 | "type": "Microsoft.ApiManagement/service/backends",
12 | "apiVersion": "2023-05-01-preview",
13 | "name": "[concat(parameters('apimServiceName'), '/products-load-balancer')]",
14 | "properties": {
15 | "description": "Load balancer for multiple backends",
16 | "type": "Pool",
17 | "protocol": "http",
18 | "url": "http://does-not-matter",
19 | "properties": null,
20 | "pool": {
21 | "services": [
22 | {
23 | "id": "/backends/contoso-monolith"
24 | },
25 | {
26 | "id": "/backends/products-api"
27 | }
28 | ]
29 | }
30 | }
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/deploy/kubernetes/README.md:
--------------------------------------------------------------------------------
1 | # Kubernetes deployment
2 |
3 | Deployment specification for running Contoso's application on Kubernetes.
--------------------------------------------------------------------------------
/deploy/kubernetes/deploy-orders.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: contoso-microservices-orders
5 | labels:
6 | app: contoso
7 | microservice: orders
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: contoso
12 | microservice: orders
13 | template:
14 | metadata:
15 | labels:
16 | app: contoso
17 | microservice: orders
18 | spec:
19 | containers:
20 | - name: orders-api
21 | image: ghcr.io/tomkerkhove/microservices-with-api-management-services-order:0.2.0
22 | ports:
23 | - containerPort: 80
24 | env:
25 | - name: AZURESTORAGE_CONNECTIONSTRING
26 | valueFrom:
27 | secretKeyRef:
28 | name: orders-secrets
29 | key: AZURESTORAGE_CONNECTIONSTRING
30 | - name: SHIPMENTS_API_URI
31 | value: http://hybrid-gateway-azure-api-management-gateway:8080/contoso/shipment-management/api/v1/shipments
32 | - name: SHIPMENTS_API_KEY
33 | valueFrom:
34 | secretKeyRef:
35 | name: orders-secrets
36 | key: SHIPMENTS_API_KEY
37 | ---
38 | apiVersion: v1
39 | kind: Service
40 | metadata:
41 | name: contoso-microservices-orders-loadbalancer-service
42 | labels:
43 | app: contoso
44 | microservice: orders
45 | annotations:
46 | service.beta.kubernetes.io/azure-dns-label-name: contoso-orders-api
47 | spec:
48 | type: LoadBalancer
49 | ports:
50 | - name: http
51 | port: 8889
52 | targetPort: 80
53 | protocol: TCP
54 | selector:
55 | app: contoso
56 | microservice: orders
57 | ---
58 | apiVersion: v1
59 | kind: Secret
60 | metadata:
61 | name: orders-secrets
62 | labels:
63 | app: contoso
64 | microservice: orders
65 | data:
66 | AZURESTORAGE_CONNECTIONSTRING:
67 | SHIPMENTS_API_KEY:
--------------------------------------------------------------------------------
/deploy/kubernetes/deploy-products.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: contoso-microservices-products
5 | labels:
6 | app: contoso
7 | microservice: products
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: contoso
12 | microservice: products
13 | template:
14 | metadata:
15 | labels:
16 | app: contoso
17 | microservice: products
18 | spec:
19 | containers:
20 | - name: products-api
21 | image: ghcr.io/tomkerkhove/microservices-with-api-management-services-product:0.2.0
22 | ports:
23 | - containerPort: 80
24 | env:
25 | - name: AZURESTORAGE_CONNECTIONSTRING
26 | valueFrom:
27 | secretKeyRef:
28 | name: products-secrets
29 | key: AZURESTORAGE_CONNECTIONSTRING
30 | ---
31 | apiVersion: v1
32 | kind: Service
33 | metadata:
34 | name: contoso-microservices-products-loadbalancer-service
35 | labels:
36 | app: contoso
37 | microservice: products
38 | annotations:
39 | service.beta.kubernetes.io/azure-dns-label-name: contoso-products-api
40 | spec:
41 | type: LoadBalancer
42 | ports:
43 | - name: http
44 | port: 8888
45 | targetPort: 80
46 | protocol: TCP
47 | selector:
48 | app: contoso
49 | microservice: products
50 | ---
51 | apiVersion: v1
52 | kind: Secret
53 | metadata:
54 | name: products-secrets
55 | labels:
56 | app: contoso
57 | microservice: products
58 | data:
59 | AZURESTORAGE_CONNECTIONSTRING:
--------------------------------------------------------------------------------
/deploy/kubernetes/deploy-shipments.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: contoso-microservices-shipments
5 | labels:
6 | app: contoso
7 | microservice: shipments
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: contoso
12 | microservice: shipments
13 | template:
14 | metadata:
15 | labels:
16 | app: contoso
17 | microservice: shipments
18 | spec:
19 | containers:
20 | - name: shipments-api
21 | image: ghcr.io/tomkerkhove/microservices-with-api-management-services-shipment:0.2.0
22 | ports:
23 | - containerPort: 80
24 | env:
25 | - name: AZURESTORAGE_CONNECTIONSTRING
26 | valueFrom:
27 | secretKeyRef:
28 | name: shipments-secrets
29 | key: AZURESTORAGE_CONNECTIONSTRING
30 | ---
31 | apiVersion: v1
32 | kind: Service
33 | metadata:
34 | name: contoso-microservices-shipments-loadbalancer-service
35 | labels:
36 | app: contoso
37 | microservice: shipments
38 | annotations:
39 | service.beta.kubernetes.io/azure-dns-label-name: contoso-shipments-api
40 | spec:
41 | type: LoadBalancer
42 | ports:
43 | - name: http
44 | port: 8890
45 | targetPort: 80
46 | protocol: TCP
47 | selector:
48 | app: contoso
49 | microservice: shipments
50 | ---
51 | apiVersion: v1
52 | kind: Service
53 | metadata:
54 | name: contoso-microservices-shipments-internal-service
55 | labels:
56 | app: contoso
57 | microservice: shipments
58 | spec:
59 | ports:
60 | - port: 88
61 | targetPort: 80
62 | protocol: TCP
63 | selector:
64 | app: contoso
65 | microservice: shipments
66 | ---
67 | apiVersion: v1
68 | kind: Secret
69 | metadata:
70 | name: shipments-secrets
71 | labels:
72 | app: contoso
73 | microservice: shipments
74 | data:
75 | AZURESTORAGE_CONNECTIONSTRING:
--------------------------------------------------------------------------------
/docs/contoso-tomorrow.md:
--------------------------------------------------------------------------------
1 | # Contoso Tomorrow
2 |
3 | At BUILD 2019, the Azure API Management team announced **Azure API Management Gateway** which is a federated gateway which allows you to deploy the same Azure API Management gateway wherever you want!
4 |
5 | This allows your consumers to communicate with a local gateway which is managed in the cloud allowing them to stay on the same network which reduces latency.
6 |
7 | 
8 |
9 | This is good news for Contoso because they can now not only decouple external customers from their application, it brings the same benefits for internal customers.
10 |
11 | When they've migrated to Kubernetes, the Order service was communicating to the Shipment service directly.
12 |
13 | This means that the Shipment service has no control over how it's called, enforce throttling or route traffic to a new version for A/B testing.
14 |
15 | 
16 |
17 | With Azure API Management Gateway, however, they can deploy the gateway inside the cluster and expose the Shipment service to their internal customers with the same API Gateway experience:
18 |
19 | 
--------------------------------------------------------------------------------
/docs/meet-contoso.md:
--------------------------------------------------------------------------------
1 | # Meet Contoso
2 | Contoso provides industry-leading APIs for purchasing Microsoft Products.
3 |
4 | Their customers can fully automate:
5 | - Listing the product catalog
6 | - Ordering products to be delivered at home
7 | - Getting information about a shipment
8 |
9 | To deliver all orders, they have partnered with multiple 3rd parties to deliver shipments.
10 | These 3rd party service providers are in charge of providing updates about package deliveries.
11 |
12 | This is handled by pushing status updates to a Contoso webhook endpoint.
13 |
14 | 
15 |
16 | ## Where are they today?
17 |
18 | As of today, Contoso is exposing their services via Azure API Management which gives them all the rich API ecosystem features they need ranging from service decoupling, developer portal, and user management.
19 |
20 | Their monolith is hosted in an Azure Web App which is all written in .NET Core.
21 |
22 | 
23 |
24 | ## Transition to microservices
25 | Contoso wants to transition to microservices:
26 | - Provide the capability to easily ship new features
27 | - Allow services to run on specialized compute
28 | - Increase service ownership
29 |
30 | But, we need to ensure that
31 | - Customer experience does not change
32 | - Developers can experiment with new approaches with A/B testing
33 | - Customers have one central gateway for all microservices
34 |
35 | ## How do we get there?
36 | Use a phased-migration approach to reduce complexity and risk
37 |
38 | Microservices are a journey:
39 | 1. Split the monolith into multiple smaller services
40 | 2. Ship them as individual containers
41 |
42 | Use as much PaaS as you can, until you need more control:
43 | - Azure App Services & Azure Integration Services are a good starting point.
44 | - Easily port your same application to Azure Kubernetes Service, if you need more control.
45 |
46 | Want to learn how?
47 | - [Migrating to Azure Web App for Containers](./migrating-to-web-app-for-containers.md)
48 | - [Migrating to Azure Kubernetes Service](./migrating-to-kubernetes.md)
49 |
--------------------------------------------------------------------------------
/docs/migrating-to-kubernetes.md:
--------------------------------------------------------------------------------
1 | # Migrating to Azure Kubernetes Service
2 |
3 | Azure Web Apps for Containers is a great way to offload all of the infrastructure to Microsoft Azure, but sometimes you need that extra control.
4 |
5 | This is what is happening to Contoso - Their business has been booming and as their customer demand is growing they need to scale to accomodate that and make sure they are always online.
6 |
7 | While App Services can still give them that, they want to spin up their own Kubernetes container orchestrator as they will onboard more services and want everything in one managed cluster.
8 |
9 | Contoso has decided to change their internal design and migrate to Kubernetes. Instead of deploying their services as Web App they will create seperate Kubernetes deployments which run multiple copies of their services, this is known as "Pods" in Kubernetes.
10 |
11 | Given Pods are locked down by default, the Shipment service will also provide an internal "Service" so that the Order service can still communicate with them.
12 |
13 | 
14 |
15 | In order to achieve this they will use the same A/B testing to guarantee that everything still works.
16 |
17 | 
18 |
19 | ## Further Reading
20 |
21 | Interested in how Contoso can decouple internal services in the future? Read about it [here](./contoso-tomorrow.md).
--------------------------------------------------------------------------------
/docs/migrating-to-web-app-for-containers.md:
--------------------------------------------------------------------------------
1 | # Migrating to Azure Web App for Containers
2 |
3 | Contoso loves Azure App Services, it allows them to run their APIs without having to worry about all the infrastructure beneath it!
4 |
5 | With their migration into multiple microservices, they want to transition to containers as it makes it a lot easier to run and makes the application more portable.
6 |
7 | However, they still want to benefit from the PaaS capabilities that Azure App Services provide so they have chosen to use Azure Web Apps for Containers!
8 |
9 | 
10 |
11 | ## Migrating to Azure Web Apps for Containers
12 | Azure Web Apps for Containers allows them to package their microservices as containers and deploy the containers on top of Azure App Services. During this migration Contoso change from packaging their app from a ZIP file to a Linux Docker container image.
13 |
14 | As part of that transition, they also had to refactor their application as the Order service needs to be able to trigger shipments. Because of this, the Shipment service has added a new API endpoint which can be consumed internally given it's not opened up on Azure API Management given customers should not have access to it.
15 |
16 | Contoso would like to experiment with serverless technologies as well, so they have decided to use Azure Functions for handling the Shipment webhook notifications coming from their 3rd party providers.
17 |
18 | ## Verifying the migration with A/B testing in Azure API Management
19 |
20 | To ensure that their customers are not impacted Contoso wants to verify their experiment before deploying it to all environments.
21 |
22 | They have chosen to use A/B testing where 50% of the traffic will be routed to the monolith and the other 50% will go to their containers running on Azure Web Apps for Containers.
23 |
24 | Azure API Management is the perfect tool to manage this as all Contoso's traffic is routed through it, but unfortunately, traffic routing is not a supported capability out-of-the-box. Luckily they can use API Management policies to extend their APIs.
25 |
26 | They managed to build a routing policy based on the [official sample](https://github.com/Azure/api-management-policy-snippets/blob/master/examples/Random%20load%20balancer.policy.xml) that they've found on GitHub.
27 |
28 | ```xml
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | @{return Guid.NewGuid().ToString();}
45 |
46 | A gateway-related error occurred while processing the request.
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Legacy
60 |
61 |
62 |
63 |
64 | Experiment (App Services with Containers)
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | ```
75 |
76 | By randomizing they will distribute the traffic across upstream services and will add a custom header in order to know where the traffic was routed to. Given Contoso uses Azure Application Insights with Azure API Management, they can also troubleshoot the traffic routing via the Application Map.
77 |
78 | ### Caveats
79 |
80 | Contoso aware that there are some caveats though:
81 |
82 | - The policy is hardcoded and requires re-deployment if the URL or traffic weight changes
83 | - The load balancing does not keep track of instance health, so if our experiment is down 50% of the requests will fail
84 |
85 | ## Further Reading
86 |
87 | Interested in how they've migrated to Kubernetes? Read about it [here](./migrating-to-kubernetes.md).
--------------------------------------------------------------------------------
/media/api-management-gateway.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/api-management-gateway.png
--------------------------------------------------------------------------------
/media/contoso-future.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/contoso-future.png
--------------------------------------------------------------------------------
/media/contoso-phase-I.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/contoso-phase-I.png
--------------------------------------------------------------------------------
/media/contoso-phase-II-internals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/contoso-phase-II-internals.png
--------------------------------------------------------------------------------
/media/contoso-phase-II.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/contoso-phase-II.png
--------------------------------------------------------------------------------
/media/contoso-today.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/contoso-today.png
--------------------------------------------------------------------------------
/media/contoso.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomkerkhove/microservices-with-azure-api-management/337786637988b5cb71605a92a19b603d99babd9e/media/contoso.jpg
--------------------------------------------------------------------------------
/openapi/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI Specifications
2 |
3 | Provides all OpenAPI specifications which are being exposed to Contoso's customers via Azure API Management.
4 |
5 | - **Orders**
6 | - **Products**
7 | - **Shipments**
8 | - **Shipment Webhooks**
9 |
10 | During the migration Contoso changed their APIs so that Order service can initiate new shipments by calling a REST endpoint exposed by the Shipments service.
11 |
12 | Given this is not a public operation they did not have to update their OpenAPI specification.
--------------------------------------------------------------------------------
/openapi/products.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "v1",
5 | "title": "Contoso - Products API",
6 | "description": "Products APIs of the Contoso platform",
7 | "contact": {
8 | "name": "Contoso",
9 | "url": "https://codit.eu"
10 | }
11 | },
12 | "host": "microservices-with-apim-monolith.azurewebsites.net",
13 | "paths": {
14 | "/api/v1/health": {
15 | "get": {
16 | "tags": [
17 | "Health"
18 | ],
19 | "summary": "Get Health",
20 | "description": "Provides an indication about the health of the scraper",
21 | "operationId": "Health_Get",
22 | "consumes": [],
23 | "produces": [],
24 | "parameters": [],
25 | "responses": {
26 | "200": {
27 | "description": "API is healthy"
28 | },
29 | "503": {
30 | "description": "API is not healthy"
31 | }
32 | }
33 | }
34 | },
35 | "/api/v1/products": {
36 | "get": {
37 | "tags": [
38 | "Products"
39 | ],
40 | "summary": "Get Products",
41 | "description": "Provides information about product catalog",
42 | "operationId": "Product_GetAll",
43 | "consumes": [],
44 | "produces": [
45 | "text/plain",
46 | "application/json",
47 | "text/json"
48 | ],
49 | "parameters": [],
50 | "responses": {
51 | "200": {
52 | "description": "Overview of our product catalog",
53 | "schema": {
54 | "uniqueItems": false,
55 | "type": "array",
56 | "items": {
57 | "$ref": "#/definitions/Product"
58 | }
59 | }
60 | },
61 | "503": {
62 | "description": "Something went wrong, please contact support"
63 | }
64 | }
65 | }
66 | },
67 | "/api/v1/products/{id}": {
68 | "get": {
69 | "tags": [
70 | "Products"
71 | ],
72 | "summary": "Get Product",
73 | "description": "Provides information about a specific product in our catalog",
74 | "operationId": "Product_Get",
75 | "consumes": [],
76 | "produces": [
77 | "text/plain",
78 | "application/json",
79 | "text/json"
80 | ],
81 | "parameters": [
82 | {
83 | "name": "id",
84 | "in": "path",
85 | "required": true,
86 | "type": "integer",
87 | "format": "int32"
88 | }
89 | ],
90 | "responses": {
91 | "200": {
92 | "description": "Information about a specific product",
93 | "schema": {
94 | "$ref": "#/definitions/Product"
95 | }
96 | },
97 | "400": {
98 | "description": "Request was invalid"
99 | },
100 | "404": {
101 | "description": "Requested product was not found"
102 | },
103 | "503": {
104 | "description": "Something went wrong, please contact support"
105 | }
106 | }
107 | }
108 | }
109 | },
110 | "definitions": {
111 | "Product": {
112 | "type": "object",
113 | "properties": {
114 | "id": {
115 | "format": "int32",
116 | "type": "integer"
117 | },
118 | "name": {
119 | "type": "string"
120 | },
121 | "price": {
122 | "format": "double",
123 | "type": "number"
124 | }
125 | }
126 | }
127 | },
128 | "tags": []
129 | }
--------------------------------------------------------------------------------
/openapi/shipment_webhook.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "v1",
5 | "title": "Contoso - Shipment Webhook API",
6 | "description": "Shipment Webhook APIs of the Contoso platform",
7 | "contact": {
8 | "name": "Contoso",
9 | "url": "https://codit.eu"
10 | }
11 | },
12 | "host": "microservices-with-apim-monolith.azurewebsites.net",
13 | "paths": {
14 | "/api/v1/health": {
15 | "get": {
16 | "tags": [
17 | "Health"
18 | ],
19 | "summary": "Get Health",
20 | "description": "Provides an indication about the health of the scraper",
21 | "operationId": "Health_Get",
22 | "consumes": [],
23 | "produces": [],
24 | "parameters": [],
25 | "responses": {
26 | "200": {
27 | "description": "API is healthy"
28 | },
29 | "503": {
30 | "description": "API is not healthy"
31 | }
32 | }
33 | }
34 | },
35 | "/api/v1/shipments": {
36 | "post": {
37 | "tags": [
38 | "Shipments"
39 | ],
40 | "summary": "Update Shipment Status",
41 | "description": "Webhook for external shipment partners to provide updates about a shipment",
42 | "operationId": "Shipment_UpdateStatus",
43 | "consumes": [
44 | "application/json-patch+json",
45 | "application/json",
46 | "text/json",
47 | "application/*+json"
48 | ],
49 | "produces": [],
50 | "parameters": [
51 | {
52 | "name": "shipmentStatusUpdate",
53 | "in": "body",
54 | "required": false,
55 | "schema": {
56 | "$ref": "#/definitions/ShipmentStatusUpdate"
57 | }
58 | }
59 | ],
60 | "responses": {
61 | "200": {
62 | "description": "Update about a specific shipment and its status"
63 | },
64 | "400": {
65 | "description": "Request was invalid"
66 | },
67 | "404": {
68 | "description": "Requested product was not found"
69 | },
70 | "503": {
71 | "description": "Something went wrong, please contact support"
72 | }
73 | }
74 | }
75 | }
76 | },
77 | "definitions": {
78 | "ShipmentStatusUpdate": {
79 | "type": "object",
80 | "properties": {
81 | "trackingNumber": {
82 | "type": "string"
83 | },
84 | "status": {
85 | "enum": [
86 | "AwaitingPickup",
87 | "InTransit",
88 | "Delivered"
89 | ],
90 | "type": "string"
91 | }
92 | }
93 | }
94 | },
95 | "tags": []
96 | }
--------------------------------------------------------------------------------
/openapi/shipments.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "v1",
5 | "title": "Contoso - Shipments API",
6 | "description": "Shipments APIs of the Contoso platform",
7 | "contact": {
8 | "name": "Contoso",
9 | "url": "https://codit.eu"
10 | }
11 | },
12 | "host": "microservices-with-apim-monolith.azurewebsites.net",
13 | "paths": {
14 | "/api/v1/health": {
15 | "get": {
16 | "tags": [
17 | "Health"
18 | ],
19 | "summary": "Get Health",
20 | "description": "Provides an indication about the health of the scraper",
21 | "operationId": "Health_Get",
22 | "consumes": [],
23 | "produces": [],
24 | "parameters": [],
25 | "responses": {
26 | "200": {
27 | "description": "API is healthy"
28 | },
29 | "503": {
30 | "description": "API is not healthy"
31 | }
32 | }
33 | }
34 | },
35 | "/api/v1/shipments/{trackingNumber}": {
36 | "get": {
37 | "tags": [
38 | "Shipments"
39 | ],
40 | "summary": "Get Shipment Information",
41 | "description": "Provides information about a shipment",
42 | "operationId": "Shipment_Get",
43 | "consumes": [],
44 | "produces": [
45 | "text/plain",
46 | "application/json",
47 | "text/json"
48 | ],
49 | "parameters": [
50 | {
51 | "name": "trackingNumber",
52 | "in": "path",
53 | "required": true,
54 | "type": "string"
55 | }
56 | ],
57 | "responses": {
58 | "200": {
59 | "description": "Information about a specific shipment",
60 | "schema": {
61 | "$ref": "#/definitions/ShipmentInformation"
62 | }
63 | },
64 | "400": {
65 | "description": "Request was invalid"
66 | },
67 | "404": {
68 | "description": "Requested product was not found"
69 | },
70 | "503": {
71 | "description": "Something went wrong, please contact support"
72 | }
73 | }
74 | }
75 | }
76 | },
77 | "definitions": {
78 | "ShipmentInformation": {
79 | "type": "object",
80 | "properties": {
81 | "trackingNumber": {
82 | "type": "string"
83 | },
84 | "status": {
85 | "enum": [
86 | "AwaitingPickup",
87 | "InTransit",
88 | "Delivered"
89 | ],
90 | "type": "string"
91 | },
92 | "deliveryAddress": {
93 | "$ref": "#/definitions/Address"
94 | }
95 | }
96 | },
97 | "Address": {
98 | "type": "object",
99 | "properties": {
100 | "street": {
101 | "type": "string"
102 | },
103 | "postalCode": {
104 | "type": "string"
105 | },
106 | "state": {
107 | "type": "string"
108 | },
109 | "country": {
110 | "type": "string"
111 | }
112 | }
113 | }
114 | },
115 | "tags": []
116 | }
--------------------------------------------------------------------------------
/product-catalog.csv:
--------------------------------------------------------------------------------
1 | Id,Name,PartitionKey,Price,RowKey,Timestamp
2 | 3,Xbox Game Pass for PC,products,3.99,3,2019-10-31T14:32:03.528Z
3 | 2,Forza Horizon 4,products,49.99,2,2019-10-31T14:31:19.000Z
4 | 1,Xbox One X,products,499.99,1,2019-10-31T14:30:18.723Z
5 |
--------------------------------------------------------------------------------
/src/microservices/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.dockerignore
2 | **/.env
3 | **/.git
4 | **/.gitignore
5 | **/.vs
6 | **/.vscode
7 | **/*.*proj.user
8 | **/azds.yaml
9 | **/charts
10 | **/bin
11 | **/obj
12 | **/Dockerfile
13 | **/Dockerfile.develop
14 | **/docker-compose.yml
15 | **/docker-compose.*.yml
16 | **/*.dbmdl
17 | **/*.jfm
18 | **/secrets.dev.yaml
19 | **/values.dev.yaml
20 | **/.toolstarget
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Compose.dcproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 2.1
5 | Linux
6 | 8b4e651d-a3d1-42b6-ad8c-aae198901bbf
7 | LaunchBrowser
8 | {Scheme}://localhost:{ServicePort}
9 | demo.microservices.api.docs
10 |
11 |
12 |
13 | docker-compose.yml
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/Address.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Orders.API.Contracts.v1
2 | {
3 | public class Address
4 | {
5 | public string Street { get; set; }
6 | public string PostalCode { get; set; }
7 | public string State { get; set; }
8 | public string Country { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Orders.API.Contracts.v1
2 | {
3 | public class Customer
4 | {
5 | public string FirstName { get; set; }
6 | public string LastName { get; set; }
7 | public Address Address { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/Order.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Demo.Microservices.Orders.API.Contracts.v1
4 | {
5 | public class Order
6 | {
7 | public Customer Customer { get; set; }
8 |
9 | public List Basket { get; set; } = new List();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/OrderConfirmation.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Orders.API.Contracts.v1
2 | {
3 | public class OrderConfirmation : Order
4 | {
5 | public string ConfirmationId { get; set; }
6 | public ShipmentInformation ShipmentInformation { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/OrderLine.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace Demo.Microservices.Orders.API.Contracts.v1
3 | {
4 | public class OrderLine
5 | {
6 | public int Amount { get; set; }
7 | public Product Product { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/Product.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Orders.API.Contracts.v1
2 | {
3 | public class Product
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; }
7 | public double Price { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/ShipmentInformation.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Orders.API.Contracts.v1
2 | {
3 | public class ShipmentInformation
4 | {
5 | public string TrackingNumber { get; set; }
6 | public ShipmentStatus Status { get; set; }
7 | public Address DeliveryAddress { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Contracts/v1/ShipmentStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Orders.API.Contracts.v1
2 | {
3 | public enum ShipmentStatus
4 | {
5 | AwaitingPickup,
6 | InTransit,
7 | Delivered
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Controllers/HealthController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 |
4 | namespace Demo.Microservices.Orders.API.Controllers
5 | {
6 | [Route("api/v1/health")]
7 | [ApiController]
8 | public class HealthController : ControllerBase
9 | {
10 | ///
11 | /// Get Health
12 | ///
13 | /// Provides an indication about the health of the scraper
14 | /// API is healthy
15 | /// API is not healthy
16 | [HttpGet]
17 | [SwaggerOperation(OperationId = "Health_Get")]
18 | public IActionResult Get()
19 | {
20 | return Ok();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Controllers/OrdersController.cs:
--------------------------------------------------------------------------------
1 | using Demo.Microservices.Orders.API.Contracts.v1;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Swashbuckle.AspNetCore.Annotations;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using Demo.Microservices.Orders.API.Managers;
7 | using Demo.Microservices.Orders.API.Repositories.Interfaces;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace Demo.Microservices.Orders.API.Controllers
11 | {
12 | [Route("api/v1/orders")]
13 | public class OrdersController : Controller
14 | {
15 | private readonly ILogger _logger;
16 | private readonly OrderManager _orderManager;
17 | private readonly IOrderRepository _orderRepository;
18 |
19 | public OrdersController(OrderManager orderManager, IOrderRepository orderRepository, ILogger logger)
20 | {
21 | _logger = logger;
22 | _orderManager = orderManager;
23 | _orderRepository = orderRepository;
24 | }
25 |
26 | ///
27 | /// Create New Order
28 | ///
29 | /// Provide capability create a new order for products from our catalog
30 | /// Order was successfully created
31 | /// Something went wrong, please contact support
32 | [HttpPost]
33 | [ProducesResponseType(typeof(OrderConfirmation), (int)HttpStatusCode.Created)]
34 | [SwaggerOperation(OperationId = "Order_Create")]
35 | public async Task Post([FromBody]Order orderRequest)
36 | {
37 | var orderConfirmation = await _orderManager.CreateAsync(orderRequest);
38 |
39 | _logger.LogInformation("Order {ConfirmationId} was created for {Customer} to deliver at {DeliverAddress} via shipment {TrackingNumber}", orderConfirmation.ConfirmationId, $"{orderConfirmation.Customer.LastName} {orderConfirmation.Customer.FirstName}", orderConfirmation.Customer.Address, orderConfirmation.ShipmentInformation.TrackingNumber);
40 |
41 | return CreatedAtAction(nameof(Get), new { ConfirmationId = orderConfirmation.ConfirmationId }, orderConfirmation);
42 | }
43 |
44 | ///
45 | /// Get Order
46 | ///
47 | /// Provide information about a previously created order
48 | /// Information about a specific order
49 | /// Request was invalid
50 | /// Requested product was not found
51 | /// Something went wrong, please contact support
52 | [HttpGet("{confirmationId}")]
53 | [ProducesResponseType(typeof(Order), (int)HttpStatusCode.OK)]
54 | [SwaggerOperation(OperationId = "Order_Get")]
55 | public async Task Get(string confirmationId)
56 | {
57 | var foundOrder = await _orderRepository.GetAsync(confirmationId);
58 | if (foundOrder == null)
59 | {
60 | _logger.LogInformation("No information found for order {ConfirmationId} ", confirmationId);
61 |
62 | return NotFound();
63 | }
64 |
65 | _logger.LogInformation("Information found for order {ConfirmationId} ", confirmationId);
66 |
67 | return Ok(foundOrder);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Data/Contracts/v1/OrderTableEntity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos.Table;
2 |
3 | namespace Demo.Microservices.Orders.API.Data.Contracts.v1
4 | {
5 | public class OrderTableEntity : TableEntity
6 | {
7 | public string ConfirmationId { get; set; }
8 | public string CustomerFirstName { get; set; }
9 | public string CustomerLastName { get; set; }
10 | public string CustomerAddress { get; set; }
11 | public string Basket { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Data/Providers/TableStorageAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using Microsoft.Azure.Cosmos.Table;
7 | using Microsoft.Extensions.Configuration;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace Demo.Microservices.Orders.API.Data.Providers
11 | {
12 | public class TableStorageAccessor
13 | {
14 | private readonly CloudTableClient _tableClient;
15 | private readonly ILogger _logger;
16 |
17 | public TableStorageAccessor(IConfiguration configuration, ILogger logger)
18 | {
19 | _logger = logger;
20 | _tableClient = CreateTableClient(configuration);
21 | }
22 |
23 | public async Task PersistAsync(string tableName, TEntity entity)
24 | where TEntity : TableEntity
25 | {
26 | var table = await GetTableAsync(tableName);
27 |
28 | var insertOrMergeOperation = TableOperation.InsertOrMerge(entity);
29 | await table.ExecuteAsync(insertOrMergeOperation);
30 | }
31 |
32 | public async Task GetAsync(string tableName, string partitionKey, string rowKey)
33 | where TEntity : TableEntity
34 | {
35 | var table = await GetTableAsync(tableName);
36 |
37 | var retrieve = TableOperation.Retrieve(partitionKey, rowKey);
38 | var tableOperationResult = await table.ExecuteAsync(retrieve);
39 |
40 | switch (tableOperationResult.HttpStatusCode)
41 | {
42 | case (int)HttpStatusCode.OK:
43 | return (TEntity)tableOperationResult.Result;
44 | case (int)HttpStatusCode.NotFound:
45 | return null;
46 | default:
47 | throw new Exception($"Failed to look up table entity with partition key '{partitionKey}' and row key '{rowKey}' from table '{tableName}'");
48 | }
49 | }
50 |
51 | public async Task> GetAsync(string tableName, string partitionKey)
52 | where TEntity : TableEntity, new()
53 | {
54 | var table = await GetTableAsync(tableName);
55 |
56 | var query = new TableQuery()
57 | .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
58 |
59 | var foundItems = table.ExecuteQuery(query);
60 | return foundItems.ToList();
61 | }
62 |
63 | private async Task GetTableAsync(string tableName)
64 | {
65 | CloudTable table = _tableClient.GetTableReference(tableName);
66 | await table.CreateIfNotExistsAsync();
67 |
68 | return table;
69 | }
70 |
71 | private CloudTableClient CreateTableClient(IConfiguration configuration)
72 | {
73 | var tableConnectionString = configuration["AZURESTORAGE_CONNECTIONSTRING"];
74 | var storageAccount = CloudStorageAccount.Parse(tableConnectionString);
75 | _logger.LogInformation($"Connecting to Azure Storage Account '{storageAccount.Credentials.AccountName}'");
76 |
77 | return storageAccount.CreateCloudTableClient();
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Demo.Microservices.Orders.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 | InProcess
6 | Linux
7 | ..\docker-compose.dcproj
8 |
9 |
10 |
11 | Docs/Open-Api.xml
12 |
13 |
14 |
15 | Docs/Open-Api.xml
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base
2 | WORKDIR /app
3 | EXPOSE 80
4 |
5 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build
6 | WORKDIR /src
7 | COPY ["Demo.Microservices.Orders.API/Demo.Microservices.Orders.API.csproj", "Demo.Microservices.Orders.API/"]
8 | RUN dotnet restore "Demo.Microservices.Orders.API/Demo.Microservices.Orders.API.csproj"
9 | COPY . .
10 | WORKDIR "/src/Demo.Microservices.Orders.API"
11 | RUN dotnet build "Demo.Microservices.Orders.API.csproj" -c Release -o /app
12 |
13 | FROM build AS publish
14 | RUN dotnet publish "Demo.Microservices.Orders.API.csproj" -c Release -o /app
15 |
16 | FROM base AS final
17 | WORKDIR /app
18 | COPY --from=publish /app .
19 | ENTRYPOINT ["dotnet", "Demo.Microservices.Orders.API.dll"]
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Docs/Open-Api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo.Microservices.Orders.API
5 |
6 |
7 |
8 |
9 | Get Health
10 |
11 | Provides an indication about the health of the scraper
12 | API is healthy
13 | API is not healthy
14 |
15 |
16 |
17 | Create New Order
18 |
19 | Provide capability create a new order for products from our catalog
20 | Order was successfully created
21 | Something went wrong, please contact support
22 |
23 |
24 |
25 | Get Order
26 |
27 | Provide information about a previously created order
28 | Information about a specific order
29 | Request was invalid
30 | Requested product was not found
31 | Something went wrong, please contact support
32 |
33 |
34 |
35 | Provides an Application Builder extension for the Swagger/OpenAPI integration
36 |
37 |
38 |
39 |
40 | Add OpenAPI specification generation
41 |
42 | The ApplicationBuilder instance
43 | The APIVersionDescriptionProvider
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Extensions/IApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Swashbuckle.AspNetCore.SwaggerUI;
3 |
4 | namespace Demo.Microservices.Orders.API.Extensions
5 | {
6 | ///
7 | /// Provides an Application Builder extension for the Swagger/OpenAPI integration
8 | ///
9 | public static class IApplicationBuilderExtensions
10 | {
11 | ///
12 | /// Add OpenAPI specification generation
13 | ///
14 | /// The ApplicationBuilder instance
15 | /// The APIVersionDescriptionProvider
16 | public static void UseOpenApiUi(this IApplicationBuilder app)
17 | {
18 | app.UseSwagger();
19 | app.UseSwaggerUI(swaggerUiOptions =>
20 | {
21 | swaggerUiOptions.SwaggerEndpoint("/swagger/v1/swagger.json", "Contoso - Orders API");
22 |
23 | swaggerUiOptions.RoutePrefix = "api/docs";
24 | swaggerUiOptions.DisplayOperationId();
25 | swaggerUiOptions.EnableDeepLinking();
26 | swaggerUiOptions.DocExpansion(DocExpansion.List);
27 | swaggerUiOptions.DisplayRequestDuration();
28 | swaggerUiOptions.EnableFilter();
29 | });
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Extensions/IServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Swashbuckle.AspNetCore.Swagger;
6 |
7 | namespace Demo.Microservices.Orders.API.Extensions
8 | {
9 | public static class IServiceCollectionExtensions
10 | {
11 | public static void UseOpenApiSpecifications(this IServiceCollection services)
12 | {
13 | var xmlDocumentationPath = GetXmlDocumentationPath(services);
14 | var openApiInformation = new Info
15 | {
16 | Contact = new Contact
17 | {
18 | Name = "Contoso",
19 | Url = "https://codit.eu"
20 | },
21 | Title = $"Contoso - Orders API",
22 | Description = $"Orders APIs of the Contoso platform",
23 | Version = "v1"
24 | };
25 |
26 | services.AddSwaggerGen(swaggerGenerationOptions =>
27 | {
28 | swaggerGenerationOptions.EnableAnnotations();
29 | swaggerGenerationOptions.SwaggerDoc("v1", openApiInformation);
30 |
31 | swaggerGenerationOptions.DescribeAllEnumsAsStrings();
32 |
33 | if (string.IsNullOrEmpty(xmlDocumentationPath) == false)
34 | {
35 | swaggerGenerationOptions.IncludeXmlComments(xmlDocumentationPath);
36 | }
37 | });
38 | }
39 |
40 | private static Info CreateApiInformation(string microserviceName)
41 | {
42 | var openApiInformation = new Info
43 | {
44 | Contact = new Contact
45 | {
46 | Name = "Contoso",
47 | Url = "https://codit.eu"
48 | },
49 | Title = $"Contoso - {microserviceName} API",
50 | Description = $"{microserviceName} APIs of the Contoso platform",
51 | Version = "v1"
52 | };
53 | return openApiInformation;
54 | }
55 |
56 | private static string GetXmlDocumentationPath(IServiceCollection services)
57 | {
58 | var hostingEnvironment = services.FirstOrDefault(service => service.ServiceType == typeof(IHostingEnvironment));
59 | if (hostingEnvironment == null)
60 | {
61 | return string.Empty;
62 | }
63 |
64 | var contentRootPath = ((IHostingEnvironment)hostingEnvironment.ImplementationInstance).ContentRootPath;
65 | var xmlDocumentationPath = $"{contentRootPath}/Docs/Open-Api.xml";
66 |
67 | return File.Exists(xmlDocumentationPath) ? xmlDocumentationPath : string.Empty;
68 | }
69 |
70 | }
71 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Managers/OrderManager.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Microservices.Orders.API.Contracts.v1;
3 | using Demo.Microservices.Orders.API.Repositories.Interfaces;
4 | using Demo.Microservices.Orders.API.Services.Interfaces;
5 |
6 | namespace Demo.Microservices.Orders.API.Managers
7 | {
8 | public class OrderManager
9 | {
10 | private readonly IOrderRepository _orderRepository;
11 | private readonly IShipmentService _shipmentService;
12 |
13 | public OrderManager(IOrderRepository orderRepository, IShipmentService shipmentService)
14 | {
15 | _orderRepository = orderRepository;
16 | _shipmentService = shipmentService;
17 | }
18 |
19 | public async Task CreateAsync(Order createdOrder)
20 | {
21 | var confirmationId = await _orderRepository.CreateAsync(createdOrder);
22 | var shipmentInformation = await _shipmentService.CreateAsync(createdOrder.Customer.Address);
23 |
24 | var orderConfirmation = new OrderConfirmation
25 | {
26 | ConfirmationId = confirmationId,
27 | ShipmentInformation = shipmentInformation,
28 | Customer = createdOrder.Customer
29 | };
30 |
31 | return orderConfirmation;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace Demo.Microservices.Orders.API
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateWebHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
14 | WebHost.CreateDefaultBuilder(args)
15 | .UseStartup();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Repositories/InMemory/InMemoryOrderRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Demo.Microservices.Orders.API.Contracts.v1;
5 | using Demo.Microservices.Orders.API.Repositories.Interfaces;
6 |
7 | namespace Demo.Microservices.Orders.API.Repositories.InMemory
8 | {
9 | public class InMemoryOrderRepository : IOrderRepository
10 | {
11 | private readonly Dictionary _orders = new Dictionary();
12 |
13 | public Task CreateAsync(Order createdOrder)
14 | {
15 | var confirmationId = Guid.NewGuid().ToString();
16 |
17 | _orders.Add(confirmationId, createdOrder);
18 |
19 | return Task.FromResult(confirmationId);
20 | }
21 |
22 | public Task GetAsync(string confirmationId)
23 | {
24 | if(_orders.TryGetValue(confirmationId, out Order createdOrder))
25 | {
26 | return Task.FromResult(createdOrder);
27 | }
28 |
29 | return Task.FromResult(null);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Repositories/InMemory/OrderTableRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Demo.Microservices.Orders.API.Contracts.v1;
6 | using Demo.Microservices.Orders.API.Data.Contracts.v1;
7 | using Demo.Microservices.Orders.API.Data.Providers;
8 | using Demo.Microservices.Orders.API.Repositories.Interfaces;
9 | using Newtonsoft.Json;
10 |
11 | namespace Demo.Microservices.Orders.API.Repositories.InMemory
12 | {
13 | public class OrderTableRepository : IOrderRepository
14 | {
15 | private readonly TableStorageAccessor _tableStorageAccessor;
16 | private const string TableName = "orders";
17 |
18 | public OrderTableRepository(TableStorageAccessor tableStorageAccessor)
19 | {
20 | _tableStorageAccessor = tableStorageAccessor;
21 | }
22 |
23 | public async Task CreateAsync(Order createdOrder)
24 | {
25 | var confirmationId = Guid.NewGuid().ToString();
26 |
27 | var orderTableEntity = new OrderTableEntity
28 | {
29 | PartitionKey = confirmationId,
30 | RowKey = DateTimeOffset.UtcNow.ToString("s"),
31 | ConfirmationId = confirmationId,
32 | CustomerFirstName = createdOrder.Customer.FirstName,
33 | CustomerLastName = createdOrder.Customer.LastName,
34 | Basket = JsonConvert.SerializeObject(createdOrder.Basket),
35 | CustomerAddress = JsonConvert.SerializeObject(createdOrder.Customer.Address)
36 | };
37 |
38 | await _tableStorageAccessor.PersistAsync(TableName, orderTableEntity);
39 |
40 | return confirmationId;
41 | }
42 |
43 | public async Task GetAsync(string confirmationId)
44 | {
45 | var persistedOrderUpdates = await _tableStorageAccessor.GetAsync(TableName, confirmationId);
46 |
47 | Order order = null;
48 | if (persistedOrderUpdates.Any())
49 | {
50 | var mostRecentOrderUpdate = persistedOrderUpdates.OrderByDescending(orderUpdate => orderUpdate.RowKey).First();
51 |
52 | order = new Order
53 | {
54 | Basket = JsonConvert.DeserializeObject>(mostRecentOrderUpdate.Basket),
55 | Customer = new Customer
56 | {
57 | FirstName = mostRecentOrderUpdate.CustomerFirstName,
58 | LastName = mostRecentOrderUpdate.CustomerLastName
59 | }
60 | };
61 |
62 | if (mostRecentOrderUpdate.CustomerAddress != null)
63 | {
64 | order.Customer.Address = JsonConvert.DeserializeObject(mostRecentOrderUpdate.CustomerAddress);
65 | }
66 | }
67 |
68 | return order;
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Repositories/Interfaces/IOrderRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Microservices.Orders.API.Contracts.v1;
3 |
4 | namespace Demo.Microservices.Orders.API.Repositories.Interfaces
5 | {
6 | public interface IOrderRepository
7 | {
8 | Task GetAsync(string confirmationId);
9 | Task CreateAsync(Order createdOrder);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Services/Interfaces/IShipmentService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Microservices.Orders.API.Contracts.v1;
3 |
4 | namespace Demo.Microservices.Orders.API.Services.Interfaces
5 | {
6 | public interface IShipmentService
7 | {
8 | Task CreateAsync(Address address);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Services/ShipmentService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Demo.Microservices.Orders.API.Contracts.v1;
7 | using Demo.Microservices.Orders.API.Services.Interfaces;
8 | using Microsoft.Extensions.Configuration;
9 | using Newtonsoft.Json;
10 |
11 | namespace Demo.Microservices.Orders.API.Services
12 | {
13 | public class ShipmentService : IShipmentService
14 | {
15 | private readonly HttpClient _httpClient = new HttpClient();
16 | private readonly IConfiguration _configuration;
17 |
18 | public ShipmentService(IConfiguration configuration)
19 | {
20 | _configuration = configuration;
21 | }
22 |
23 | public async Task CreateAsync(Address address)
24 | {
25 | var shipmentBaseUri = _configuration["SHIPMENTS_API_URI"];
26 | var request = new HttpRequestMessage(HttpMethod.Post, shipmentBaseUri);
27 |
28 | var rawBody = JsonConvert.SerializeObject(address);
29 | request.Content = new StringContent(rawBody,Encoding.UTF8, "application/json");
30 |
31 | var shipmentApiKey = _configuration["SHIPMENTS_API_KEY"];
32 | if (!string.IsNullOrWhiteSpace(shipmentApiKey))
33 | {
34 | request.Headers.Add("API-Key", shipmentApiKey);
35 | }
36 |
37 | var response = await _httpClient.SendAsync(request);
38 | if (response.StatusCode != HttpStatusCode.Created)
39 | {
40 | throw new Exception("Unable to initiate a shipment");
41 | }
42 |
43 | var rawResponse = await response.Content.ReadAsStringAsync();
44 | return JsonConvert.DeserializeObject(rawResponse);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/Startup.cs:
--------------------------------------------------------------------------------
1 | using Arcus.WebApi.Logging;
2 | using Demo.Microservices.Orders.API.Data.Providers;
3 | using Demo.Microservices.Orders.API.Extensions;
4 | using Demo.Microservices.Orders.API.Managers;
5 | using Demo.Microservices.Orders.API.Repositories.InMemory;
6 | using Demo.Microservices.Orders.API.Repositories.Interfaces;
7 | using Demo.Microservices.Orders.API.Services;
8 | using Microsoft.AspNetCore.Builder;
9 | using Microsoft.AspNetCore.Hosting;
10 | using Microsoft.AspNetCore.Mvc;
11 | using Microsoft.Extensions.Configuration;
12 | using Microsoft.Extensions.DependencyInjection;
13 | using Newtonsoft.Json;
14 | using IShipmentService = Demo.Microservices.Orders.API.Services.Interfaces.IShipmentService;
15 |
16 | namespace Demo.Microservices.Orders.API
17 | {
18 | public class Startup
19 | {
20 | public Startup(IConfiguration configuration)
21 | {
22 | Configuration = configuration;
23 | }
24 |
25 | public IConfiguration Configuration { get; }
26 |
27 | // This method gets called by the runtime. Use this method to add services to the container.
28 | public void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddMvc()
31 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
32 | .AddJsonOptions(options =>
33 | {
34 | options.SerializerSettings.Formatting = Formatting.Indented;
35 | options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
36 | });
37 |
38 | services.AddScoped();
39 | services.AddScoped();
40 | services.AddScoped();
41 | services.AddScoped();
42 |
43 | services.UseOpenApiSpecifications();
44 | }
45 |
46 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
47 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
48 | {
49 | if (env.IsDevelopment())
50 | {
51 | app.UseDeveloperExceptionPage();
52 | }
53 | else
54 | {
55 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
56 | app.UseHsts();
57 | }
58 |
59 | app.UseHttpsRedirection();
60 | app.UseMiddleware();
61 | app.UseMvc();
62 | app.UseOpenApiUi();
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Orders.API/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Contracts/v1/Product.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Products.API.Contracts.v1
2 | {
3 | public class Product
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; }
7 | public double Price { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Controllers/HealthController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 |
4 | namespace Demo.Microservices.Products.API.Controllers
5 | {
6 | [Route("api/v1/health")]
7 | [ApiController]
8 | public class HealthController : ControllerBase
9 | {
10 | ///
11 | /// Get Health
12 | ///
13 | /// Provides an indication about the health of the scraper
14 | /// API is healthy
15 | /// API is not healthy
16 | [HttpGet]
17 | [SwaggerOperation(OperationId = "Health_Get")]
18 | public IActionResult Get()
19 | {
20 | return Ok();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Controllers/ProductsController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Threading.Tasks;
3 | using System.Collections.Generic;
4 | using Demo.Microservices.Products.API.Contracts.v1;
5 | using Demo.Microservices.Products.API.Repositories.Interfaces;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Swashbuckle.AspNetCore.Annotations;
8 |
9 | namespace Demo.Microservices.Products.API.Controllers
10 | {
11 | [Route("api/v1/products")]
12 | public class ProductsController : Controller
13 | {
14 | private readonly IProductRepository _productRepository;
15 |
16 | public ProductsController(IProductRepository productRepository)
17 | {
18 | _productRepository = productRepository;
19 | }
20 |
21 | ///
22 | /// Get Products
23 | ///
24 | /// Provides information about product catalog
25 | /// Overview of our product catalog
26 | /// Something went wrong, please contact support
27 | [HttpGet]
28 | [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)]
29 | [SwaggerOperation(OperationId = "Product_GetAll")]
30 | public async Task Get()
31 | {
32 | var products = await _productRepository.GetAsync();
33 | return Ok(products);
34 | }
35 |
36 | ///
37 | /// Get Product
38 | ///
39 | /// Provides information about a specific product in our catalog
40 | /// Information about a specific product
41 | /// Request was invalid
42 | /// Requested product was not found
43 | /// Something went wrong, please contact support
44 | [HttpGet("{id}")]
45 | [ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
46 | [SwaggerOperation(OperationId = "Product_Get")]
47 | public async Task Get(int id)
48 | {
49 | var foundProduct = await _productRepository.GetAsync(id);
50 | if (foundProduct == null)
51 | {
52 | return NotFound();
53 | }
54 |
55 | return Ok(foundProduct);
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Data/Contracts/v1/ProductTableEntity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos.Table;
2 |
3 | namespace Demo.Microservices.Products.API.Data.Contracts.v1
4 | {
5 | public class ProductTableEntity : TableEntity
6 | {
7 | public int Id { get; set; }
8 | public string Name { get; set; }
9 | public double Price { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Data/Providers/TableStorageAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using Microsoft.Azure.Cosmos.Table;
7 | using Microsoft.Extensions.Configuration;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace Demo.Microservices.Products.API.Data.Providers
11 | {
12 | public class TableStorageAccessor
13 | {
14 | private readonly CloudTableClient _tableClient;
15 | private readonly ILogger _logger;
16 |
17 | public TableStorageAccessor(IConfiguration configuration, ILogger logger)
18 | {
19 | _logger = logger;
20 | _tableClient = CreateTableClient(configuration);
21 | }
22 |
23 | public async Task GetAsync(string tableName, string partitionKey, string rowKey)
24 | where TEntity : TableEntity
25 | {
26 | var table = await GetTableAsync(tableName);
27 |
28 | var retrieve = TableOperation.Retrieve(partitionKey, rowKey);
29 | var tableOperationResult = await table.ExecuteAsync(retrieve);
30 |
31 | switch (tableOperationResult.HttpStatusCode)
32 | {
33 | case (int)HttpStatusCode.OK:
34 | return (TEntity)tableOperationResult.Result;
35 | case (int)HttpStatusCode.NotFound:
36 | return null;
37 | default:
38 | throw new Exception($"Failed to look up table entity with partition key '{partitionKey}' and row key '{rowKey}' from table '{tableName}'");
39 | }
40 | }
41 |
42 | public async Task> GetAsync(string tableName, string partitionKey)
43 | where TEntity : TableEntity, new()
44 | {
45 | var table = await GetTableAsync(tableName);
46 |
47 | var query = new TableQuery()
48 | .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
49 |
50 | var foundItems = table.ExecuteQuery(query);
51 | return foundItems.ToList();
52 | }
53 |
54 | private async Task GetTableAsync(string tableName)
55 | {
56 | CloudTable table = _tableClient.GetTableReference(tableName);
57 | await table.CreateIfNotExistsAsync();
58 |
59 | return table;
60 | }
61 |
62 | private CloudTableClient CreateTableClient(IConfiguration configuration)
63 | {
64 | var tableConnectionString = configuration["AZURESTORAGE_CONNECTIONSTRING"];
65 | var storageAccount = CloudStorageAccount.Parse(tableConnectionString);
66 | _logger.LogInformation($"Connecting to Azure Storage Account '{storageAccount.Credentials.AccountName}'");
67 |
68 | return storageAccount.CreateCloudTableClient();
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Demo.Microservices.Products.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 | InProcess
6 | Linux
7 | ..\docker-compose.dcproj
8 |
9 |
10 |
11 | Docs/Open-Api.xml
12 |
13 |
14 |
15 | Docs/Open-Api.xml
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base
2 | WORKDIR /app
3 | EXPOSE 80
4 |
5 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build
6 | WORKDIR /src
7 | COPY ["Demo.Microservices.Products.API/Demo.Microservices.Products.API.csproj", "Demo.Microservices.Products.API/"]
8 | RUN dotnet restore "Demo.Microservices.Products.API/Demo.Microservices.Products.API.csproj"
9 | COPY . .
10 | WORKDIR "/src/Demo.Microservices.Products.API"
11 | RUN dotnet build "Demo.Microservices.Products.API.csproj" -c Release -o /app
12 |
13 | FROM build AS publish
14 | RUN dotnet publish "Demo.Microservices.Products.API.csproj" -c Release -o /app
15 |
16 | FROM base AS final
17 | WORKDIR /app
18 | COPY --from=publish /app .
19 | ENTRYPOINT ["dotnet", "Demo.Microservices.Products.API.dll"]
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Docs/Open-Api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo.Microservices.Products.API
5 |
6 |
7 |
8 |
9 | Get Health
10 |
11 | Provides an indication about the health of the scraper
12 | API is healthy
13 | API is not healthy
14 |
15 |
16 |
17 | Get Products
18 |
19 | Provides information about product catalog
20 | Overview of our product catalog
21 | Something went wrong, please contact support
22 |
23 |
24 |
25 | Get Product
26 |
27 | Provides information about a specific product in our catalog
28 | Information about a specific product
29 | Request was invalid
30 | Requested product was not found
31 | Something went wrong, please contact support
32 |
33 |
34 |
35 | Provides an Application Builder extension for the Swagger/OpenAPI integration
36 |
37 |
38 |
39 |
40 | Add OpenAPI specification generation
41 |
42 | The ApplicationBuilder instance
43 | The APIVersionDescriptionProvider
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Extensions/IApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Swashbuckle.AspNetCore.SwaggerUI;
3 |
4 | namespace Demo.Microservices.Products.API.Extensions
5 | {
6 | ///
7 | /// Provides an Application Builder extension for the Swagger/OpenAPI integration
8 | ///
9 | public static class IApplicationBuilderExtensions
10 | {
11 | ///
12 | /// Add OpenAPI specification generation
13 | ///
14 | /// The ApplicationBuilder instance
15 | /// The APIVersionDescriptionProvider
16 | public static void UseOpenApiUi(this IApplicationBuilder app)
17 | {
18 | app.UseSwagger();
19 | app.UseSwaggerUI(swaggerUiOptions =>
20 | {
21 | swaggerUiOptions.SwaggerEndpoint("/swagger/v1/swagger.json", "Contoso - Products API");
22 |
23 | swaggerUiOptions.RoutePrefix = "api/docs";
24 | swaggerUiOptions.DisplayOperationId();
25 | swaggerUiOptions.EnableDeepLinking();
26 | swaggerUiOptions.DocExpansion(DocExpansion.List);
27 | swaggerUiOptions.DisplayRequestDuration();
28 | swaggerUiOptions.EnableFilter();
29 | });
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Extensions/IServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Swashbuckle.AspNetCore.Swagger;
6 |
7 | namespace Demo.Microservices.Products.API.Extensions
8 | {
9 | public static class IServiceCollectionExtensions
10 | {
11 | public static void UseOpenApiSpecifications(this IServiceCollection services)
12 | {
13 | var xmlDocumentationPath = GetXmlDocumentationPath(services);
14 | var openApiInformation = new Info
15 | {
16 | Contact = new Contact
17 | {
18 | Name = "Contoso",
19 | Url = "https://codit.eu"
20 | },
21 | Title = $"Contoso - Product API",
22 | Description = $"Product APIs of the Contoso platform",
23 | Version = "v1"
24 | };
25 |
26 | services.AddSwaggerGen(swaggerGenerationOptions =>
27 | {
28 | swaggerGenerationOptions.EnableAnnotations();
29 | swaggerGenerationOptions.SwaggerDoc("v1", openApiInformation);
30 |
31 | swaggerGenerationOptions.DescribeAllEnumsAsStrings();
32 |
33 | if (string.IsNullOrEmpty(xmlDocumentationPath) == false)
34 | {
35 | swaggerGenerationOptions.IncludeXmlComments(xmlDocumentationPath);
36 | }
37 | });
38 | }
39 |
40 | private static string GetXmlDocumentationPath(IServiceCollection services)
41 | {
42 | var hostingEnvironment = services.FirstOrDefault(service => service.ServiceType == typeof(IHostingEnvironment));
43 | if (hostingEnvironment == null)
44 | {
45 | return string.Empty;
46 | }
47 |
48 | var contentRootPath = ((IHostingEnvironment)hostingEnvironment.ImplementationInstance).ContentRootPath;
49 | var xmlDocumentationPath = $"{contentRootPath}/Docs/Open-Api.xml";
50 |
51 | return File.Exists(xmlDocumentationPath) ? xmlDocumentationPath : string.Empty;
52 | }
53 |
54 | }
55 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace Demo.Microservices.Products.API
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateWebHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
14 | WebHost.CreateDefaultBuilder(args)
15 | .UseStartup();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Repositories/InMemory/InMemoryProductRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Demo.Microservices.Products.API.Contracts.v1;
5 | using Demo.Microservices.Products.API.Repositories.Interfaces;
6 |
7 | namespace Demo.Microservices.Products.API.Repositories.InMemory
8 | {
9 | public class InMemoryProductRepository : IProductRepository
10 | {
11 | private readonly List _availableProducts = new List
12 | {
13 | new Product {Id = 1, Name = "Surface Go", Price = 399},
14 | new Product {Id = 2, Name = "Surface Pro 6", Price = 799},
15 | new Product {Id = 3, Name = "Surface Book 2", Price = 1049},
16 | new Product {Id = 4, Name = "Surface Laptop 2", Price = 1199},
17 | new Product {Id = 5, Name = "Surface Studio 2", Price = 3499}
18 | };
19 |
20 | public Task GetAsync(int id)
21 | {
22 | var foundProduct = _availableProducts.SingleOrDefault(product => product.Id == id);
23 | return Task.FromResult(foundProduct);
24 | }
25 |
26 | public Task> GetAsync()
27 | {
28 | return Task.FromResult(_availableProducts);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Repositories/InMemory/ProductTableRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Demo.Microservices.Products.API.Contracts.v1;
5 | using Demo.Microservices.Products.API.Data.Contracts.v1;
6 | using Demo.Microservices.Products.API.Data.Providers;
7 | using Demo.Microservices.Products.API.Repositories.Interfaces;
8 |
9 | namespace Demo.Microservices.Products.API.Repositories.InMemory
10 | {
11 | public class ProductTableRepository : IProductRepository
12 | {
13 | private readonly TableStorageAccessor _tableStorageAccessor;
14 | private const string TableName = "products";
15 |
16 | public ProductTableRepository(TableStorageAccessor tableStorageAccessor)
17 | {
18 | _tableStorageAccessor = tableStorageAccessor;
19 | }
20 |
21 | public async Task> GetAsync()
22 | {
23 | var products = await _tableStorageAccessor.GetAsync(TableName, "products");
24 | return products.Select(MapToContract).ToList();
25 | }
26 |
27 | public async Task GetAsync(int id)
28 | {
29 | var persistedProduct = await _tableStorageAccessor.GetAsync(TableName, "products", id.ToString());
30 |
31 | Product order = null;
32 | if (persistedProduct != null)
33 | {
34 | order = MapToContract(persistedProduct);
35 | }
36 |
37 | return order;
38 | }
39 |
40 | private Product MapToContract(ProductTableEntity productTableEntity)
41 | {
42 | return new Product
43 | {
44 | Id = productTableEntity.Id,
45 | Name = productTableEntity.Name,
46 | Price = productTableEntity.Price,
47 | };
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Repositories/Interfaces/IProductRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Demo.Microservices.Products.API.Contracts.v1;
4 |
5 | namespace Demo.Microservices.Products.API.Repositories.Interfaces
6 | {
7 | public interface IProductRepository
8 | {
9 | Task GetAsync(int id);
10 | Task> GetAsync();
11 | }
12 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/Startup.cs:
--------------------------------------------------------------------------------
1 | using Arcus.WebApi.Logging;
2 | using Demo.Microservices.Products.API.Data.Providers;
3 | using Demo.Microservices.Products.API.Extensions;
4 | using Demo.Microservices.Products.API.Repositories.InMemory;
5 | using Demo.Microservices.Products.API.Repositories.Interfaces;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Newtonsoft.Json;
12 |
13 | namespace Demo.Microservices.Products.API
14 | {
15 | public class Startup
16 | {
17 | public Startup(IConfiguration configuration)
18 | {
19 | Configuration = configuration;
20 | }
21 |
22 | public IConfiguration Configuration { get; }
23 |
24 | // This method gets called by the runtime. Use this method to add services to the container.
25 | public void ConfigureServices(IServiceCollection services)
26 | {
27 | services.AddMvc()
28 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
29 | .AddJsonOptions(options =>
30 | {
31 | options.SerializerSettings.Formatting = Formatting.Indented;
32 | options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
33 | });
34 |
35 | services.AddScoped();
36 | services.AddScoped();
37 |
38 | services.UseOpenApiSpecifications();
39 | }
40 |
41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
42 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
43 | {
44 | if (env.IsDevelopment())
45 | {
46 | app.UseDeveloperExceptionPage();
47 | }
48 | else
49 | {
50 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
51 | app.UseHsts();
52 | }
53 |
54 | app.UseHttpsRedirection();
55 | app.UseMiddleware();
56 | app.UseMvc();
57 | app.UseOpenApiUi();
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Products.API/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Controllers/HealthController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 |
4 | namespace Demo.Microservices.Shipments.API.Controllers
5 | {
6 | [Route("api/v1/health")]
7 | [ApiController]
8 | public class HealthController : ControllerBase
9 | {
10 | ///
11 | /// Get Health
12 | ///
13 | /// Provides an indication about the health of the scraper
14 | /// API is healthy
15 | /// API is not healthy
16 | [HttpGet]
17 | [SwaggerOperation(OperationId = "Health_Get")]
18 | public IActionResult Get()
19 | {
20 | return Ok();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Controllers/ShipmentsController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Threading.Tasks;
3 | using Demo.Microservices.Shipments.API.OpenAPI;
4 | using Demo.Microservices.Shipments.Contracts.v1;
5 | using Demo.Microservices.Shipments.Data.Repositories.Interfaces;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.Extensions.Logging;
8 | using Swashbuckle.AspNetCore.Annotations;
9 |
10 | namespace Demo.Microservices.Shipments.API.Controllers
11 | {
12 | [Route("api/v1/shipments")]
13 | [ApiController]
14 | [ApiExplorerSettings(GroupName = OpenApiCategories.Shipments)]
15 | public class ShipmentsController : ControllerBase
16 | {
17 | private readonly IShipmentRepository _shipmentRepository;
18 | private readonly ILogger _logger;
19 |
20 | public ShipmentsController(IShipmentRepository shipmentRepository, ILogger logger)
21 | {
22 | _logger = logger;
23 | _shipmentRepository = shipmentRepository;
24 | }
25 |
26 | ///
27 | /// Get Shipment Information
28 | ///
29 | /// Provides information about a shipment
30 | /// Information about a specific shipment
31 | /// Request was invalid
32 | /// Requested product was not found
33 | /// Something went wrong, please contact support
34 | [HttpGet("{trackingNumber}")]
35 | [ProducesResponseType(typeof(ShipmentInformation), (int)HttpStatusCode.OK)]
36 | [SwaggerOperation(OperationId = "Shipment_Get")]
37 | public async Task Get(string trackingNumber)
38 | {
39 | var shipmentInformation = await _shipmentRepository.GetAsync(trackingNumber);
40 | if (shipmentInformation == null)
41 | {
42 | _logger.LogInformation("No information found for shipment {TrackingNumber} ", trackingNumber);
43 |
44 | return NotFound();
45 | }
46 |
47 | _logger.LogInformation("Information found for shipment {TrackingNumber} ", trackingNumber);
48 |
49 | return Ok(shipmentInformation);
50 | }
51 |
52 | ///
53 | /// Create New Shipment
54 | ///
55 | /// Creates a new request to ship to an address
56 | /// Shipment was initiated
57 | /// Request was invalid
58 | /// Requested product was not found
59 | /// Something went wrong, please contact support
60 | [HttpPost]
61 | [SwaggerOperation(OperationId = "Shipment_Create")]
62 | [ProducesResponseType(typeof(ShipmentInformation), (int)HttpStatusCode.Created)]
63 | [ApiExplorerSettings(GroupName = OpenApiCategories.ShipmentManagement)]
64 | public async Task Create([FromBody] Address address)
65 | {
66 | var shipmentInformation = await _shipmentRepository.CreateAsync(address);
67 |
68 | _logger.LogInformation("Shipment {TrackingNumber} was created to deliver at {DeliverAddress}", shipmentInformation.TrackingNumber, shipmentInformation.DeliveryAddress);
69 |
70 | return CreatedAtAction(nameof(Get), new { trackingNumber = shipmentInformation.TrackingNumber }, shipmentInformation);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Demo.Microservices.Shipments.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 | InProcess
6 | Linux
7 | ..\docker-compose.dcproj
8 |
9 |
10 |
11 | Docs/Open-Api.xml
12 |
13 |
14 |
15 | Docs/Open-Api.xml
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base
2 | WORKDIR /app
3 | EXPOSE 80
4 |
5 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build
6 | WORKDIR /src
7 | COPY ["Demo.Microservices.Shipments.API/Demo.Microservices.Shipments.API.csproj", "Demo.Microservices.Shipments.API/"]
8 | RUN dotnet restore "Demo.Microservices.Shipments.API/Demo.Microservices.Shipments.API.csproj"
9 | COPY . .
10 | WORKDIR "/src/Demo.Microservices.Shipments.API"
11 | RUN dotnet build "Demo.Microservices.Shipments.API.csproj" -c Release -o /app
12 |
13 | FROM build AS publish
14 | RUN dotnet publish "Demo.Microservices.Shipments.API.csproj" -c Release -o /app
15 |
16 | FROM base AS final
17 | WORKDIR /app
18 | COPY --from=publish /app .
19 | ENTRYPOINT ["dotnet", "Demo.Microservices.Shipments.API.dll"]
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Docs/Open-Api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo.Microservices.Shipments.API
5 |
6 |
7 |
8 |
9 | Get Health
10 |
11 | Provides an indication about the health of the scraper
12 | API is healthy
13 | API is not healthy
14 |
15 |
16 |
17 | Get Shipment Information
18 |
19 | Provides information about a shipment
20 | Information about a specific shipment
21 | Request was invalid
22 | Requested product was not found
23 | Something went wrong, please contact support
24 |
25 |
26 |
27 | Create New Shipment
28 |
29 | Creates a new request to ship to an address
30 | Shipment was initiated
31 | Request was invalid
32 | Requested product was not found
33 | Something went wrong, please contact support
34 |
35 |
36 |
37 | Provides an Application Builder extension for the Swagger/OpenAPI integration
38 |
39 |
40 |
41 |
42 | Add OpenAPI specification generation
43 |
44 | The ApplicationBuilder instance
45 | The APIVersionDescriptionProvider
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Extensions/IApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Demo.Microservices.Shipments.API.OpenAPI;
2 | using Microsoft.AspNetCore.Builder;
3 | using Swashbuckle.AspNetCore.SwaggerUI;
4 |
5 | namespace Demo.Microservices.Shipments.API.Extensions
6 | {
7 | ///
8 | /// Provides an Application Builder extension for the Swagger/OpenAPI integration
9 | ///
10 | public static class IApplicationBuilderExtensions
11 | {
12 | ///
13 | /// Add OpenAPI specification generation
14 | ///
15 | /// The ApplicationBuilder instance
16 | /// The APIVersionDescriptionProvider
17 | public static void UseOpenApiUi(this IApplicationBuilder app)
18 | {
19 | app.UseSwagger();
20 | app.UseSwaggerUI(swaggerUiOptions =>
21 | {
22 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.Shipments}/swagger.json", "Contoso - Shipments API");
23 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.ShipmentManagement}/swagger.json", "Contoso - Shipment Management API");
24 |
25 | swaggerUiOptions.RoutePrefix = "api/docs";
26 | swaggerUiOptions.DisplayOperationId();
27 | swaggerUiOptions.EnableDeepLinking();
28 | swaggerUiOptions.DocExpansion(DocExpansion.List);
29 | swaggerUiOptions.DisplayRequestDuration();
30 | swaggerUiOptions.EnableFilter();
31 | });
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Extensions/IServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using Demo.Microservices.Shipments.API.OpenAPI;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Swashbuckle.AspNetCore.Swagger;
7 |
8 | namespace Demo.Microservices.Shipments.API.Extensions
9 | {
10 | public static class IServiceCollectionExtensions
11 | {
12 | public static void UseOpenApiSpecifications(this IServiceCollection services)
13 | {
14 | var xmlDocumentationPath = GetXmlDocumentationPath(services);
15 |
16 | services.AddSwaggerGen(swaggerGenerationOptions =>
17 | {
18 | swaggerGenerationOptions.EnableAnnotations();
19 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.Shipments, CreateApiInformation("Shipments"));
20 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.ShipmentManagement, CreateApiInformation("Shipment Management"));
21 |
22 | swaggerGenerationOptions.DescribeAllEnumsAsStrings();
23 |
24 | if (string.IsNullOrEmpty(xmlDocumentationPath) == false)
25 | {
26 | swaggerGenerationOptions.IncludeXmlComments(xmlDocumentationPath);
27 | }
28 | });
29 | }
30 |
31 | private static Info CreateApiInformation(string microserviceName)
32 | {
33 | var openApiInformation = new Info
34 | {
35 | Contact = new Contact
36 | {
37 | Name = "Contoso",
38 | Url = "https://codit.eu"
39 | },
40 | Title = $"Contoso - {microserviceName} API",
41 | Description = $"{microserviceName} APIs of the Contoso platform",
42 | Version = "v1"
43 | };
44 | return openApiInformation;
45 | }
46 |
47 | private static string GetXmlDocumentationPath(IServiceCollection services)
48 | {
49 | var hostingEnvironment = services.FirstOrDefault(service => service.ServiceType == typeof(IHostingEnvironment));
50 | if (hostingEnvironment == null)
51 | {
52 | return string.Empty;
53 | }
54 |
55 | var contentRootPath = ((IHostingEnvironment)hostingEnvironment.ImplementationInstance).ContentRootPath;
56 | var xmlDocumentationPath = $"{contentRootPath}/Docs/Open-Api.xml";
57 |
58 | return File.Exists(xmlDocumentationPath) ? xmlDocumentationPath : string.Empty;
59 | }
60 |
61 | }
62 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/OpenAPI/OpenApiCategories.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Shipments.API.OpenAPI
2 | {
3 | public class OpenApiCategories
4 | {
5 | public const string Shipments = "shipments";
6 | public const string ShipmentManagement = "shipment_management";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace Demo.Microservices.Shipments.API
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateWebHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
14 | WebHost.CreateDefaultBuilder(args)
15 | .UseStartup();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/Startup.cs:
--------------------------------------------------------------------------------
1 | using Arcus.WebApi.Logging;
2 | using Demo.Microservices.Shipments.API.Extensions;
3 | using Demo.Microservices.Shipments.Data.Providers;
4 | using Demo.Microservices.Shipments.Data.Repositories.InMemory;
5 | using Demo.Microservices.Shipments.Data.Repositories.Interfaces;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Newtonsoft.Json;
12 |
13 | namespace Demo.Microservices.Shipments.API
14 | {
15 | public class Startup
16 | {
17 | public Startup(IConfiguration configuration)
18 | {
19 | Configuration = configuration;
20 | }
21 |
22 | public IConfiguration Configuration { get; }
23 |
24 | // This method gets called by the runtime. Use this method to add services to the container.
25 | public void ConfigureServices(IServiceCollection services)
26 | {
27 | services.AddMvc()
28 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
29 | .AddJsonOptions(options =>
30 | {
31 | options.SerializerSettings.Formatting = Formatting.Indented;
32 | options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
33 | });
34 |
35 | services.AddScoped();
36 | services.AddScoped();
37 |
38 | services.UseOpenApiSpecifications();
39 | }
40 |
41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
42 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
43 | {
44 | if (env.IsDevelopment())
45 | {
46 | app.UseDeveloperExceptionPage();
47 | }
48 | else
49 | {
50 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
51 | app.UseHsts();
52 | }
53 |
54 | app.UseHttpsRedirection();
55 | app.UseMiddleware();
56 | app.UseMvc();
57 | app.UseOpenApiUi();
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.API/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Contracts/Demo.Microservices.Shipments.Contracts.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Contracts/v1/Address.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Shipments.Contracts.v1
2 | {
3 | public class Address
4 | {
5 | public string Street { get; set; }
6 | public string PostalCode { get; set; }
7 | public string State { get; set; }
8 | public string Country { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Contracts/v1/ShipmentInformation.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Shipments.Contracts.v1
2 | {
3 | public class ShipmentInformation
4 | {
5 | public string TrackingNumber { get; set; }
6 | public ShipmentStatus Status { get; set; }
7 | public Address DeliveryAddress { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Contracts/v1/ShipmentStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Shipments.Contracts.v1
2 | {
3 | public enum ShipmentStatus
4 | {
5 | AwaitingPickup,
6 | InTransit,
7 | Delivered
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Contracts/v1/ShipmentStatusUpdate.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Microservices.Shipments.Contracts.v1
2 | {
3 | public class ShipmentStatusUpdate
4 | {
5 | public string TrackingNumber { get; set; }
6 | public ShipmentStatus Status { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Contracts/v1/ShipmentTableEntity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos.Table;
2 |
3 | namespace Demo.Microservices.Shipments.Data.Contracts.v1
4 | {
5 | public class ShipmentTableEntity : TableEntity
6 | {
7 | public string TrackingNumber { get; set; }
8 | public string Status { get; set; }
9 | public string DeliveryAddress { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Demo.Microservices.Shipments.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Exceptions/ShipmentNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Demo.Microservices.Shipments.Data.Exceptions
4 | {
5 | public class ShipmentNotFoundException : Exception
6 | {
7 | public ShipmentNotFoundException(string trackingNumber)
8 | {
9 | TrackingNumber = trackingNumber;
10 | }
11 |
12 | public string TrackingNumber { get; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Providers/TableStorageAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using Microsoft.Azure.Cosmos.Table;
7 | using Microsoft.Extensions.Configuration;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace Demo.Microservices.Shipments.Data.Providers
11 | {
12 | public class TableStorageAccessor
13 | {
14 | private readonly CloudTableClient _tableClient;
15 | private readonly ILogger _logger;
16 |
17 | public TableStorageAccessor(IConfiguration configuration, ILogger logger)
18 | {
19 | _logger = logger;
20 | _tableClient = CreateTableClient(configuration);
21 | }
22 |
23 | public async Task PersistAsync(string tableName, TEntity entity)
24 | where TEntity : TableEntity
25 | {
26 | var table = await GetTableAsync(tableName);
27 |
28 | var insertOrMergeOperation = TableOperation.InsertOrMerge(entity);
29 | await table.ExecuteAsync(insertOrMergeOperation);
30 | }
31 |
32 | public async Task GetAsync(string tableName, string partitionKey, string rowKey)
33 | where TEntity : TableEntity
34 | {
35 | var table = await GetTableAsync(tableName);
36 |
37 | var retrieve = TableOperation.Retrieve(partitionKey, rowKey);
38 | var tableOperationResult = await table.ExecuteAsync(retrieve);
39 |
40 | switch (tableOperationResult.HttpStatusCode)
41 | {
42 | case (int)HttpStatusCode.OK:
43 | return (TEntity)tableOperationResult.Result;
44 | case (int)HttpStatusCode.NotFound:
45 | return null;
46 | default:
47 | throw new Exception($"Failed to look up table entity with partition key '{partitionKey}' and row key '{rowKey}' from table '{tableName}'");
48 | }
49 | }
50 |
51 | public async Task> GetAsync(string tableName, string partitionKey)
52 | where TEntity : TableEntity, new()
53 | {
54 | var table = await GetTableAsync(tableName);
55 |
56 | var query = new TableQuery()
57 | .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
58 |
59 | var foundItems = table.ExecuteQuery(query);
60 | return foundItems.ToList();
61 | }
62 |
63 | private async Task GetTableAsync(string tableName)
64 | {
65 | CloudTable table = _tableClient.GetTableReference(tableName);
66 | await table.CreateIfNotExistsAsync();
67 |
68 | return table;
69 | }
70 |
71 | private CloudTableClient CreateTableClient(IConfiguration configuration)
72 | {
73 | var tableConnectionString = configuration["AZURESTORAGE_CONNECTIONSTRING"];
74 | var storageAccount = CloudStorageAccount.Parse(tableConnectionString);
75 | _logger.LogInformation($"Connecting to Azure Storage Account '{storageAccount.Credentials.AccountName}'");
76 |
77 | return storageAccount.CreateCloudTableClient();
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Repositories/InMemory/InMemoryShipmentRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Demo.Microservices.Shipments.Contracts.v1;
5 | using Demo.Microservices.Shipments.Data.Exceptions;
6 | using Demo.Microservices.Shipments.Data.Repositories.Interfaces;
7 |
8 | namespace Demo.Microservices.Shipments.Data.Repositories.InMemory
9 | {
10 | public class InMemoryShipmentRepository : IShipmentRepository
11 | {
12 | private readonly Dictionary _shipments = new Dictionary();
13 |
14 | public Task CreateAsync(Address address)
15 | {
16 | var trackingNumber = Guid.NewGuid().ToString();
17 |
18 | var shipmentInformation = new ShipmentInformation
19 | {
20 | TrackingNumber = trackingNumber,
21 | DeliveryAddress = address,
22 | Status = ShipmentStatus.AwaitingPickup
23 | };
24 |
25 | _shipments.Add(trackingNumber, shipmentInformation);
26 |
27 | return Task.FromResult(shipmentInformation);
28 | }
29 |
30 | public Task GetAsync(string trackingNumber)
31 | {
32 | if (_shipments.TryGetValue(trackingNumber, out ShipmentInformation shipment))
33 | {
34 | return Task.FromResult(shipment);
35 | }
36 |
37 | return Task.FromResult(null);
38 | }
39 |
40 | public Task UpdateAsync(ShipmentStatusUpdate shipmentStatusUpdate)
41 | {
42 | if (!_shipments.TryGetValue(shipmentStatusUpdate.TrackingNumber, out var foundShipmentInformation))
43 | {
44 | throw new ShipmentNotFoundException(shipmentStatusUpdate.TrackingNumber);
45 | }
46 |
47 | foundShipmentInformation.Status = shipmentStatusUpdate.Status;
48 | _shipments[shipmentStatusUpdate.TrackingNumber] = foundShipmentInformation;
49 |
50 | return Task.CompletedTask;
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Repositories/InMemory/ShipmentsTableRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Demo.Microservices.Shipments.Contracts.v1;
5 | using Demo.Microservices.Shipments.Data.Contracts.v1;
6 | using Demo.Microservices.Shipments.Data.Providers;
7 | using Demo.Microservices.Shipments.Data.Repositories.Interfaces;
8 | using Newtonsoft.Json;
9 |
10 | namespace Demo.Microservices.Shipments.Data.Repositories.InMemory
11 | {
12 | public class ShipmentsTableRepository : IShipmentRepository
13 | {
14 | private readonly TableStorageAccessor _tableStorageAccessor;
15 | private const string TableName = "shipments";
16 |
17 | public ShipmentsTableRepository(TableStorageAccessor tableStorageAccessor)
18 | {
19 | _tableStorageAccessor = tableStorageAccessor;
20 | }
21 |
22 | public async Task CreateAsync(Address address)
23 | {
24 | var trackingNumber = Guid.NewGuid().ToString();
25 |
26 | var shipmentInformation = new ShipmentInformation
27 | {
28 | TrackingNumber = trackingNumber,
29 | DeliveryAddress = address,
30 | Status = ShipmentStatus.AwaitingPickup
31 | };
32 |
33 | var shipmentInformationTableEntry = MapToTableEntry(shipmentInformation);
34 | await _tableStorageAccessor.PersistAsync(TableName, shipmentInformationTableEntry);
35 |
36 | return shipmentInformation;
37 | }
38 |
39 | public async Task GetAsync(string trackingNumber)
40 | {
41 | var shipmentTableEntry = await GetShipmentTableEntryAsync(trackingNumber);
42 | if (shipmentTableEntry == null)
43 | {
44 | return null;
45 | }
46 |
47 | var shipmentInformation = MapToContract(shipmentTableEntry);
48 | return shipmentInformation;
49 | }
50 |
51 | public async Task UpdateAsync(ShipmentStatusUpdate shipmentStatusUpdate)
52 | {
53 | var shipmentTableEntry = await GetShipmentTableEntryAsync(shipmentStatusUpdate.TrackingNumber);
54 |
55 | shipmentTableEntry.RowKey = DateTimeOffset.UtcNow.ToString("s");
56 | shipmentTableEntry.Status = shipmentStatusUpdate.Status.ToString();
57 |
58 | await _tableStorageAccessor.PersistAsync(TableName, shipmentTableEntry);
59 | }
60 |
61 | private async Task GetShipmentTableEntryAsync(string trackingNumber)
62 | {
63 | var shipmentUpdates = await _tableStorageAccessor.GetAsync(TableName, trackingNumber);
64 | return shipmentUpdates.OrderByDescending(shipmentUpdate => shipmentUpdate.RowKey).FirstOrDefault();
65 | }
66 |
67 | private ShipmentInformation MapToContract(ShipmentTableEntity shipmentTableEntry)
68 | {
69 | var shipmentStatus = (ShipmentStatus)Enum.Parse(typeof(ShipmentStatus), shipmentTableEntry.Status);
70 |
71 | return new ShipmentInformation
72 | {
73 | TrackingNumber = shipmentTableEntry.TrackingNumber,
74 | DeliveryAddress = JsonConvert.DeserializeObject(shipmentTableEntry.DeliveryAddress),
75 | Status = shipmentStatus,
76 | };
77 | }
78 |
79 | private ShipmentTableEntity MapToTableEntry(ShipmentInformation shipmentInformation)
80 | {
81 | return new ShipmentTableEntity
82 | {
83 | PartitionKey = shipmentInformation.TrackingNumber,
84 | RowKey = DateTimeOffset.UtcNow.ToString("s"),
85 | TrackingNumber = shipmentInformation.TrackingNumber,
86 | DeliveryAddress = JsonConvert.SerializeObject(shipmentInformation.DeliveryAddress),
87 | Status = shipmentInformation.Status.ToString(),
88 | };
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Data/Repositories/Interfaces/IShipmentRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Microservices.Shipments.Contracts.v1;
3 |
4 | namespace Demo.Microservices.Shipments.Data.Repositories.Interfaces
5 | {
6 | public interface IShipmentRepository
7 | {
8 | Task CreateAsync(Address address);
9 | Task GetAsync(string trackingNumber);
10 | Task UpdateAsync(ShipmentStatusUpdate shipmentStatusUpdate);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Webhooks/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # Azure Functions localsettings file
5 | local.settings.json
6 |
7 | # User-specific files
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # NCrunch
117 | _NCrunch_*
118 | .*crunch*.local.xml
119 | nCrunchTemp_*
120 |
121 | # MightyMoose
122 | *.mm.*
123 | AutoTest.Net/
124 |
125 | # Web workbench (sass)
126 | .sass-cache/
127 |
128 | # Installshield output folder
129 | [Ee]xpress/
130 |
131 | # DocProject is a documentation generator add-in
132 | DocProject/buildhelp/
133 | DocProject/Help/*.HxT
134 | DocProject/Help/*.HxC
135 | DocProject/Help/*.hhc
136 | DocProject/Help/*.hhk
137 | DocProject/Help/*.hhp
138 | DocProject/Help/Html2
139 | DocProject/Help/html
140 |
141 | # Click-Once directory
142 | publish/
143 |
144 | # Publish Web Output
145 | *.[Pp]ublish.xml
146 | *.azurePubxml
147 | # TODO: Comment the next line if you want to checkin your web deploy settings
148 | # but database connection strings (with potential passwords) will be unencrypted
149 | #*.pubxml
150 | *.publishproj
151 |
152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
153 | # checkin your Azure Web App publish settings, but sensitive information contained
154 | # in these scripts will be unencrypted
155 | PublishScripts/
156 |
157 | # NuGet Packages
158 | *.nupkg
159 | # The packages folder can be ignored because of Package Restore
160 | **/packages/*
161 | # except build/, which is used as an MSBuild target.
162 | !**/packages/build/
163 | # Uncomment if necessary however generally it will be regenerated when needed
164 | #!**/packages/repositories.config
165 | # NuGet v3's project.json files produces more ignoreable files
166 | *.nuget.props
167 | *.nuget.targets
168 |
169 | # Microsoft Azure Build Output
170 | csx/
171 | *.build.csdef
172 |
173 | # Microsoft Azure Emulator
174 | ecf/
175 | rcf/
176 |
177 | # Windows Store app package directories and files
178 | AppPackages/
179 | BundleArtifacts/
180 | Package.StoreAssociation.xml
181 | _pkginfo.txt
182 |
183 | # Visual Studio cache files
184 | # files ending in .cache can be ignored
185 | *.[Cc]ache
186 | # but keep track of directories ending in .cache
187 | !*.[Cc]ache/
188 |
189 | # Others
190 | ClientBin/
191 | ~$*
192 | *~
193 | *.dbmdl
194 | *.dbproj.schemaview
195 | *.jfm
196 | *.pfx
197 | *.publishsettings
198 | node_modules/
199 | orleans.codegen.cs
200 |
201 | # Since there are multiple workflows, uncomment next line to ignore bower_components
202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
203 | #bower_components/
204 |
205 | # RIA/Silverlight projects
206 | Generated_Code/
207 |
208 | # Backup & report files from converting an old project file
209 | # to a newer Visual Studio version. Backup files are not needed,
210 | # because we have git ;-)
211 | _UpgradeReport_Files/
212 | Backup*/
213 | UpgradeLog*.XML
214 | UpgradeLog*.htm
215 |
216 | # SQL Server files
217 | *.mdf
218 | *.ldf
219 |
220 | # Business Intelligence projects
221 | *.rdl.data
222 | *.bim.layout
223 | *.bim_*.settings
224 |
225 | # Microsoft Fakes
226 | FakesAssemblies/
227 |
228 | # GhostDoc plugin setting file
229 | *.GhostDoc.xml
230 |
231 | # Node.js Tools for Visual Studio
232 | .ntvs_analysis.dat
233 |
234 | # Visual Studio 6 build log
235 | *.plg
236 |
237 | # Visual Studio 6 workspace options file
238 | *.opt
239 |
240 | # Visual Studio LightSwitch build output
241 | **/*.HTMLClient/GeneratedArtifacts
242 | **/*.DesktopClient/GeneratedArtifacts
243 | **/*.DesktopClient/ModelManifest.xml
244 | **/*.Server/GeneratedArtifacts
245 | **/*.Server/ModelManifest.xml
246 | _Pvt_Extensions
247 |
248 | # Paket dependency manager
249 | .paket/paket.exe
250 | paket-files/
251 |
252 | # FAKE - F# Make
253 | .fake/
254 |
255 | # JetBrains Rider
256 | .idea/
257 | *.sln.iml
258 |
259 | # CodeRush
260 | .cr/
261 |
262 | # Python Tools for Visual Studio (PTVS)
263 | __pycache__/
264 | *.pyc
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Webhooks/Demo.Microservices.Shipments.Webhooks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp2.1
4 | v2
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | PreserveNewest
17 |
18 |
19 | PreserveNewest
20 | Never
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Webhooks/Startup.cs:
--------------------------------------------------------------------------------
1 |
2 | using Demo.Microservices.Shipments.Data.Providers;
3 | using Demo.Microservices.Shipments.Data.Repositories.InMemory;
4 | using Demo.Microservices.Shipments.Data.Repositories.Interfaces;
5 | using Microsoft.Azure.Functions.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | [assembly: FunctionsStartup(typeof(Demo.Microservices.Shipments.Webhooks.Startup))]
9 | namespace Demo.Microservices.Shipments.Webhooks
10 | {
11 | public class Startup : FunctionsStartup
12 | {
13 | public override void Configure(IFunctionsHostBuilder builder)
14 | {
15 | builder.Services.AddScoped();
16 | builder.Services.AddScoped();
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Webhooks/StatusUpdateFunction.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using Demo.Microservices.Shipments.Contracts.v1;
4 | using Demo.Microservices.Shipments.Data.Exceptions;
5 | using Demo.Microservices.Shipments.Data.Repositories.Interfaces;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.Azure.WebJobs;
8 | using Microsoft.Azure.WebJobs.Extensions.Http;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.Extensions.Logging;
11 | using Newtonsoft.Json;
12 |
13 | namespace Demo.Microservices.Shipments.Webhooks
14 | {
15 | public class StatusUpdateFunction
16 | {
17 | private readonly IShipmentRepository _shipmentsRepository;
18 |
19 | public StatusUpdateFunction(IShipmentRepository shipmentsRepository)
20 | {
21 | _shipmentsRepository = shipmentsRepository;
22 | }
23 |
24 | [FunctionName("StatusUpdateFunction")]
25 | public async Task Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = "update-status")] HttpRequest request, ILogger logger)
26 | {
27 | logger.LogInformation("Shipment webhook arrived with an update");
28 |
29 | string requestBody = await new StreamReader(request.Body).ReadToEndAsync();
30 | var shipmentStatusUpdate = JsonConvert.DeserializeObject(requestBody);
31 | if (shipmentStatusUpdate == null)
32 | {
33 | return new BadRequestResult();
34 | }
35 |
36 | logger.LogInformation("Shipment {TrackingNumber} has changed to {Status}", shipmentStatusUpdate.TrackingNumber, shipmentStatusUpdate.Status);
37 |
38 | try
39 | {
40 | await _shipmentsRepository.UpdateAsync(shipmentStatusUpdate);
41 |
42 | logger.LogInformation("Shipment {TrackingNumber} was updated to status {Status}", shipmentStatusUpdate.TrackingNumber, shipmentStatusUpdate.Status);
43 |
44 | return new OkResult();
45 | }
46 | catch (ShipmentNotFoundException)
47 | {
48 | logger.LogInformation("Shipment {TrackingNumber} was not found");
49 | return new NotFoundResult();
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.Shipments.Webhooks/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0"
3 | }
--------------------------------------------------------------------------------
/src/microservices/Demo.Microservices.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28922.388
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Microservices.Products.API", "Demo.Microservices.Products.API\Demo.Microservices.Products.API.csproj", "{09CB7C79-13E2-4E79-8D20-08916AFC7959}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Microservices.Orders.API", "Demo.Microservices.Orders.API\Demo.Microservices.Orders.API.csproj", "{43492D69-F030-4AC9-B494-3CAFFCB67699}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Microservices.Shipments.API", "Demo.Microservices.Shipments.API\Demo.Microservices.Shipments.API.csproj", "{7A6EF080-7B11-4F3E-8620-61A8A617668A}"
11 | EndProject
12 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "Demo.Microservices.Compose", "Demo.Microservices.Compose.dcproj", "{8B4E651D-A3D1-42B6-AD8C-AAE198901BBF}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Orders", "Orders", "{0E9142A0-B98F-4F37-AB7D-CF98C4C7F86D}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shipments", "Shipments", "{679CA2F9-7877-4C28-8447-ECF32DF5F48D}"
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Products", "Products", "{F3EF006F-55A2-4EBB-A764-09551676FC71}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Microservices.Shipments.Webhooks", "Demo.Microservices.Shipments.Webhooks\Demo.Microservices.Shipments.Webhooks.csproj", "{80351A19-FEA7-45BA-AB06-87193917F75E}"
21 | EndProject
22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Microservices.Shipments.Data", "Demo.Microservices.Shipments.Data\Demo.Microservices.Shipments.Data.csproj", "{56869B08-C1B0-4386-BFC3-B42A14C7BF41}"
23 | EndProject
24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Microservices.Shipments.Contracts", "Demo.Microservices.Shipments.Contracts\Demo.Microservices.Shipments.Contracts.csproj", "{47BE11F5-67E5-4BA2-BB3C-EFC127767A1D}"
25 | EndProject
26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{04FAE427-5BFA-4562-AB9D-9E56FD5E3DCD}"
27 | ProjectSection(SolutionItems) = preProject
28 | api-gateway.conf = api-gateway.conf
29 | EndProjectSection
30 | EndProject
31 | Global
32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
33 | Debug|Any CPU = Debug|Any CPU
34 | Release|Any CPU = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
37 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {43492D69-F030-4AC9-B494-3CAFFCB67699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {43492D69-F030-4AC9-B494-3CAFFCB67699}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {43492D69-F030-4AC9-B494-3CAFFCB67699}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {43492D69-F030-4AC9-B494-3CAFFCB67699}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {7A6EF080-7B11-4F3E-8620-61A8A617668A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {7A6EF080-7B11-4F3E-8620-61A8A617668A}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {7A6EF080-7B11-4F3E-8620-61A8A617668A}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {7A6EF080-7B11-4F3E-8620-61A8A617668A}.Release|Any CPU.Build.0 = Release|Any CPU
49 | {8B4E651D-A3D1-42B6-AD8C-AAE198901BBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50 | {8B4E651D-A3D1-42B6-AD8C-AAE198901BBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 | {8B4E651D-A3D1-42B6-AD8C-AAE198901BBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {8B4E651D-A3D1-42B6-AD8C-AAE198901BBF}.Release|Any CPU.Build.0 = Release|Any CPU
53 | {80351A19-FEA7-45BA-AB06-87193917F75E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54 | {80351A19-FEA7-45BA-AB06-87193917F75E}.Debug|Any CPU.Build.0 = Debug|Any CPU
55 | {80351A19-FEA7-45BA-AB06-87193917F75E}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {80351A19-FEA7-45BA-AB06-87193917F75E}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {56869B08-C1B0-4386-BFC3-B42A14C7BF41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58 | {56869B08-C1B0-4386-BFC3-B42A14C7BF41}.Debug|Any CPU.Build.0 = Debug|Any CPU
59 | {56869B08-C1B0-4386-BFC3-B42A14C7BF41}.Release|Any CPU.ActiveCfg = Release|Any CPU
60 | {56869B08-C1B0-4386-BFC3-B42A14C7BF41}.Release|Any CPU.Build.0 = Release|Any CPU
61 | {47BE11F5-67E5-4BA2-BB3C-EFC127767A1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62 | {47BE11F5-67E5-4BA2-BB3C-EFC127767A1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
63 | {47BE11F5-67E5-4BA2-BB3C-EFC127767A1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {47BE11F5-67E5-4BA2-BB3C-EFC127767A1D}.Release|Any CPU.Build.0 = Release|Any CPU
65 | EndGlobalSection
66 | GlobalSection(SolutionProperties) = preSolution
67 | HideSolutionNode = FALSE
68 | EndGlobalSection
69 | GlobalSection(NestedProjects) = preSolution
70 | {09CB7C79-13E2-4E79-8D20-08916AFC7959} = {F3EF006F-55A2-4EBB-A764-09551676FC71}
71 | {43492D69-F030-4AC9-B494-3CAFFCB67699} = {0E9142A0-B98F-4F37-AB7D-CF98C4C7F86D}
72 | {7A6EF080-7B11-4F3E-8620-61A8A617668A} = {679CA2F9-7877-4C28-8447-ECF32DF5F48D}
73 | {80351A19-FEA7-45BA-AB06-87193917F75E} = {679CA2F9-7877-4C28-8447-ECF32DF5F48D}
74 | {56869B08-C1B0-4386-BFC3-B42A14C7BF41} = {679CA2F9-7877-4C28-8447-ECF32DF5F48D}
75 | {47BE11F5-67E5-4BA2-BB3C-EFC127767A1D} = {679CA2F9-7877-4C28-8447-ECF32DF5F48D}
76 | EndGlobalSection
77 | GlobalSection(ExtensibilityGlobals) = postSolution
78 | SolutionGuid = {CA31D803-7AD4-43A7-8568-2F8EB4922D90}
79 | EndGlobalSection
80 | EndGlobal
81 |
--------------------------------------------------------------------------------
/src/microservices/api-gateway.conf:
--------------------------------------------------------------------------------
1 | config.service.endpoint=
2 | config.service.auth=
--------------------------------------------------------------------------------
/src/microservices/docker-compose.override.yml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 |
3 | services:
4 | demo.microservices.orders.api:
5 | environment:
6 | - ASPNETCORE_ENVIRONMENT=Development
7 | - SHIPMENTS_API_URI=http://demo.microservices.shipments.api/api/v1/shipments
8 | - AZURESTORAGE_CONNECTIONSTRING=
9 | ports:
10 | - "80"
11 |
12 | demo.microservices.products.api:
13 | environment:
14 | - ASPNETCORE_ENVIRONMENT=Development
15 | - AZURESTORAGE_CONNECTIONSTRING=
16 | ports:
17 | - "80"
18 |
19 | demo.microservices.shipments.api:
20 | environment:
21 | - ASPNETCORE_ENVIRONMENT=Development
22 | - AZURESTORAGE_CONNECTIONSTRING=
23 | ports:
24 | - "80"
25 |
26 | demo.microservices.api.docs:
27 | ports:
28 | - "889:80"
29 |
30 | demo.microservices.gateway:
31 | env_file:
32 | - api-gateway.conf
33 | ports:
34 | - "888:8080"
35 |
--------------------------------------------------------------------------------
/src/microservices/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 |
3 | services:
4 | demo.microservices.orders.api:
5 | image: tomkerkhove/demo.microservices.orders.api
6 | build:
7 | context: .
8 | dockerfile: Demo.Microservices.Orders.API/Dockerfile
9 |
10 | demo.microservices.products.api:
11 | image: tomkerkhove/demo.microservices.products.api
12 | build:
13 | context: .
14 | dockerfile: Demo.Microservices.Products.API/Dockerfile
15 |
16 | demo.microservices.shipments.api:
17 | image: tomkerkhove/demo.microservices.shipments.api
18 | build:
19 | context: .
20 | dockerfile: Demo.Microservices.Shipments.API/Dockerfile
21 |
22 | demo.microservices.api.docs:
23 | image: tomkerkhove/demo.microservices.api.docs
24 | build:
25 | context: ./../../deploy/api-management/dev-portal/
26 | dockerfile: Dockerfile
27 |
28 | demo.microservices.gateway:
29 | image: mcr.microsoft.com/azure-api-management/gateway:0.1.0
--------------------------------------------------------------------------------
/src/monolith/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "dotnet-ef": {
6 | "version": "7.0.5",
7 | "commands": [
8 | "dotnet-ef"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/Address.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public class Address
4 | {
5 | public string Street { get; set; }
6 | public string PostalCode { get; set; }
7 | public string State { get; set; }
8 | public string Country { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public class Customer
4 | {
5 | public string FirstName { get; set; }
6 | public string LastName { get; set; }
7 | public Address Address { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/Order.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Demo.Monolith.API.Contracts.v1
4 | {
5 | public class Order
6 | {
7 | public Customer Customer { get; set; }
8 |
9 | public List Basket { get; set; } = new List();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/OrderConfirmation.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public class OrderConfirmation : Order
4 | {
5 | public string ConfirmationId { get; set; }
6 | public ShipmentInformation ShipmentInformation { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/OrderLine.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace Demo.Monolith.API.Contracts.v1
3 | {
4 | public class OrderLine
5 | {
6 | public int Amount { get; set; }
7 | public Product Product { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/Product.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public class Product
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; }
7 | public double Price { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/ShipmentInformation.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public class ShipmentInformation
4 | {
5 | public string TrackingNumber { get; set; }
6 | public ShipmentStatus Status { get; set; }
7 | public Address DeliveryAddress { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/ShipmentStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public enum ShipmentStatus
4 | {
5 | AwaitingPickup,
6 | InTransit,
7 | Delivered
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Contracts/v1/ShipmentStatusUpdate.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.Contracts.v1
2 | {
3 | public class ShipmentStatusUpdate
4 | {
5 | public string TrackingNumber { get; set; }
6 | public ShipmentStatus Status { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Controllers/HealthController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Swashbuckle.AspNetCore.Annotations;
3 |
4 | namespace Demo.Monolith.API.Controllers
5 | {
6 | [Route("api/v1/health")]
7 | [ApiController]
8 | public class HealthController : ControllerBase
9 | {
10 | ///
11 | /// Get Health
12 | ///
13 | /// Provides an indication about the health of the scraper
14 | /// API is healthy
15 | /// API is not healthy
16 | [HttpGet]
17 | [SwaggerOperation(OperationId = "Health_Get")]
18 | public IActionResult Get()
19 | {
20 | return Ok();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Controllers/OrdersController.cs:
--------------------------------------------------------------------------------
1 | using Demo.Monolith.API.Contracts.v1;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Swashbuckle.AspNetCore.Annotations;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using Demo.Monolith.API.Managers;
7 | using Demo.Monolith.API.OpenAPI;
8 | using Demo.Monolith.API.Repositories.Interfaces;
9 |
10 | namespace Demo.Monolith.API.Controllers
11 | {
12 | [Route("api/v1/orders")]
13 | [ApiExplorerSettings(GroupName = OpenApiCategories.Orders)]
14 | public class OrdersController : Controller
15 | {
16 | private readonly OrderManager _orderManager;
17 | private readonly IOrderRepository _orderRepository;
18 |
19 | public OrdersController(OrderManager orderManager, IOrderRepository orderRepository)
20 | {
21 | _orderManager = orderManager;
22 | _orderRepository = orderRepository;
23 | }
24 |
25 | ///
26 | /// Create New Order
27 | ///
28 | /// Provide capability create a new order for products from our catalog
29 | /// Order was successfully created
30 | /// Something went wrong, please contact support
31 | [HttpPost]
32 | [ProducesResponseType(typeof(OrderConfirmation), (int)HttpStatusCode.Created)]
33 | [SwaggerOperation(OperationId = "Order_Create")]
34 | public async Task Post([FromBody]Order orderRequest)
35 | {
36 | var orderConfirmation = await _orderManager.CreateAsync(orderRequest);
37 | return CreatedAtAction(nameof(Get), new { ConfirmationId = orderConfirmation.ConfirmationId }, orderConfirmation);
38 | }
39 |
40 | ///
41 | /// Get Order
42 | ///
43 | /// Provide information about a previously created order
44 | /// Information about a specific order
45 | /// Request was invalid
46 | /// Requested product was not found
47 | /// Something went wrong, please contact support
48 | [HttpGet("{confirmationId}")]
49 | [ProducesResponseType(typeof(Order), (int)HttpStatusCode.OK)]
50 | [SwaggerOperation(OperationId = "Order_Get")]
51 | public async Task Get(string confirmationId)
52 | {
53 | var foundOrder = await _orderRepository.GetAsync(confirmationId);
54 | if (foundOrder == null)
55 | {
56 | return NotFound();
57 | }
58 |
59 | return Ok(foundOrder);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Controllers/ProductsController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Threading.Tasks;
3 | using System.Collections.Generic;
4 | using Demo.Monolith.API.Contracts.v1;
5 | using Demo.Monolith.API.OpenAPI;
6 | using Demo.Monolith.API.Repositories.Interfaces;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Swashbuckle.AspNetCore.Annotations;
9 |
10 | namespace Demo.Monolith.API.Controllers
11 | {
12 | [Route("api/v1/products")]
13 | [ApiExplorerSettings(GroupName = OpenApiCategories.Products)]
14 | public class ProductsController : Controller
15 | {
16 | private readonly IProductRepository _productRepository;
17 |
18 | public ProductsController(IProductRepository productRepository)
19 | {
20 | _productRepository = productRepository;
21 | }
22 |
23 | ///
24 | /// Get Products
25 | ///
26 | /// Provides information about product catalog
27 | /// Overview of our product catalog
28 | /// Something went wrong, please contact support
29 | [HttpGet]
30 | [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)]
31 | [SwaggerOperation(OperationId = "Product_GetAll")]
32 | public async Task Get()
33 | {
34 | var products = await _productRepository.GetAsync();
35 | return Ok(products);
36 | }
37 |
38 | ///
39 | /// Get Product
40 | ///
41 | /// Provides information about a specific product in our catalog
42 | /// Information about a specific product
43 | /// Request was invalid
44 | /// Requested product was not found
45 | /// Something went wrong, please contact support
46 | [HttpGet("{id}")]
47 | [ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
48 | [SwaggerOperation(OperationId = "Product_Get")]
49 | public async Task Get(int id)
50 | {
51 | var foundProduct = await _productRepository.GetAsync(id);
52 | if (foundProduct == null)
53 | {
54 | return NotFound();
55 | }
56 |
57 | return Ok(foundProduct);
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Controllers/ShipmentsController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Threading.Tasks;
3 | using Demo.Monolith.API.Contracts.v1;
4 | using Demo.Monolith.API.Exceptions;
5 | using Demo.Monolith.API.OpenAPI;
6 | using Demo.Monolith.API.Repositories.Interfaces;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Swashbuckle.AspNetCore.Annotations;
9 |
10 | namespace Demo.Monolith.API.Controllers
11 | {
12 | [Route("api/v1/shipments")]
13 | [ApiController]
14 | [ApiExplorerSettings(GroupName = OpenApiCategories.Shipments)]
15 | public class ShipmentsController : ControllerBase
16 | {
17 | private readonly IShipmentRepository _shipmentRepository;
18 |
19 | public ShipmentsController(IShipmentRepository shipmentRepository)
20 | {
21 | _shipmentRepository = shipmentRepository;
22 | }
23 |
24 | ///
25 | /// Get Shipment Information
26 | ///
27 | /// Provides information about a shipment
28 | /// Information about a specific shipment
29 | /// Request was invalid
30 | /// Requested product was not found
31 | /// Something went wrong, please contact support
32 | [HttpGet("{trackingNumber}")]
33 | [ProducesResponseType(typeof(ShipmentInformation), (int)HttpStatusCode.OK)]
34 | [SwaggerOperation(OperationId = "Shipment_Get")]
35 | public async Task Get(string trackingNumber)
36 | {
37 | var shipmentInformation = await _shipmentRepository.GetAsync(trackingNumber);
38 | if (shipmentInformation == null)
39 | {
40 | return NotFound();
41 | }
42 |
43 | return Ok(shipmentInformation);
44 | }
45 |
46 | ///
47 | /// Update Shipment Status
48 | ///
49 | /// Webhook for external shipment partners to provide updates about a shipment
50 | /// Update about a specific shipment and its status
51 | /// Request was invalid
52 | /// Requested product was not found
53 | /// Something went wrong, please contact support
54 | [HttpPost]
55 | [ProducesResponseType((int)HttpStatusCode.OK)]
56 | [SwaggerOperation(OperationId = "Shipment_UpdateStatus")]
57 | [ApiExplorerSettings(GroupName = OpenApiCategories.ShipmentWebhook)]
58 | public async Task Shipment_UpdateStatus([FromBody] ShipmentStatusUpdate shipmentStatusUpdate)
59 | {
60 | try
61 | {
62 | await _shipmentRepository.UpdateAsync(shipmentStatusUpdate);
63 | return Ok();
64 | }
65 | catch (ShipmentNotFoundException)
66 | {
67 | return NotFound();
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Data/Contracts/v1/OrderTableEntity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos.Table;
2 |
3 | namespace Demo.Monolith.API.Data.Contracts.v1
4 | {
5 | public class OrderTableEntity : TableEntity
6 | {
7 | public string ConfirmationId { get; set; }
8 | public string CustomerFirstName { get; set; }
9 | public string CustomerLastName { get; set; }
10 | public string CustomerAddress { get; set; }
11 | public string Basket { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Data/Contracts/v1/ProductTableEntity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos.Table;
2 |
3 | namespace Demo.Monolith.API.Data.Contracts.v1
4 | {
5 | public class ProductTableEntity : TableEntity
6 | {
7 | public int Id { get; set; }
8 | public string Name { get; set; }
9 | public double Price { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Data/Contracts/v1/ShipmentTableEntity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos.Table;
2 |
3 | namespace Demo.Monolith.API.Data.Contracts.v1
4 | {
5 | public class ShipmentTableEntity : TableEntity
6 | {
7 | public string TrackingNumber { get; set; }
8 | public string Status { get; set; }
9 | public string DeliveryAddress { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Data/Providers/TableStorageAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using Microsoft.Azure.Cosmos.Table;
7 | using Microsoft.Extensions.Configuration;
8 |
9 | namespace Demo.Monolith.API.Data.Providers
10 | {
11 | public class TableStorageAccessor
12 | {
13 | private readonly CloudTableClient _tableClient;
14 |
15 | public TableStorageAccessor(IConfiguration configuration)
16 | {
17 | _tableClient = CreateTableClient(configuration);
18 | }
19 |
20 | public async Task PersistAsync(string tableName, TEntity entity)
21 | where TEntity : TableEntity
22 | {
23 | var table = await GetTableAsync(tableName);
24 |
25 | var insertOrMergeOperation = TableOperation.InsertOrMerge(entity);
26 | await table.ExecuteAsync(insertOrMergeOperation);
27 | }
28 |
29 | public async Task GetAsync(string tableName, string partitionKey, string rowKey)
30 | where TEntity : TableEntity
31 | {
32 | var table = await GetTableAsync(tableName);
33 |
34 | var retrieve = TableOperation.Retrieve(partitionKey, rowKey);
35 | var tableOperationResult = await table.ExecuteAsync(retrieve);
36 |
37 | switch (tableOperationResult.HttpStatusCode)
38 | {
39 | case (int)HttpStatusCode.OK:
40 | return (TEntity)tableOperationResult.Result;
41 | case (int)HttpStatusCode.NotFound:
42 | return null;
43 | default:
44 | throw new Exception($"Failed to look up table entity with partition key '{partitionKey}' and row key '{rowKey}' from table '{tableName}'");
45 | }
46 | }
47 |
48 | public async Task> GetAsync(string tableName, string partitionKey)
49 | where TEntity : TableEntity, new()
50 | {
51 | var table = await GetTableAsync(tableName);
52 |
53 | var query = new TableQuery()
54 | .Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
55 |
56 | var foundItems = table.ExecuteQuery(query);
57 | return foundItems.ToList();
58 | }
59 |
60 | private async Task GetTableAsync(string tableName)
61 | {
62 | CloudTable table = _tableClient.GetTableReference(tableName);
63 | await table.CreateIfNotExistsAsync();
64 |
65 | return table;
66 | }
67 |
68 | private CloudTableClient CreateTableClient(IConfiguration configuration)
69 | {
70 | var tableConnectionString = configuration["TableConnectionString"];
71 | var storageAccount = CloudStorageAccount.Parse(tableConnectionString);
72 | return storageAccount.CreateCloudTableClient();
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Demo.Monolith.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net7.0
4 | InProcess
5 | Linux
6 | 6062a5ec-d1f9-4d58-8007-dada8da63ff8
7 |
8 |
9 | Docs/Open-Api.xml
10 |
11 |
12 | Docs/Open-Api.xml
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Dockerfile:
--------------------------------------------------------------------------------
1 | #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
4 | WORKDIR /app
5 | EXPOSE 80
6 | EXPOSE 443
7 |
8 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
9 | WORKDIR /src
10 | COPY ["Demo.Monolith.API/Demo.Monolith.API.csproj", "Demo.Monolith.API/"]
11 | RUN dotnet restore "Demo.Monolith.API/Demo.Monolith.API.csproj"
12 | COPY . .
13 | WORKDIR "/src/Demo.Monolith.API"
14 | RUN dotnet build "Demo.Monolith.API.csproj" -c Release -o /app/build
15 |
16 | FROM build AS publish
17 | RUN dotnet publish "Demo.Monolith.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
18 |
19 | FROM base AS final
20 | WORKDIR /app
21 | COPY --from=publish /app/publish .
22 | ENTRYPOINT ["dotnet", "Demo.Monolith.API.dll"]
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Docs/Open-Api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo.Monolith.API
5 |
6 |
7 |
8 |
9 | Get Health
10 |
11 | Provides an indication about the health of the scraper
12 | API is healthy
13 | API is not healthy
14 |
15 |
16 |
17 | Create New Order
18 |
19 | Provide capability create a new order for products from our catalog
20 | Order was successfully created
21 | Something went wrong, please contact support
22 |
23 |
24 |
25 | Get Order
26 |
27 | Provide information about a previously created order
28 | Information about a specific order
29 | Request was invalid
30 | Requested product was not found
31 | Something went wrong, please contact support
32 |
33 |
34 |
35 | Get Products
36 |
37 | Provides information about product catalog
38 | Overview of our product catalog
39 | Something went wrong, please contact support
40 |
41 |
42 |
43 | Get Product
44 |
45 | Provides information about a specific product in our catalog
46 | Information about a specific product
47 | Request was invalid
48 | Requested product was not found
49 | Something went wrong, please contact support
50 |
51 |
52 |
53 | Get Shipment Information
54 |
55 | Provides information about a shipment
56 | Information about a specific shipment
57 | Request was invalid
58 | Requested product was not found
59 | Something went wrong, please contact support
60 |
61 |
62 |
63 | Update Shipment Status
64 |
65 | Webhook for external shipment partners to provide updates about a shipment
66 | Update about a specific shipment and its status
67 | Request was invalid
68 | Requested product was not found
69 | Something went wrong, please contact support
70 |
71 |
72 |
73 | Provides an Application Builder extension for the Swagger/OpenAPI integration
74 |
75 |
76 |
77 |
78 | Add OpenAPI specification generation
79 |
80 | The ApplicationBuilder instance
81 | The APIVersionDescriptionProvider
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Exceptions/ShipmentNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Demo.Monolith.API.Exceptions
4 | {
5 | public class ShipmentNotFoundException : Exception
6 | {
7 | public ShipmentNotFoundException(string trackingNumber)
8 | {
9 | TrackingNumber = trackingNumber;
10 | }
11 |
12 | public string TrackingNumber { get; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Extensions/IApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Demo.Monolith.API.OpenAPI;
2 | using Microsoft.AspNetCore.Builder;
3 | using Swashbuckle.AspNetCore.SwaggerUI;
4 |
5 | namespace Demo.Monolith.API.Extensions
6 | {
7 | ///
8 | /// Provides an Application Builder extension for the Swagger/OpenAPI integration
9 | ///
10 | public static class IApplicationBuilderExtensions
11 | {
12 | ///
13 | /// Add OpenAPI specification generation
14 | ///
15 | /// The ApplicationBuilder instance
16 | /// The APIVersionDescriptionProvider
17 | public static void UseOpenApiUi(this IApplicationBuilder app)
18 | {
19 | app.UseSwagger();
20 | app.UseSwaggerUI(swaggerUiOptions =>
21 | {
22 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.Monolith}/swagger.json", "Contoso - Monolith API");
23 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.Orders}/swagger.json", "Contoso - Orders API");
24 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.Products}/swagger.json", "Contoso - Products API");
25 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.Shipments}/swagger.json", "Contoso - Shipments API");
26 | swaggerUiOptions.SwaggerEndpoint($"/swagger/{OpenApiCategories.ShipmentWebhook}/swagger.json", "Contoso - Shipment Webhook API");
27 |
28 | swaggerUiOptions.RoutePrefix = "api/docs";
29 | swaggerUiOptions.DisplayOperationId();
30 | swaggerUiOptions.EnableDeepLinking();
31 | swaggerUiOptions.DocExpansion(DocExpansion.List);
32 | swaggerUiOptions.DisplayRequestDuration();
33 | swaggerUiOptions.EnableFilter();
34 | });
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Extensions/IServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using Demo.Monolith.API.OpenAPI;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.OpenApi.Models;
9 | using Swashbuckle.AspNetCore.Swagger;
10 |
11 | namespace Demo.Monolith.API.Extensions
12 | {
13 | public static class IServiceCollectionExtensions
14 | {
15 | public static void UseOpenApiSpecifications(this IServiceCollection services)
16 | {
17 | var xmlDocumentationPath = GetXmlDocumentationPath(services);
18 |
19 | services.AddSwaggerGen(swaggerGenerationOptions =>
20 | {
21 | swaggerGenerationOptions.EnableAnnotations();
22 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.Monolith, CreateApiInformation("Monolith"));
23 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.Orders, CreateApiInformation("Orders"));
24 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.Products, CreateApiInformation("Products"));
25 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.Shipments, CreateApiInformation("Shipments"));
26 | swaggerGenerationOptions.SwaggerDoc(OpenApiCategories.ShipmentWebhook, CreateApiInformation("Shipment Webhook"));
27 | swaggerGenerationOptions.DocInclusionPredicate(IncludeAllOperationsInMonolithApi);
28 |
29 | if (string.IsNullOrEmpty(xmlDocumentationPath) == false)
30 | {
31 | swaggerGenerationOptions.IncludeXmlComments(xmlDocumentationPath);
32 | }
33 | });
34 | }
35 |
36 | private static bool IncludeAllOperationsInMonolithApi(string docName, ApiDescription apiDesc)
37 | {
38 | if (docName.Equals(OpenApiCategories.Monolith, StringComparison.InvariantCultureIgnoreCase)
39 | || apiDesc.GroupName == null)
40 | {
41 | return true;
42 | }
43 |
44 | return apiDesc.GroupName.Equals(docName, StringComparison.InvariantCultureIgnoreCase);
45 | }
46 |
47 | private static OpenApiInfo CreateApiInformation(string microserviceName)
48 | {
49 | var openApiInformation = new OpenApiInfo
50 | {
51 | Title = $"Contoso - {microserviceName} API",
52 | Description = $"{microserviceName} APIs of the Contoso platform",
53 | Version = "v1"
54 | };
55 | return openApiInformation;
56 | }
57 |
58 | private static string GetXmlDocumentationPath(IServiceCollection services)
59 | {
60 | var hostingEnvironment = services.FirstOrDefault(service => service.ServiceType == typeof(IHostingEnvironment));
61 | if (hostingEnvironment == null)
62 | {
63 | return string.Empty;
64 | }
65 |
66 | var contentRootPath = ((IHostingEnvironment)hostingEnvironment.ImplementationInstance).ContentRootPath;
67 | var xmlDocumentationPath = $"{contentRootPath}/Docs/Open-Api.xml";
68 |
69 | return File.Exists(xmlDocumentationPath) ? xmlDocumentationPath : string.Empty;
70 | }
71 |
72 | }
73 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Managers/OrderManager.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Monolith.API.Contracts.v1;
3 | using Demo.Monolith.API.Repositories.Interfaces;
4 |
5 | namespace Demo.Monolith.API.Managers
6 | {
7 | public class OrderManager
8 | {
9 | private readonly IOrderRepository _orderRepository;
10 | private readonly IShipmentRepository _shipmentRepository;
11 |
12 | public OrderManager(IOrderRepository orderRepository, IShipmentRepository shipmentRepository)
13 | {
14 | _orderRepository = orderRepository;
15 | _shipmentRepository = shipmentRepository;
16 | }
17 |
18 | public async Task CreateAsync(Order createdOrder)
19 | {
20 | var confirmationId = await _orderRepository.CreateAsync(createdOrder);
21 | var shipmentInformation = await _shipmentRepository.CreateAsync(createdOrder);
22 |
23 | var orderConfirmation = new OrderConfirmation
24 | {
25 | ConfirmationId = confirmationId,
26 | ShipmentInformation = shipmentInformation,
27 | Customer = createdOrder.Customer
28 | };
29 |
30 | return orderConfirmation;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/OpenAPI/OpenApiCategories.cs:
--------------------------------------------------------------------------------
1 | namespace Demo.Monolith.API.OpenAPI
2 | {
3 | public class OpenApiCategories
4 | {
5 | public const string Monolith = "monolith";
6 | public const string Orders = "orders";
7 | public const string Products = "products";
8 | public const string Shipments = "shipments";
9 | public const string ShipmentWebhook = "shipment_webhook";
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace Demo.Monolith.API
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateWebHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args).UseStartup();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/InMemory/InMemoryOrderRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Demo.Monolith.API.Contracts.v1;
5 | using Demo.Monolith.API.Repositories.Interfaces;
6 |
7 | namespace Demo.Monolith.API.Repositories.InMemory
8 | {
9 | public class InMemoryOrderRepository : IOrderRepository
10 | {
11 | private readonly Dictionary _orders = new Dictionary();
12 |
13 | public Task CreateAsync(Order createdOrder)
14 | {
15 | var confirmationId = Guid.NewGuid().ToString();
16 |
17 | _orders.Add(confirmationId, createdOrder);
18 |
19 | return Task.FromResult(confirmationId);
20 | }
21 |
22 | public Task GetAsync(string confirmationId)
23 | {
24 | if(_orders.TryGetValue(confirmationId, out Order createdOrder))
25 | {
26 | return Task.FromResult(createdOrder);
27 | }
28 |
29 | return Task.FromResult(null);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/InMemory/InMemoryProductRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Demo.Monolith.API.Contracts.v1;
5 | using Demo.Monolith.API.Repositories.Interfaces;
6 |
7 | namespace Demo.Monolith.API.Repositories.InMemory
8 | {
9 | public class InMemoryProductRepository : IProductRepository
10 | {
11 | private readonly List _availableProducts = new List
12 | {
13 | new Product {Id = 1, Name = "Surface Go", Price = 399},
14 | new Product {Id = 2, Name = "Surface Pro 6", Price = 799},
15 | new Product {Id = 3, Name = "Surface Book 2", Price = 1049},
16 | new Product {Id = 4, Name = "Surface Laptop 2", Price = 1199},
17 | new Product {Id = 5, Name = "Surface Studio 2", Price = 3499}
18 | };
19 |
20 | public Task GetAsync(int id)
21 | {
22 | var foundProduct = _availableProducts.SingleOrDefault(product => product.Id == id);
23 | return Task.FromResult(foundProduct);
24 | }
25 |
26 | public Task> GetAsync()
27 | {
28 | return Task.FromResult(_availableProducts);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/InMemory/InMemoryShipmentRepository.cs:
--------------------------------------------------------------------------------
1 |
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 | using Demo.Monolith.API.Contracts.v1;
6 | using Demo.Monolith.API.Exceptions;
7 | using Demo.Monolith.API.Repositories.Interfaces;
8 |
9 | namespace Demo.Monolith.API.Repositories.InMemory
10 | {
11 | public class InMemoryShipmentRepository : IShipmentRepository
12 | {
13 | private readonly Dictionary _shipments = new Dictionary();
14 |
15 | public Task CreateAsync(Order createdOrder)
16 | {
17 | var trackingNumber = Guid.NewGuid().ToString();
18 |
19 | var shipmentInformation = new ShipmentInformation
20 | {
21 | TrackingNumber = trackingNumber,
22 | DeliveryAddress = createdOrder.Customer.Address,
23 | Status = ShipmentStatus.AwaitingPickup
24 | };
25 |
26 | _shipments.Add(trackingNumber, shipmentInformation);
27 |
28 | return Task.FromResult(shipmentInformation);
29 | }
30 |
31 | public Task GetAsync(string trackingNumber)
32 | {
33 | if (_shipments.TryGetValue(trackingNumber, out ShipmentInformation shipment))
34 | {
35 | return Task.FromResult(shipment);
36 | }
37 |
38 | return Task.FromResult(null);
39 | }
40 |
41 | public Task UpdateAsync(ShipmentStatusUpdate shipmentStatusUpdate)
42 | {
43 | if (!_shipments.TryGetValue(shipmentStatusUpdate.TrackingNumber, out var foundShipmentInformation))
44 | {
45 | throw new ShipmentNotFoundException(shipmentStatusUpdate.TrackingNumber);
46 | }
47 |
48 | foundShipmentInformation.Status = shipmentStatusUpdate.Status;
49 | _shipments[shipmentStatusUpdate.TrackingNumber] = foundShipmentInformation;
50 |
51 | return Task.CompletedTask;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/InMemory/OrderTableRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Demo.Monolith.API.Contracts.v1;
6 | using Demo.Monolith.API.Data.Contracts.v1;
7 | using Demo.Monolith.API.Data.Providers;
8 | using Demo.Monolith.API.Repositories.Interfaces;
9 | using Newtonsoft.Json;
10 |
11 | namespace Demo.Monolith.API.Repositories.InMemory
12 | {
13 | public class OrderTableRepository : IOrderRepository
14 | {
15 | private readonly TableStorageAccessor _tableStorageAccessor;
16 | private const string TableName = "orders";
17 |
18 | public OrderTableRepository(TableStorageAccessor tableStorageAccessor)
19 | {
20 | _tableStorageAccessor = tableStorageAccessor;
21 | }
22 |
23 | public async Task CreateAsync(Order createdOrder)
24 | {
25 | var confirmationId = Guid.NewGuid().ToString();
26 |
27 | var orderTableEntity = new OrderTableEntity
28 | {
29 | PartitionKey = confirmationId,
30 | RowKey = DateTimeOffset.UtcNow.ToString("s"),
31 | ConfirmationId = confirmationId,
32 | CustomerFirstName = createdOrder.Customer.FirstName,
33 | CustomerLastName = createdOrder.Customer.LastName,
34 | Basket = JsonConvert.SerializeObject(createdOrder.Basket),
35 | CustomerAddress = JsonConvert.SerializeObject(createdOrder.Customer.Address)
36 | };
37 |
38 | await _tableStorageAccessor.PersistAsync(TableName, orderTableEntity);
39 |
40 | return confirmationId;
41 | }
42 |
43 | public async Task GetAsync(string confirmationId)
44 | {
45 | var persistedOrderUpdates = await _tableStorageAccessor.GetAsync(TableName, confirmationId);
46 |
47 | Order order = null;
48 | if (persistedOrderUpdates.Any())
49 | {
50 | var mostRecentOrderUpdate = persistedOrderUpdates.OrderByDescending(orderUpdate => orderUpdate.RowKey).First();
51 |
52 | order = new Order
53 | {
54 | Basket = JsonConvert.DeserializeObject>(mostRecentOrderUpdate.Basket),
55 | Customer = new Customer
56 | {
57 | FirstName = mostRecentOrderUpdate.CustomerFirstName,
58 | LastName = mostRecentOrderUpdate.CustomerLastName
59 | }
60 | };
61 |
62 | if (mostRecentOrderUpdate.CustomerAddress != null)
63 | {
64 | order.Customer.Address = JsonConvert.DeserializeObject(mostRecentOrderUpdate.CustomerAddress);
65 | }
66 | }
67 |
68 | return order;
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/InMemory/ProductTableRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Demo.Monolith.API.Contracts.v1;
5 | using Demo.Monolith.API.Data.Contracts.v1;
6 | using Demo.Monolith.API.Data.Providers;
7 | using Demo.Monolith.API.Repositories.Interfaces;
8 |
9 | namespace Demo.Monolith.API.Repositories.InMemory
10 | {
11 | public class ProductTableRepository : IProductRepository
12 | {
13 | private readonly TableStorageAccessor _tableStorageAccessor;
14 | private const string TableName = "products";
15 |
16 | public ProductTableRepository(TableStorageAccessor tableStorageAccessor)
17 | {
18 | _tableStorageAccessor = tableStorageAccessor;
19 | }
20 |
21 | public async Task> GetAsync()
22 | {
23 | var products = await _tableStorageAccessor.GetAsync(TableName, "products");
24 | return products.Select(MapToContract).ToList();
25 | }
26 |
27 | public async Task GetAsync(int id)
28 | {
29 | var persistedProduct = await _tableStorageAccessor.GetAsync(TableName, "products", id.ToString());
30 |
31 | Product order = null;
32 | if (persistedProduct != null)
33 | {
34 | order = MapToContract(persistedProduct);
35 | }
36 |
37 | return order;
38 | }
39 |
40 | private Product MapToContract(ProductTableEntity productTableEntity)
41 | {
42 | return new Product
43 | {
44 | Id = productTableEntity.Id,
45 | Name = productTableEntity.Name,
46 | Price = productTableEntity.Price,
47 | };
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/InMemory/ShipmentsTableRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Demo.Monolith.API.Contracts.v1;
6 | using Demo.Monolith.API.Data.Contracts.v1;
7 | using Demo.Monolith.API.Data.Providers;
8 | using Demo.Monolith.API.Repositories.Interfaces;
9 | using Newtonsoft.Json;
10 |
11 | namespace Demo.Monolith.API.Repositories.InMemory
12 | {
13 | public class ShipmentsTableRepository : IShipmentRepository
14 | {
15 | private readonly TableStorageAccessor _tableStorageAccessor;
16 | private const string TableName = "shipments";
17 |
18 | public ShipmentsTableRepository(TableStorageAccessor tableStorageAccessor)
19 | {
20 | _tableStorageAccessor = tableStorageAccessor;
21 | }
22 |
23 | private readonly Dictionary _shipments = new Dictionary();
24 |
25 | public async Task CreateAsync(Order createdOrder)
26 | {
27 | var trackingNumber = Guid.NewGuid().ToString();
28 |
29 | var shipmentInformation = new ShipmentInformation
30 | {
31 | TrackingNumber = trackingNumber,
32 | DeliveryAddress = createdOrder.Customer.Address,
33 | Status = ShipmentStatus.AwaitingPickup
34 | };
35 |
36 | var shipmentInformationTableEntry = MapToTableEntry(shipmentInformation);
37 | await _tableStorageAccessor.PersistAsync(TableName, shipmentInformationTableEntry);
38 |
39 | return shipmentInformation;
40 | }
41 |
42 | public async Task GetAsync(string trackingNumber)
43 | {
44 | var shipmentTableEntry = await GetShipmentTableEntryAsync(trackingNumber);
45 | if (shipmentTableEntry == null)
46 | {
47 | return null;
48 | }
49 |
50 | var shipmentInformation = MapToContract(shipmentTableEntry);
51 | return shipmentInformation;
52 | }
53 |
54 | public async Task UpdateAsync(ShipmentStatusUpdate shipmentStatusUpdate)
55 | {
56 | var shipmentTableEntry = await GetShipmentTableEntryAsync(shipmentStatusUpdate.TrackingNumber);
57 |
58 | shipmentTableEntry.RowKey = DateTimeOffset.UtcNow.ToString("s");
59 | shipmentTableEntry.Status = shipmentStatusUpdate.Status.ToString();
60 |
61 | await _tableStorageAccessor.PersistAsync(TableName, shipmentTableEntry);
62 | }
63 |
64 | private async Task GetShipmentTableEntryAsync(string trackingNumber)
65 | {
66 | var shipmentUpdates = await _tableStorageAccessor.GetAsync(TableName, trackingNumber);
67 | return shipmentUpdates.OrderByDescending(shipmentUpdate => shipmentUpdate.RowKey).FirstOrDefault();
68 | }
69 |
70 | private ShipmentInformation MapToContract(ShipmentTableEntity shipmentTableEntry)
71 | {
72 | return new ShipmentInformation
73 | {
74 | TrackingNumber = shipmentTableEntry.TrackingNumber,
75 | DeliveryAddress = JsonConvert.DeserializeObject(shipmentTableEntry.DeliveryAddress),
76 | Status = Enum.Parse(shipmentTableEntry.Status),
77 | };
78 | }
79 |
80 | private ShipmentTableEntity MapToTableEntry(ShipmentInformation shipmentInformation)
81 | {
82 | return new ShipmentTableEntity
83 | {
84 | PartitionKey = shipmentInformation.TrackingNumber,
85 | RowKey = DateTimeOffset.UtcNow.ToString("s"),
86 | TrackingNumber = shipmentInformation.TrackingNumber,
87 | DeliveryAddress = JsonConvert.SerializeObject(shipmentInformation.DeliveryAddress),
88 | Status = shipmentInformation.Status.ToString(),
89 | };
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/Interfaces/IOrderRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Monolith.API.Contracts.v1;
3 |
4 | namespace Demo.Monolith.API.Repositories.Interfaces
5 | {
6 | public interface IOrderRepository
7 | {
8 | Task GetAsync(string confirmationId);
9 | Task CreateAsync(Order createdOrder);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/Interfaces/IProductRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Demo.Monolith.API.Contracts.v1;
4 |
5 | namespace Demo.Monolith.API.Repositories.Interfaces
6 | {
7 | public interface IProductRepository
8 | {
9 | Task GetAsync(int id);
10 | Task> GetAsync();
11 | }
12 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Repositories/Interfaces/IShipmentRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Demo.Monolith.API.Contracts.v1;
3 |
4 | namespace Demo.Monolith.API.Repositories.Interfaces
5 | {
6 | public interface IShipmentRepository
7 | {
8 | Task CreateAsync(Order order);
9 | Task GetAsync(string trackingNumber);
10 | Task UpdateAsync(ShipmentStatusUpdate shipmentStatusUpdate);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/Startup.cs:
--------------------------------------------------------------------------------
1 | using Demo.Monolith.API.Data.Providers;
2 | using Demo.Monolith.API.Extensions;
3 | using Demo.Monolith.API.Managers;
4 | using Demo.Monolith.API.Repositories.InMemory;
5 | using Demo.Monolith.API.Repositories.Interfaces;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Newtonsoft.Json;
12 |
13 | namespace Demo.Monolith.API
14 | {
15 | public class Startup
16 | {
17 | public Startup(IConfiguration configuration)
18 | {
19 | Configuration = configuration;
20 | }
21 |
22 | public IConfiguration Configuration { get; }
23 |
24 | // This method gets called by the runtime. Use this method to add services to the container.
25 | public void ConfigureServices(IServiceCollection services)
26 | {
27 | services.AddMvc()
28 | .AddNewtonsoftJson(options =>
29 | {
30 | options.SerializerSettings.Formatting = Formatting.Indented;
31 | options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
32 |
33 | });
34 |
35 | services.AddScoped();
36 | services.AddScoped();
37 | services.AddScoped();
38 | services.AddScoped();
39 | services.AddScoped();
40 |
41 | services.UseOpenApiSpecifications();
42 | }
43 |
44 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
45 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
46 | {
47 | if (env.IsDevelopment())
48 | {
49 | app.UseDeveloperExceptionPage();
50 | }
51 | else
52 | {
53 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
54 | app.UseHsts();
55 | }
56 |
57 | app.UseRouting()
58 | .UseEndpoints(o => o.MapControllers())
59 | .UseOpenApiUi();
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | },
9 | "TableConnectionString": "UseDevelopmentStorage=true"
10 | }
11 |
--------------------------------------------------------------------------------
/src/monolith/Demo.Monolith.API/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*",
8 | "TableConnectionString": ""
9 | }
10 |
--------------------------------------------------------------------------------
/src/monolith/Demo.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28922.388
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Monolith", "Monolith", "{98D4760C-CFE0-404A-B703-321B1F0CA599}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Monolith.API", "Demo.Monolith.API\Demo.Monolith.API.csproj", "{09CB7C79-13E2-4E79-8D20-08916AFC7959}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {09CB7C79-13E2-4E79-8D20-08916AFC7959}.Release|Any CPU.Build.0 = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(SolutionProperties) = preSolution
22 | HideSolutionNode = FALSE
23 | EndGlobalSection
24 | GlobalSection(NestedProjects) = preSolution
25 | {09CB7C79-13E2-4E79-8D20-08916AFC7959} = {98D4760C-CFE0-404A-B703-321B1F0CA599}
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {CA31D803-7AD4-43A7-8568-2F8EB4922D90}
29 | EndGlobalSection
30 | EndGlobal
31 |
--------------------------------------------------------------------------------