├── .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 | ![](./media/contoso.jpg) 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 | ![Azure API Management Gateway](./../media/api-management-gateway.png) 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 | ![Contoso migration to Azure Web App for Containers](./../media/contoso-phase-II-internals.png) 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 | ![Contoso using Azure API Management Gateway inside the cluster](./../media/contoso-future.png) -------------------------------------------------------------------------------- /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 | ![Contoso](./../media/contoso.jpg) 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 | ![Contoso today](./../media/contoso-today.png) 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 | ![Contoso migration to Azure Web App for Containers](./../media/contoso-phase-II-internals.png) 14 | 15 | In order to achieve this they will use the same A/B testing to guarantee that everything still works. 16 | 17 | ![Contoso migration to Azure Web App for Containers](./../media/contoso-phase-II.png) 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 | ![Contoso migration to Azure Web App for Containers](./../media/contoso-phase-I.png) 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 | --------------------------------------------------------------------------------