├── .github
└── workflows
│ └── build&Test.yml
├── .gitignore
├── Distribt.sln
├── GlobalSettings.msbuild
├── LICENSE
├── README.md
├── assets
├── diagram.png
├── distribtLogo.jpg
└── logo.png
├── docker-compose.yaml
├── docs
├── communication
│ └── Readme.md
├── discovery
│ └── Readme.md
└── secrets
│ └── Readme.md
├── src
├── Api
│ ├── Private
│ │ └── Distribt.API.Private
│ │ │ ├── Distribt.API.Private.csproj
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ └── Public
│ │ └── Distribt.API.Public
│ │ ├── Distribt.API.Public.csproj
│ │ ├── Program.cs
│ │ ├── Properties
│ │ └── launchSettings.json
│ │ ├── appsettings.Development.json
│ │ └── appsettings.json
├── Services
│ ├── Emails
│ │ └── Distribt.Services.Emails
│ │ │ ├── Controllers
│ │ │ └── EmailController.cs
│ │ │ ├── Distribt.Services.Emails.csproj
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ ├── Orders
│ │ ├── Distribt.Services.Orders.BusinessLogic
│ │ │ ├── Data
│ │ │ │ └── External
│ │ │ │ │ └── ProductRepository.cs
│ │ │ ├── Distribt.Services.Orders.BusinessLogic.csproj
│ │ │ ├── HealthChecks
│ │ │ │ └── ProductsHealthCheck.cs
│ │ │ ├── OrdersBusinessLogicDependencyInjection.cs
│ │ │ └── Services
│ │ │ │ └── External
│ │ │ │ └── ProductNameService.cs
│ │ ├── Distribt.Services.Orders.Consumer
│ │ │ ├── Controllers
│ │ │ │ └── IntegrationConsumerController.cs
│ │ │ ├── Distribt.Services.Orders.Consumer.csproj
│ │ │ ├── Handler
│ │ │ │ ├── OrderCreatedHandler.cs
│ │ │ │ └── ProductModifierHandler.cs
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ │ └── launchSettings.json
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ │ ├── Distribt.Services.Orders.Dto
│ │ │ ├── Distribt.Services.Orders.Dto.csproj
│ │ │ ├── OrderRequest.cs
│ │ │ └── OrderResponse.cs
│ │ └── Distribt.Services.Orders
│ │ │ ├── Aggregates
│ │ │ ├── MongoMapping.cs
│ │ │ └── OrderDetails.cs
│ │ │ ├── Controllers
│ │ │ └── OrderController.cs
│ │ │ ├── Data
│ │ │ └── OrderRepository.cs
│ │ │ ├── Distribt.Services.Orders.csproj
│ │ │ ├── Events
│ │ │ ├── OrderEvents.cs
│ │ │ └── ProductEvents.cs
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ │ ├── Services
│ │ │ ├── CreateOrderService.cs
│ │ │ ├── GetOrderService.cs
│ │ │ ├── OrderDeliveredService.cs
│ │ │ ├── OrderDispatchedService.cs
│ │ │ └── OrderPaidService.cs
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ ├── Products
│ │ ├── Distribt.Services.Products.Api.Read
│ │ │ ├── Distribt.Services.Products.Api.Read.csproj
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ │ └── launchSettings.json
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ │ ├── Distribt.Services.Products.Api.Write
│ │ │ ├── Controllers
│ │ │ │ └── ProductController.cs
│ │ │ ├── Distribt - Backup.Services.Products.csproj
│ │ │ ├── Distribt.Services.Products.Api.Write.csproj
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ │ └── launchSettings.json
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ │ ├── Distribt.Services.Products.BusinessLogic
│ │ │ ├── DataAccess
│ │ │ │ ├── ProductsReadStore.cs
│ │ │ │ └── ProductsWriteStore.cs
│ │ │ ├── Distribt.Services.Products.BusinessLogic.csproj
│ │ │ └── UseCases
│ │ │ │ ├── CreateProductDetails.cs
│ │ │ │ └── UpdateProductDetails.cs
│ │ ├── Distribt.Services.Products.Consumer
│ │ │ ├── Controllers
│ │ │ │ └── DomainConsumerController.cs
│ │ │ ├── Distribt.Services.Products.Consumer.csproj
│ │ │ ├── Handlers
│ │ │ │ ├── ProductCreatedHandler.cs
│ │ │ │ └── ProductUpdatedHandler.cs
│ │ │ ├── Program.cs
│ │ │ ├── Properties
│ │ │ │ └── launchSettings.json
│ │ │ ├── appsettings.Development.json
│ │ │ └── appsettings.json
│ │ └── Distribt.Services.Products.Dtos
│ │ │ ├── Distribt.Services.Products.Dtos.csproj
│ │ │ └── ProductDto.cs
│ └── Subscriptions
│ │ ├── Distribt.Services.Subscriptions.Consumer
│ │ ├── Controllers
│ │ │ └── IntegrationConsumerController.cs
│ │ ├── Distribt.Services.Subscriptions.Consumer.csproj
│ │ ├── Handler
│ │ │ ├── SubscriptionHandler.cs
│ │ │ └── UnSubscriptionHandler.cs
│ │ ├── Program.cs
│ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ ├── appsettings.Development.json
│ │ └── appsettings.json
│ │ ├── Distribt.Services.Subscriptions.Dtos
│ │ ├── Distribt.Services.Subscriptions.Dtos.csproj
│ │ ├── SubscriptionDto.cs
│ │ └── UnSubscriptionDto.cs
│ │ └── Distribt.Services.Subscriptions
│ │ ├── Controllers
│ │ └── SubscriptionController.cs
│ │ ├── Distribt.Services.Subscriptions.csproj
│ │ ├── Program.cs
│ │ ├── Properties
│ │ └── launchSettings.json
│ │ ├── appsettings.Development.json
│ │ └── appsettings.json
├── Shared
│ ├── Distribt.Shared.Api
│ │ └── Distribt.Shared.Api.csproj
│ ├── Distribt.Shared.Discovery
│ │ ├── ConsulServiceDiscovery.cs
│ │ ├── DiscoveryDependencyInjection.cs
│ │ ├── DiscoveryServices.cs
│ │ └── Distribt.Shared.Discovery.csproj
│ ├── Distribt.Shared.EventSourcing
│ │ ├── Aggregate.cs
│ │ ├── AggregateChange.cs
│ │ ├── AggregateRepository.cs
│ │ ├── Distribt.Shared.EventSourcing.csproj
│ │ ├── EventSourcingDependencyInjection.cs
│ │ ├── EventStores
│ │ │ ├── EventStore.cs
│ │ │ ├── IEventStoreManager.cs
│ │ │ └── MongoEventStoreManager.cs
│ │ ├── Extensions
│ │ │ ├── AggregateMappers.cs
│ │ │ └── DynamicExtensions.cs
│ │ └── IApply.cs
│ ├── Distribt.Shared.Logging
│ │ ├── ConfigureLogger.cs
│ │ ├── Distribt.Shared.Logging.csproj
│ │ └── Loggers
│ │ │ ├── ConsoleLoggerConfiguration.cs
│ │ │ ├── GraylogLoggerConfiguration.cs
│ │ │ └── LoggerConfigurationExtensions.cs
│ ├── Distribt.Shared.Secrets
│ │ ├── Distribt.Shared.Secrets.csproj
│ │ ├── Extensions
│ │ │ └── ObjectExtensions.cs
│ │ ├── VaultDependencyInjection.cs
│ │ ├── VaultSecretManager.cs
│ │ └── VaultSettings.cs
│ ├── Distribt.Shared.Serialization
│ │ ├── Distribt.Shared.Serialization.csproj
│ │ ├── ISerializer.cs
│ │ ├── SerializationDependencyInjection.cs
│ │ └── Serializer.cs
│ ├── Distribt.Shared.Setup
│ │ ├── API
│ │ │ ├── DefaultDistribtWebApplication.cs
│ │ │ ├── HealthCheckHelper.cs
│ │ │ ├── Key
│ │ │ │ ├── ApiKeyConfiguration.cs
│ │ │ │ ├── ApiKeyDependencyInjection.cs
│ │ │ │ └── ApiKeyMiddleware.cs
│ │ │ └── RateLimiting
│ │ │ │ └── DistribtRateLimiterPolicy.cs
│ │ ├── Databases
│ │ │ ├── MongoDb.cs
│ │ │ └── MySql.cs
│ │ ├── Distribt.Shared.Setup.csproj
│ │ ├── Extensions
│ │ │ └── LinqExtensions.cs
│ │ ├── GlobalUsings.cs
│ │ ├── Observability
│ │ │ └── OpenTelemetry.cs
│ │ └── Services
│ │ │ ├── EventSourcing.cs
│ │ │ ├── SecretManager.cs
│ │ │ ├── ServiceBus.cs
│ │ │ └── ServiceDiscovery.cs
│ ├── Shared.Communication
│ │ ├── Distribt.Shared.Communication.RabbitMQ
│ │ │ ├── Consumer
│ │ │ │ ├── RabbitMQMessageConsumer.cs
│ │ │ │ └── RabbitMQMessageReceiver.cs
│ │ │ ├── Distribt.Shared.Communication.RabbitMQ.csproj
│ │ │ ├── Publisher
│ │ │ │ └── RabbitMQMessagePublisher.cs
│ │ │ ├── RabbitMQDependencyInjection.cs
│ │ │ └── RabbitMQSettings.cs
│ │ └── Distribt.Shared.Communication
│ │ │ ├── CommunicationDependencyInjection.cs
│ │ │ ├── Consumer
│ │ │ ├── Handler
│ │ │ │ ├── HandleMessage.cs
│ │ │ │ ├── IMessageHandler.cs
│ │ │ │ └── MessageHandlerRegistry.cs
│ │ │ ├── Host
│ │ │ │ ├── ConsumerController.cs
│ │ │ │ └── ConsumerHostedService.cs
│ │ │ ├── IMessageConsumer.cs
│ │ │ └── Manager
│ │ │ │ ├── ConsumerManager.cs
│ │ │ │ └── IConsumerManager.cs
│ │ │ ├── Distribt.Shared.Communication.csproj
│ │ │ ├── Messages
│ │ │ ├── DomainMessage.cs
│ │ │ ├── IMessage.cs
│ │ │ ├── IntegrationMessage.cs
│ │ │ └── Metadata.cs
│ │ │ └── Publisher
│ │ │ ├── Domain
│ │ │ ├── DefaultDomainMessagePublisher.cs
│ │ │ └── DomainMessageMapper.cs
│ │ │ ├── IExternalMessagePublisher.cs
│ │ │ └── Integration
│ │ │ ├── DefaultIntegrationMessagePublisher.cs
│ │ │ └── IntegrationMessageMapper.cs
│ └── Shared.Databases
│ │ ├── Distribt.Shared.Databases.MongoDb
│ │ ├── Distribt.Shared.Databases.MongoDb.csproj
│ │ ├── MongoDbConnectionProvider.cs
│ │ └── MongoDbDependencyInjection.cs
│ │ └── Distribt.Shared.Databases.MySql
│ │ ├── Distribt.Shared.Databases.MySql.csproj
│ │ └── MySqlDependencyInjection.cs
└── Tests
│ ├── Services
│ ├── Orders
│ │ └── Distribt.Tests.Services.Orders.BusinessLogicTests
│ │ │ ├── Distribt.Tests.Services.Orders.BusinessLogicTests.csproj
│ │ │ └── Services
│ │ │ └── External
│ │ │ └── TestProductNameService.cs
│ └── Subscriptions
│ │ └── Distribt.Tests.Services.Subscriptions.ApiTests
│ │ ├── Distribt.Tests.Services.Subscriptions.ApiTests.csproj
│ │ └── SubscriptionControllerTest.cs
│ └── Shared
│ └── Discovery
│ └── Distribt.Test.Shared.Discovery.Tests
│ ├── ConsulServiceDiscoveryTest.cs
│ └── Distribt.Test.Shared.Discovery.Tests.csproj
└── tools
├── consul
└── config.sh
├── local-development
└── up.sh
├── mongodb
└── mongo-init.js
├── mysql
└── init.sql
├── rabbitmq
├── definitions.json
├── enabled_plugins
└── rabbitmq.conf
├── telemetry
├── grafana_datasources.yaml
├── otel-collector-config.yaml
├── prometheus.yaml
└── rabbitmq-overview_rev11.json
└── vault
└── config.sh
/.github/workflows/build&Test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v3
21 | with:
22 | dotnet-version: 8.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test ./src/Tests/Services/Orders/Distribt.Tests.Services.Orders.BusinessLogicTests/Distribt.Tests.Services.Orders.BusinessLogicTests.csproj --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/GlobalSettings.msbuild:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ivan Abad
4 | NetMentor
5 | NetMentor Distribt
6 | Copyright NetMentor 2022
7 | 1.0.0
8 | MIT
9 | https://github.com/ElectNewt/Distribt
10 | Library to build distributed applications
11 |
12 |
13 | false
14 | 12.0
15 | net8.0
16 | enable
17 | enable
18 |
19 |
20 |
21 |
22 | Configuration=Debug
23 | Debug
24 | Configuration=Debug
25 |
26 |
27 | Configuration=Release
28 | Release
29 | Configuration=Release
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 ElectNewt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | The Software cannot be used to be part of other software or architecture that
16 | will hurt other human being in any way, which include but not limited to
17 | physically, mentally. A few common examples are gambling, cryptocurrencies,
18 | guns (including any part of the company), etc.
19 |
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | SOFTWARE.
28 |
--------------------------------------------------------------------------------
/assets/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElectNewt/Distribt/705cbf93b5edcc5bd7abd4a4841144542751cfee/assets/diagram.png
--------------------------------------------------------------------------------
/assets/distribtLogo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElectNewt/Distribt/705cbf93b5edcc5bd7abd4a4841144542751cfee/assets/distribtLogo.jpg
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElectNewt/Distribt/705cbf93b5edcc5bd7abd4a4841144542751cfee/assets/logo.png
--------------------------------------------------------------------------------
/docs/discovery/Readme.md:
--------------------------------------------------------------------------------
1 | # Shared.Discovery
2 |
3 | Para **evitar** tener que indicar la URL de cada servicio de forma manual ya sea en los ficheros de configuración o directamente en el código.
4 | Podemos utilizar un `Service Discovery` (También llamado Service Registry).
5 |
6 | En nuestro caso, vamos a utilizar `Consul`, pero puede ser cualquier otro.
7 |
8 |
9 | # Implementación de Distribt.Shared.Discovery
10 |
11 | Para implementarlo hay que importar el paquete `Distribt.Shared.Discovery``y después en tu contenedor de dependencias utilizar el método `.AddDiscovery(Iconfiguration)`.
12 |
13 | En la configuración, dentro del `appsettings` tiene que contener la siguiente información:
14 |
15 | ````json
16 | {
17 | ...
18 | "Discovery": {
19 | "Address": "http://localhost:8500"
20 | },
21 | ...
22 | }
23 | ````
24 | Donde `localhost:8500` es la localización de tu Consul service.
25 |
26 | Ahora, dentro de tu código únicamente debes inyectar la interfaz `IServiceDiscovery` y usarlo de la siguiente manera:
27 |
28 | ````csharp
29 | public class YourClass
30 | {
31 | private readonly IServiceDiscovery _discovery;
32 |
33 | public ProductsHealthCheck(IServiceDiscovery discovery)
34 | {
35 | _discovery = discovery;
36 | }
37 |
38 | public async Task Execute(CancellationToken cancellationToken = default)
39 | {
40 |
41 | string productsReadApi =
42 | await _discovery.GetFullAddress(Product.Service.Name, cancellationToken);
43 | }
44 | }
45 |
46 | ````
47 |
48 | Hay dos opciones, una para recibir la url entera, como acabamos de ver `.GetFullAddress(name, cancellationToken)`
49 |
50 | o la opción `.GetDiscoveryData(name, cancellationToken)` que devuelve `DiscoveryData` el cual contiene las propiedades `Server` y `Port` las cuales son definidas dentro del servicio de registro.
51 |
52 |
53 | ### Incluir información en el servicio de registro consul
54 |
55 | En nuestro caso utilizaremos Consul, puedes hacerlo de forma manual, o por la CLI con el comando
56 | ```bash
57 | consul services register -name=RabbitMQ -address=localhost
58 | ```
59 |
--------------------------------------------------------------------------------
/docs/secrets/Readme.md:
--------------------------------------------------------------------------------
1 | # Shared.Secrets
2 |
3 | Disponemos de un servicio que nos permite tener las credenciales alojadas en una aplicación de terceros en vez de tener dichas credenciales en los ficheros de configuración o incluso inyectarlas en el contenedor/server que vamos autilizar de alguna otra manera.
4 |
5 | En nuestro caso, vamos a utilizar Vault de Hashicorp.
6 |
7 | # Implementación de Distribt.Shared.Secrets
8 |
9 | Deberas inyectar en el contenedor o la aplicacion una variable de entorno, que será la API Token (solo desarrollo, implemnta seguridad fuera de desarrollo) para que así tu app pueda comunicarse con Vault.
10 |
11 | Este token debe ser una variable de entonro llamada `VAULT-TOKEN`
12 |
13 | Dentro de nuestro contenedor de dependencias debes llamar a `.AddSecretManager(Iconfiguration)` el cual nos inyectará la interfaz `ISecretManager`.
14 |
15 | ## Recibir secrest desde el gestor de credenciales
16 |
17 | La interfaz `ISecretManager` dispone de un único método, llamado `GeT(path)` el cual deveulve el tipo del objeto que hay en el path, por ejemplo:
18 | ```csharp
19 |
20 | RabbitMQCredentials credentials = await secretManager.Get("rabbitmq/config/connection");
21 | ```
22 | nos devuelve un tipo `RabbitMQCredentials` dentro del path `rabbitmq/cofnig/connection` en vault.
23 |
24 | La librería no tiene funcionalidad de añadir, únicamente de recibir.
25 |
26 | Para insertar secrets en el vault puedes utilizar la UI o el siguiente comando:
27 | ```sh
28 | vault write rabbitmq/config/connection \
29 | connection_uri="http://rabbitmq:15672" \
30 | username="DistribtAdmin" \
31 | password="DistribtPass" \
32 | ```
--------------------------------------------------------------------------------
/src/Api/Private/Distribt.API.Private/Distribt.API.Private.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.API.Private
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Api/Private/Distribt.API.Private/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Setup.API.Key;
2 | using Distribt.Shared.Setup.API.RateLimiting;
3 |
4 | WebApplication app = DefaultDistribtWebApplication.Create(args, webappBuilder =>
5 | {
6 | webappBuilder.Services.AddReverseProxy()
7 | .LoadFromConfig(webappBuilder.Configuration.GetSection("ReverseProxy"));
8 |
9 | webappBuilder.Services.AddApiToken(webappBuilder.Configuration);
10 | });
11 |
12 | app.UseApiTokenMiddleware();
13 | app.UseRateLimiter();
14 | app.MapGet("/", () => "Hello World!");
15 | app.MapGet("/rate-limiting-test", () =>
16 | {
17 | return "Hello World!";
18 | }).RequireRateLimiting(new DistribtRateLimiterPolicy());
19 |
20 | app.MapReverseProxy();
21 |
22 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Api/Private/Distribt.API.Private/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:49228",
7 | "sslPort": 44357
8 | }
9 | },
10 | "profiles": {
11 | "Distribt.API.Private": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "https://localhost:7022;http://localhost:6032",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development",
18 | "VAULT-TOKEN": "vault-distribt-token"
19 | }
20 | },
21 | "IIS Express": {
22 | "commandName": "IISExpress",
23 | "launchBrowser": true,
24 | "environmentVariables": {
25 | "ASPNETCORE_ENVIRONMENT": "Development"
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Api/Private/Distribt.API.Private/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Api/Private/Distribt.API.Private/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "API.Privae",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "ReverseProxy": {
10 | "Routes": {
11 | "ReportsRoute": {
12 | "ClusterId": "OrderCluster",
13 | "Match": {
14 | "Path": "reports/{**catch-all}"
15 | },
16 | "Transforms": [
17 | {
18 | "PathPattern": "{**catch-all}"
19 | }
20 | ]
21 | }
22 | },
23 | "Clusters": {
24 | "OrderCluster": {
25 | "Destinations": {
26 | "OrderCluster/destination1": {
27 | "Address": "https://localhost:60220/"
28 | }
29 | }
30 | }
31 | }
32 | },
33 | "Discovery": {
34 | "Address": "http://localhost:8500"
35 | },
36 | "AllowedHosts": "*",
37 | "ApiKey": {
38 | "clientId": "1",
39 | "value": "b92b0bdf-da95-42a8-a2b1-780ca461aaf3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Api/Public/Distribt.API.Public/Distribt.API.Public.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.API.Public
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Api/Public/Distribt.API.Public/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Subscriptions.Dtos;
2 |
3 | WebApplication app = DefaultDistribtWebApplication.Create(args, webappBuilder =>
4 | {
5 | webappBuilder.Services.AddReverseProxy()
6 | .LoadFromConfig(webappBuilder.Configuration.GetSection("ReverseProxy"));
7 | webappBuilder.Services.AddServiceBusIntegrationPublisher(webappBuilder.Configuration);
8 | });
9 |
10 | app.MapGet("/", () => "Hello World!");
11 |
12 | app.MapPost("/subscribe", async (SubscriptionDto subscriptionDto) =>
13 | {
14 | IIntegrationMessagePublisher publisher = app.Services.GetRequiredService();
15 | await publisher.Publish(subscriptionDto, routingKey: "subscription");
16 | });
17 |
18 |
19 | app.MapReverseProxy();
20 |
21 |
22 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Api/Public/Distribt.API.Public/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:60729",
7 | "sslPort": 44369
8 | }
9 | },
10 | "profiles": {
11 | "Distribt.API.Public": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "https://localhost:7283;http://localhost:5187",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development",
18 | "VAULT-TOKEN": "vault-distribt-token"
19 | }
20 | },
21 | "IIS Express": {
22 | "commandName": "IISExpress",
23 | "launchBrowser": true,
24 | "environmentVariables": {
25 | "ASPNETCORE_ENVIRONMENT": "Development"
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Api/Public/Distribt.API.Public/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Api/Public/Distribt.API.Public/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Api.Public",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "ReverseProxy": {
10 | "Routes": {
11 | "OrderRoute": {
12 | "ClusterId": "OrderCluster",
13 | "Match": {
14 | "Path": "order-ms/{**catch-all}"
15 | },
16 | "Transforms": [
17 | {
18 | "PathPattern": "{**catch-all}"
19 | }
20 | ]
21 | },
22 | "ProductRoute": {
23 | "ClusterId": "ProductCluster",
24 | "Match": {
25 | "Path": "product-ms/{**catch-all}"
26 | },
27 | "Transforms": [
28 | {
29 | "PathPattern": "{**catch-all}"
30 | }
31 | ]
32 | }
33 | },
34 | "Clusters": {
35 | "OrderCluster": {
36 | "Destinations": {
37 | "OrderCluster/destination1": {
38 | "Address": "https://localhost:60220/"
39 | }
40 | }
41 | },
42 | "ProductCluster": {
43 | "Destinations": {
44 | "ProductCluster/destination1": {
45 | "Address": "https://localhost:60320/"
46 | }
47 | }
48 | }
49 | }
50 | },
51 | "Bus": {
52 | "RabbitMQ": {
53 | "Publisher": {
54 | "IntegrationExchange": "api.public.exchange"
55 | }
56 | }
57 | },
58 | "Discovery": {
59 | "Address": "http://localhost:8500"
60 | },
61 | "AllowedHosts": "*"
62 | }
63 |
--------------------------------------------------------------------------------
/src/Services/Emails/Distribt.Services.Emails/Controllers/EmailController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace Distribt.Services.Emails.Controllers;
4 | [ApiController]
5 | [Route("[controller]")]
6 | public class EmailController
7 | {
8 | [HttpPost(Name = "send")]
9 | public Task Send(EmailDto emailDto)
10 | {
11 | //TODO: logic to send the email.
12 | return Task.FromResult(true);
13 | }
14 | }
15 |
16 | public record EmailDto(string from, string to, string subject, string body);
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Emails/Distribt.Services.Emails/Distribt.Services.Emails.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Emails
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Emails/Distribt.Services.Emails/Program.cs:
--------------------------------------------------------------------------------
1 | WebApplication app = DefaultDistribtWebApplication.Create(args);
2 |
3 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Services/Emails/Distribt.Services.Emails/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:60164",
8 | "sslPort": 44316
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Emails": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:60120;http://localhost:60110",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Services/Emails/Distribt.Services.Emails/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Emails/Distribt.Services.Emails/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Email.API",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Discovery": {
10 | "Address": "http://localhost:8500"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.BusinessLogic/Data/External/ProductRepository.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Databases.MongoDb;
2 | using Microsoft.Extensions.Options;
3 | using MongoDB.Bson;
4 | using MongoDB.Bson.Serialization.Attributes;
5 | using MongoDB.Driver;
6 |
7 | namespace Distribt.Services.Orders.BusinessLogic.Data.External;
8 |
9 | public interface IProductRepository
10 | {
11 | Task GetProductName(int id, CancellationToken cancellationToken = default(CancellationToken));
12 |
13 | Task UpsertProductName(int id, string name, CancellationToken cancellationToken = default(CancellationToken));
14 | }
15 |
16 | public class ProductRepository : IProductRepository
17 | {
18 | private readonly MongoClient _mongoClient;
19 | private const string CollectionName = "ProductName";
20 | private readonly IMongoDatabase _mongoDatabase;
21 |
22 | public ProductRepository(IMongoDbConnectionProvider mongoDbConnectionProvider,
23 | IOptions databaseConfiguration)
24 | {
25 | _mongoClient = new MongoClient(mongoDbConnectionProvider.GetMongoUrl());
26 | _mongoDatabase = _mongoClient.GetDatabase(databaseConfiguration.Value.DatabaseName);
27 | }
28 |
29 |
30 | public async Task GetProductName(int id, CancellationToken cancellationToken = default(CancellationToken))
31 | {
32 | IMongoCollection
33 | collection = _mongoDatabase.GetCollection(CollectionName);
34 | FilterDefinition filter = Builders.Filter.Eq("Id", id);
35 | ProductNameEntity entity = await collection.Find(filter).FirstOrDefaultAsync(cancellationToken);
36 | return entity?.Name;
37 | }
38 |
39 | public async Task UpsertProductName(int id, string name,
40 | CancellationToken cancellationToken = default(CancellationToken))
41 | {
42 | IMongoCollection
43 | collection = _mongoDatabase.GetCollection(CollectionName);
44 |
45 | FilterDefinition filter = Builders.Filter.Eq("Id", id);
46 |
47 | ProductNameEntity entity =
48 | await collection.Find(filter).FirstOrDefaultAsync(cancellationToken)
49 | ?? new ProductNameEntity();
50 |
51 | entity.Id ??= id;
52 | entity.Name = name;
53 |
54 | var replaceOne = await collection.ReplaceOneAsync(filter,
55 | entity,
56 | new ReplaceOptions()
57 | {
58 | IsUpsert = true
59 | }, cancellationToken);
60 |
61 | return replaceOne.IsAcknowledged;
62 | }
63 |
64 | private class ProductNameEntity
65 | {
66 | [BsonId] public ObjectId _id { get; set; }
67 | public int? Id { get; set; }
68 | public string? Name { get; set; }
69 |
70 | public ProductNameEntity()
71 | {
72 | _id = ObjectId.GenerateNewId();
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.BusinessLogic/Distribt.Services.Orders.BusinessLogic.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Orders.BusinessLogic
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.BusinessLogic/HealthChecks/ProductsHealthCheck.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Discovery;
2 | using Microsoft.Extensions.Diagnostics.HealthChecks;
3 |
4 | namespace Distribt.Services.Orders.BusinessLogic.HealthChecks;
5 |
6 | public class ProductsHealthCheck : IHealthCheck
7 | {
8 | private readonly IHttpClientFactory _httpClientFactory;
9 | private readonly IServiceDiscovery _discovery;
10 |
11 | public ProductsHealthCheck(IHttpClientFactory httpClientFactory, IServiceDiscovery discovery)
12 | {
13 | _httpClientFactory = httpClientFactory;
14 | _discovery = discovery;
15 | }
16 |
17 | public async Task CheckHealthAsync(HealthCheckContext context,
18 | CancellationToken cancellationToken = new CancellationToken())
19 | {
20 | //TODO: abstract out all the HTTP calls to other distribt microservices #26
21 | HttpClient client = _httpClientFactory.CreateClient();
22 | string productsReadApi =
23 | await _discovery.GetFullAddress(DiscoveryServices.Microservices.ProductsApi.ApiRead, cancellationToken);
24 | client.BaseAddress = new Uri(productsReadApi);
25 | HttpResponseMessage responseMessage = await client.GetAsync($"health", cancellationToken);
26 | if (responseMessage.IsSuccessStatusCode)
27 | {
28 | return HealthCheckResult.Healthy("Product service is healthy");
29 | }
30 |
31 | return HealthCheckResult.Degraded("Product service is down");
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.BusinessLogic/OrdersBusinessLogicDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.BusinessLogic.Data.External;
2 | using Distribt.Services.Orders.BusinessLogic.Services.External;
3 | using Distribt.Shared.Setup.Databases;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace Distribt.Services.Orders.BusinessLogic;
8 |
9 | public static class OrdersBusinessLogicDependencyInjection
10 | {
11 | public static void AddProductService(this IServiceCollection serviceCollection, IConfiguration configuration)
12 | {
13 | serviceCollection.AddDistribtMongoDbConnectionProvider(configuration, "productStore");
14 | serviceCollection.AddScoped();
15 | serviceCollection.AddScoped();
16 | serviceCollection.AddHttpClient();
17 | //For now we do not need redis, as is only for local, in prod I recommend redis.
18 | serviceCollection.AddDistributedMemoryCache();
19 | }
20 |
21 |
22 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.BusinessLogic/Services/External/ProductNameService.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 | using Distribt.Services.Orders.BusinessLogic.Data.External;
3 | using Distribt.Services.Products.Dtos;
4 | using Distribt.Shared.Discovery;
5 | using Microsoft.Extensions.Caching.Distributed;
6 |
7 | namespace Distribt.Services.Orders.BusinessLogic.Services.External;
8 |
9 | public interface IProductNameService
10 | {
11 | Task GetProductName(int id, CancellationToken cancellationToken = default(CancellationToken));
12 | Task SetProductName(int id, string name, CancellationToken cancellationToken = default(CancellationToken));
13 | }
14 |
15 | //TODO: #25
16 | public class ProductNameService : IProductNameService
17 | {
18 | private readonly IProductRepository _productRepository;
19 | private readonly IDistributedCache _cache;
20 | private readonly IHttpClientFactory _httpClientFactory;
21 | private readonly IServiceDiscovery _discovery;
22 |
23 | public ProductNameService(IProductRepository productRepository, IDistributedCache cache,
24 | IHttpClientFactory httpClientFactory, IServiceDiscovery discovery)
25 | {
26 | _productRepository = productRepository;
27 | _cache = cache;
28 | _httpClientFactory = httpClientFactory;
29 | _discovery = discovery;
30 | }
31 |
32 |
33 | public async Task GetProductName(int id, CancellationToken cancellationToken = default(CancellationToken))
34 | {
35 | string? value = await _cache.GetStringAsync($"ORDERS-PRODUCT::{id}", cancellationToken);
36 | if (value!=null)
37 | {
38 | return value;
39 | }
40 | string productName = await RetrieveProductName(id, cancellationToken);
41 |
42 | return productName;
43 | }
44 |
45 | public async Task SetProductName(int id, string name,
46 | CancellationToken cancellationToken = default(CancellationToken))
47 | {
48 | await _productRepository.UpsertProductName(id, name, cancellationToken);
49 | await _cache.RemoveAsync($"ORDERS-PRODUCT::{id}", cancellationToken);
50 | await _cache.SetStringAsync($"ORDERS-PRODUCT::{id}", name, cancellationToken);
51 | }
52 |
53 |
54 | private async Task RetrieveProductName(int id, CancellationToken cancellationToken)
55 | {
56 | string? productName = await _productRepository.GetProductName(id, cancellationToken);
57 |
58 | if (productName == null)
59 | {
60 | FullProductResponse product = await GetProduct(id, cancellationToken);
61 | await SetProductName(id, product.Details.Name, cancellationToken);
62 | productName = product.Details.Name;
63 | }
64 |
65 | return productName;
66 | }
67 |
68 | private async Task GetProduct(int productId, CancellationToken cancellationToken = default(CancellationToken))
69 | {
70 | //TODO: abstract out all the HTTP calls to other distribt microservices #26
71 | HttpClient client = _httpClientFactory.CreateClient();
72 | string productsReadApi =
73 | await _discovery.GetFullAddress(DiscoveryServices.Microservices.ProductsApi.ApiRead, cancellationToken);
74 | client.BaseAddress = new Uri(productsReadApi);
75 |
76 | //TODO: replace exception
77 | return await client.GetFromJsonAsync($"product/{productId}", cancellationToken) ??
78 | throw new ArgumentException("Product does not exist");
79 | }
80 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/Controllers/IntegrationConsumerController.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer.Host;
2 | using Distribt.Shared.Communication.Consumer.Manager;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace Distribt.Services.Orders.Consumer.Controllers;
6 |
7 | [ApiController]
8 | [Route("[controller]")]
9 | public class IntegrationConsumerController : ConsumerController
10 | {
11 | public IntegrationConsumerController(IConsumerManager consumerManager) : base(consumerManager)
12 | {
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/Distribt.Services.Orders.Consumer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Orders.Consumer
9 |
10 |
11 |
12 |
13 | GlobalUsings.cs
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/Handler/OrderCreatedHandler.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Dto;
2 |
3 | namespace Distribt.Services.Orders.Consumer.Handler;
4 |
5 | public class OrderCreatedHandler : IDomainMessageHandler
6 | {
7 | public Task Handle(DomainMessage message, CancellationToken cancelToken = default(CancellationToken))
8 | {
9 | //Refactored for simplicity while doing the videos.
10 | Console.WriteLine($"Order {message.Content.OrderId} created");
11 | //TODO: create order flow
12 | return Task.CompletedTask;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/Handler/ProductModifierHandler.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.BusinessLogic.Services.External;
2 | using Distribt.Services.Products.Dtos;
3 |
4 | namespace Distribt.Services.Orders.Consumer.Handler;
5 |
6 | public class ProductModifierHandler : IIntegrationMessageHandler
7 | {
8 | private readonly IProductNameService _productNameService;
9 |
10 | public ProductModifierHandler(IProductNameService productNameService)
11 | {
12 | _productNameService = productNameService;
13 | }
14 |
15 | public async Task Handle(IntegrationMessage message, CancellationToken cancelToken = default(CancellationToken))
16 | {
17 | await _productNameService.SetProductName(message.Content.ProductId, message.Content.Details.Name, cancelToken);
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.BusinessLogic;
2 | using Distribt.Services.Orders.Consumer.Handler;
3 |
4 | WebApplication app = DefaultDistribtWebApplication.Create(args, builder =>
5 | {
6 | builder.Services.AddProductService(builder.Configuration);
7 |
8 | builder.Services.AddHandlersInAssembly();
9 | builder.Services.AddServiceBusDomainConsumer(builder.Configuration);
10 | builder.Services.AddServiceBusIntegrationConsumer(builder.Configuration);
11 | });
12 |
13 |
14 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:49420",
8 | "sslPort": 44374
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Orders.Consumer": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:7225;http://localhost:5225",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Consumer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Orders.Consumer",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Bus": {
10 | "RabbitMQ": {
11 | "Hostname" : "localhost",
12 | "Consumer": {
13 | "DomainQueue" : "order-domain-queue",
14 | "IntegrationQueue": "order-queue"
15 | }
16 | }
17 | },
18 | "Discovery": {
19 | "Address": "http://localhost:8500"
20 | },
21 | "Database": {
22 | "MongoDb": {
23 | "DatabaseName" : "distribt"
24 | }
25 | },
26 | "AllowedHosts": "*"
27 | }
28 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Dto/Distribt.Services.Orders.Dto.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | Distribt.Services.Orders.Dto
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Dto/OrderRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Services.Orders.Dto;
2 |
3 |
4 | public record CreateOrderRequest(DeliveryDetails DeliveryDetails, PaymentInformation PaymentInformation,
5 | List Products);
6 |
7 | public record CreateOrderResponse(Guid OrderId, string Location);
8 |
9 | public record ProductQuantity(int ProductId, int Quantity);
10 |
11 | public record DeliveryDetails(string Street, string City, string Country);
12 |
13 | public record PaymentInformation(string CardNumber, string ExpireDate, string Security);
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders.Dto/OrderResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Services.Orders.Dto;
2 |
3 | public record OrderResponse(Guid OrderId, string orderStatus, DeliveryDetails DeliveryDetails, PaymentInformation PaymentInformation,
4 | List Products);
5 |
6 | public record ProductQuantityName(int ProductId, int Quantity, string Name);
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Aggregates/MongoMapping.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Events;
2 | using MongoDB.Bson.Serialization;
3 |
4 | namespace Distribt.Services.Orders.Aggregates;
5 |
6 | public static class MongoMapping
7 | {
8 | public static void RegisterClasses()
9 | {
10 | //#22 find a way to register the classes automatically or avoid the registration
11 | BsonClassMap.RegisterClassMap();
12 | BsonClassMap.RegisterClassMap();
13 | BsonClassMap.RegisterClassMap();
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Aggregates/OrderDetails.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Dto;
2 | using Distribt.Services.Orders.Events;
3 | using Distribt.Shared.EventSourcing;
4 |
5 | namespace Distribt.Services.Orders.Aggregates;
6 |
7 | public class OrderDetails : Aggregate, IApply, IApply, IApply, IApply
8 | {
9 | public DeliveryDetails Delivery { get; private set; } = default!;
10 | public PaymentInformation PaymentInformation { get; private set; } = default!;
11 | public List Products { get; private set; } = new List();
12 | public OrderStatus Status { get; private set; }
13 |
14 | public OrderDetails(Guid id) : base(id)
15 | {
16 | }
17 |
18 | public void Apply(OrderCreated ev)
19 | {
20 | Delivery = ev.Delivery;
21 | PaymentInformation = ev.PaymentInformation;
22 | Products = ev.Products;
23 | Status = OrderStatus.Created;
24 | ApplyChange(ev);
25 | }
26 |
27 | public void Apply(OrderPaid ev)
28 | {
29 | Status = OrderStatus.Paid;
30 | ApplyChange(ev);
31 | }
32 |
33 | public void Apply(OrderDispatched ev)
34 | {
35 | Status = OrderStatus.Dispatched;
36 | ApplyChange(ev);
37 | }
38 |
39 | public void Apply(OrderCompleted ev)
40 | {
41 | Status = OrderStatus.Completed;
42 | ApplyChange(ev);
43 | }
44 | }
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Controllers/OrderController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Distribt.Services.Orders.Dto;
3 | using Distribt.Services.Orders.Services;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace Distribt.Services.Orders.Controllers;
7 |
8 | [ApiController]
9 | [Route("[controller]")]
10 | public class OrderController
11 | {
12 | private readonly ICreateOrderService _createOrderService;
13 | private readonly IGetOrderService _getOrderService;
14 | private readonly IOrderPaidService _orderPaidService;
15 | private readonly IOrderDispatchedService _orderDispatchedService;
16 |
17 |
18 | public OrderController(ICreateOrderService createOrderService,
19 | IGetOrderService getOrderService, IOrderPaidService orderPaidService,
20 | IOrderDispatchedService orderDispatchedService)
21 | {
22 | _createOrderService = createOrderService;
23 | _getOrderService = getOrderService;
24 | _orderPaidService = orderPaidService;
25 | _orderDispatchedService = orderDispatchedService;
26 | }
27 |
28 | [HttpGet("{orderId}")]
29 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.OK)]
30 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.NotFound)]
31 | public async Task GetOrder(Guid orderId)
32 | => await _getOrderService.Execute(orderId)
33 | .UseSuccessHttpStatusCode(HttpStatusCode.OK)
34 | .ToActionResult();
35 |
36 |
37 | [HttpGet("getorderstatus/{orderId}")]
38 | public Task GetOrderStatus(Guid orderId)
39 | {
40 | throw new NotImplementedException();
41 | }
42 |
43 | [HttpPost("create")]
44 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Created)]
45 | public async Task CreateOrder(CreateOrderRequest createOrderRequest,
46 | CancellationToken cancellationToken = default(CancellationToken))
47 | {
48 | return await _createOrderService.Execute(createOrderRequest, cancellationToken)
49 | .UseSuccessHttpStatusCode(HttpStatusCode.Created)
50 | .ToActionResult();
51 | }
52 |
53 | [HttpPut("markaspaid")]
54 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Accepted)]
55 | public async Task OrderPaid(Guid orderId,
56 | CancellationToken cancellationToken = default(CancellationToken))
57 | => await _orderPaidService.Execute(orderId, cancellationToken)
58 | .Success().Async().ToActionResult();
59 |
60 | [HttpPut("markasdispatched")]
61 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Accepted)]
62 | public async Task OrderDispatched(Guid orderId, CancellationToken cancellationToken = default(CancellationToken))
63 | => await _orderDispatchedService.Execute(orderId, cancellationToken)
64 | .Success().Async().ToActionResult();
65 |
66 | [HttpPut("markasdelivered")]
67 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Accepted)]
68 | public async Task OrderDelivered(Guid orderId, CancellationToken cancellationToken = default(CancellationToken))
69 | => await _orderDispatchedService.Execute(orderId, cancellationToken)
70 | .Success().Async().ToActionResult();
71 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Data/OrderRepository.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Aggregates;
2 | using Distribt.Shared.EventSourcing;
3 | using Distribt.Shared.EventSourcing.EventStores;
4 |
5 | namespace Distribt.Services.Orders.Data;
6 |
7 | public interface IOrderRepository
8 | {
9 | Task GetById(Guid id, CancellationToken cancellationToken = default(CancellationToken));
10 | Task GetByIdOrDefault(Guid id, CancellationToken cancellationToken = default(CancellationToken));
11 | Task Save(OrderDetails orderDetails, CancellationToken cancellationToken = default(CancellationToken));
12 | }
13 |
14 | public class OrderRepository : AggregateRepository, IOrderRepository
15 | {
16 | public OrderRepository(IEventStore eventStore) : base(eventStore)
17 | {
18 | }
19 |
20 | public async Task GetById(Guid id, CancellationToken cancellationToken = default(CancellationToken))
21 | => await GetByIdAsync(id, cancellationToken);
22 |
23 | public async Task GetByIdOrDefault(Guid id,
24 | CancellationToken cancellationToken = default(CancellationToken))
25 | => await GetByIdOrDefaultAsync(id, cancellationToken);
26 |
27 | public async Task Save(OrderDetails orderDetails, CancellationToken cancellationToken = default(CancellationToken))
28 | => await SaveAsync(orderDetails, cancellationToken);
29 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Distribt.Services.Orders.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Orders
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Events/OrderEvents.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Dto;
2 |
3 | namespace Distribt.Services.Orders.Events;
4 |
5 | public record OrderCreated(DeliveryDetails Delivery, PaymentInformation PaymentInformation,
6 | List Products);
7 |
8 | public record OrderPaid();
9 |
10 | public record OrderDispatched();
11 |
12 | public record OrderCompleted();
13 |
14 |
15 | public enum OrderStatus
16 | {
17 | Created,
18 | Paid,
19 | Dispatched,
20 | Completed,
21 | Failed
22 | }
23 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Events/ProductEvents.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Services.Orders.Events;
2 |
3 |
4 | public record ProductInformation(int ProductId, string ProductName);
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Aggregates;
2 | using Distribt.Services.Orders.BusinessLogic;
3 | using Distribt.Services.Orders.BusinessLogic.HealthChecks;
4 | using Distribt.Services.Orders.Data;
5 | using Distribt.Services.Orders.Services;
6 |
7 | WebApplication app = DefaultDistribtWebApplication.Create(args, webappBuilder =>
8 | {
9 | MongoMapping.RegisterClasses();
10 | webappBuilder.Services.AddServiceBusDomainPublisher(webappBuilder.Configuration);
11 | webappBuilder.Services.AddDistribtMongoDbConnectionProvider(webappBuilder.Configuration);
12 | webappBuilder.Services.AddEventSourcing(webappBuilder.Configuration);
13 | webappBuilder.Services.AddScoped();
14 | webappBuilder.Services.AddScoped();
15 | webappBuilder.Services.AddScoped();
16 | webappBuilder.Services.AddScoped();
17 | webappBuilder.Services.AddScoped();
18 | webappBuilder.Services.AddScoped();
19 | webappBuilder.Services.AddProductService(webappBuilder.Configuration);
20 | webappBuilder.Services.AddHealthChecks().AddCheck(nameof(ProductsHealthCheck));
21 | });
22 |
23 |
24 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:14344",
8 | "sslPort": 44394
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Orders": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:60220;http://localhost:60210",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development",
29 | "VAULT-TOKEN": "vault-distribt-token"
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Services/CreateOrderService.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Distribt.Services.Orders.Aggregates;
3 | using Distribt.Services.Orders.BusinessLogic.Services.External;
4 | using Distribt.Services.Orders.Data;
5 | using Distribt.Services.Orders.Dto;
6 | using Distribt.Services.Orders.Events;
7 | using Distribt.Shared.Setup.Extensions;
8 |
9 | namespace Distribt.Services.Orders.Services;
10 |
11 | public interface ICreateOrderService
12 | {
13 | Task> Execute(CreateOrderRequest createOrder,
14 | CancellationToken cancellationToken = default(CancellationToken));
15 | }
16 |
17 | public class CreateOrderService : ICreateOrderService
18 | {
19 | private readonly IOrderRepository _orderRepository;
20 | private readonly IDomainMessagePublisher _domainMessagePublisher;
21 | private readonly IProductNameService _productNameService;
22 |
23 | public CreateOrderService(IOrderRepository orderRepository, IDomainMessagePublisher domainMessagePublisher,
24 | IProductNameService productNameService)
25 | {
26 | _orderRepository = orderRepository;
27 | _domainMessagePublisher = domainMessagePublisher;
28 | _productNameService = productNameService;
29 | }
30 |
31 |
32 | public async Task> Execute(CreateOrderRequest createOrder,
33 | CancellationToken cancellationToken = default(CancellationToken))
34 | {
35 | return await CreateOrder(createOrder)
36 | .Async()
37 | //On a real scenario:
38 | //validate orders
39 | .Bind(x=> ValidateFraudCheck(x, cancellationToken))
40 | .Bind(x => SaveOrder(x, cancellationToken))
41 | .Then(x => MapToOrderResponse(x)
42 | .Bind(PublishDomainEvent))
43 | .Map(x => new CreateOrderResponse(x.Id, $"order/getorderstatus/{x.Id}"));
44 | }
45 |
46 | private Result CreateOrder(CreateOrderRequest createOrder)
47 | {
48 | Guid createdOrderId = Guid.NewGuid();
49 |
50 | OrderDetails orderDetails = new OrderDetails(createdOrderId);
51 | orderDetails.Apply(new OrderCreated(createOrder.DeliveryDetails, createOrder.PaymentInformation,
52 | createOrder.Products));
53 |
54 | return orderDetails;
55 | }
56 |
57 | private async Task> SaveOrder(OrderDetails orderDetails, CancellationToken cancellationToken)
58 | {
59 | await _orderRepository.Save(orderDetails, cancellationToken);
60 | return orderDetails;
61 | }
62 |
63 | private async Task> MapToOrderResponse(OrderDetails orderDetails)
64 | {
65 | var products = await orderDetails.Products
66 | .SelectAsync(async p => new ProductQuantityName(p.ProductId, p.Quantity,
67 | await _productNameService.GetProductName(p.ProductId)));
68 |
69 |
70 | OrderResponse orderResponse = new OrderResponse(orderDetails.Id, orderDetails.Status.ToString(),
71 | orderDetails.Delivery, orderDetails.PaymentInformation, products.ToList());
72 | return orderResponse;
73 | }
74 |
75 | private async Task> PublishDomainEvent(OrderResponse orderResponse)
76 | {
77 | await _domainMessagePublisher.Publish(orderResponse, routingKey: "order");
78 | return orderResponse.OrderId;
79 | }
80 |
81 | private async Task> ValidateFraudCheck(OrderDetails orderDetails,
82 | CancellationToken cancellationToken)
83 | {
84 | //Simulation of fraud check validation
85 | return orderDetails;
86 | }
87 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Services/GetOrderService.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Aggregates;
2 | using Distribt.Services.Orders.BusinessLogic.Services.External;
3 | using Distribt.Services.Orders.Data;
4 | using Distribt.Services.Orders.Dto;
5 | using Distribt.Shared.Setup.Extensions;
6 |
7 | namespace Distribt.Services.Orders.Services;
8 |
9 | public interface IGetOrderService
10 | {
11 | Task> Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken));
12 | }
13 |
14 | public class GetOrderService : IGetOrderService
15 | {
16 | private readonly IOrderRepository _orderRepository;
17 | private readonly IProductNameService _productNameService;
18 |
19 | public GetOrderService(IOrderRepository orderRepository, IProductNameService productNameService)
20 | {
21 | _orderRepository = orderRepository;
22 | _productNameService = productNameService;
23 | }
24 |
25 |
26 | public async Task> Execute(Guid orderId,
27 | CancellationToken cancellationToken = default(CancellationToken))
28 | {
29 | return await GetOrderDetails(orderId, cancellationToken)
30 | .Bind(x => MapToOrderResponse(x, cancellationToken));
31 | }
32 |
33 | private async Task> GetOrderDetails(Guid orderId,
34 | CancellationToken cancellationToken = default(CancellationToken))
35 | {
36 | OrderDetails? orderDetails = await _orderRepository.GetByIdOrDefault(orderId, cancellationToken);
37 | if (orderDetails == null)
38 | return Result.NotFound($"Order {orderId} not found");
39 |
40 | return orderDetails;
41 | }
42 |
43 | private async Task> MapToOrderResponse(OrderDetails orderDetails,
44 | CancellationToken cancellationToken = default(CancellationToken))
45 | {
46 | //SelectAsync is a custom method, go to the source code to check it out
47 | var products = await orderDetails.Products
48 | .SelectAsync(async p => new ProductQuantityName(p.ProductId, p.Quantity,
49 | await _productNameService.GetProductName(p.ProductId, cancellationToken)));
50 |
51 | return new OrderResponse(orderDetails.Id, orderDetails.Status.ToString(), orderDetails.Delivery,
52 | orderDetails.PaymentInformation, products.ToList());
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Services/OrderDeliveredService.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Aggregates;
2 | using Distribt.Services.Orders.Data;
3 | using Distribt.Services.Orders.Events;
4 |
5 | namespace Distribt.Services.Orders.Services;
6 |
7 | public interface IOrderDeliveredService
8 | {
9 | Task Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken));
10 | }
11 |
12 | public class OrderDeliveredService : IOrderDeliveredService
13 | {
14 | private readonly IOrderRepository _orderRepository;
15 |
16 | public OrderDeliveredService(IOrderRepository orderRepository)
17 | {
18 | _orderRepository = orderRepository;
19 | }
20 |
21 |
22 | public async Task Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken))
23 | {
24 |
25 | OrderDetails orderDetails = await _orderRepository.GetById(orderId, cancellationToken);
26 | orderDetails.Apply(new OrderCompleted());
27 | await _orderRepository.Save(orderDetails, cancellationToken);
28 | return true;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Services/OrderDispatchedService.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Aggregates;
2 | using Distribt.Services.Orders.Data;
3 | using Distribt.Services.Orders.Events;
4 |
5 | namespace Distribt.Services.Orders.Services;
6 |
7 | public interface IOrderDispatchedService
8 | {
9 | Task Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken));
10 | }
11 |
12 | public class OrderDispatchedService : IOrderDispatchedService
13 | {
14 | private readonly IOrderRepository _orderRepository;
15 |
16 | public OrderDispatchedService(IOrderRepository orderRepository)
17 | {
18 | _orderRepository = orderRepository;
19 | }
20 |
21 |
22 | public async Task Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken))
23 | {
24 |
25 | OrderDetails orderDetails = await _orderRepository.GetById(orderId, cancellationToken);
26 | orderDetails.Apply(new OrderDispatched());
27 | await _orderRepository.Save(orderDetails, cancellationToken);
28 | return true;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/Services/OrderPaidService.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Orders.Aggregates;
2 | using Distribt.Services.Orders.Data;
3 | using Distribt.Services.Orders.Events;
4 |
5 | namespace Distribt.Services.Orders.Services;
6 |
7 | public interface IOrderPaidService
8 | {
9 | Task Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken));
10 | }
11 |
12 | public class OrderPaidService : IOrderPaidService
13 | {
14 | private readonly IOrderRepository _orderRepository;
15 |
16 | public OrderPaidService(IOrderRepository orderRepository)
17 | {
18 | _orderRepository = orderRepository;
19 | }
20 |
21 | public async Task Execute(Guid orderId, CancellationToken cancellationToken = default(CancellationToken))
22 | {
23 | OrderDetails orderDetails = await _orderRepository.GetById(orderId, cancellationToken);
24 | orderDetails.Apply(new OrderPaid());
25 | await _orderRepository.Save(orderDetails, cancellationToken);
26 | return true;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Orders/Distribt.Services.Orders/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Orders.API",
3 | "Logging": {
4 | "Console": {
5 | "Enabled": true,
6 | "MinimumLevel": "Error"
7 | },
8 | "Graylog": {
9 | "Enabled": true,
10 | "MinimumLevel": "Error"
11 | }
12 | },
13 | "Bus": {
14 | "RabbitMQ": {
15 | "Publisher": {
16 | "DomainExchange": "order.exchange"
17 | }
18 | }
19 | },
20 | "EventSourcing": {
21 | "DatabaseName": "distribt",
22 | "CollectionName": "EventsOrders"
23 | },
24 | "Database": {
25 | "MongoDb": {
26 | "DatabaseName": "distribt"
27 | }
28 | },
29 | "Discovery": {
30 | "Address": "http://localhost:8500"
31 | },
32 | "AllowedHosts": "*"
33 | }
34 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Read/Distribt.Services.Products.Api.Read.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Products.Api.Read
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Read/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 |
3 | WebApplication app = DefaultDistribtWebApplication.Create(args, builder =>
4 | {
5 | builder.Services.AddDistribtMongoDbConnectionProvider(builder.Configuration)
6 | .AddScoped();
7 | });
8 |
9 |
10 | app.MapGet("product/{productId}", async (int productId, IProductsReadStore readStore)
11 | => await readStore.GetFullProduct(productId)); //TODO: result struct gives an error on minimal api?
12 |
13 |
14 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Read/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:30166",
8 | "sslPort": 44344
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Products.Api.Read": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:60321;http://localhost:60311",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Read/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Read/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Products.API.Read",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Discovery": {
10 | "Address": "http://localhost:8500"
11 | },
12 | "Database": {
13 | "MongoDb": {
14 | "DatabaseName" : "distribt"
15 | }
16 | },
17 | "AllowedHosts": "*"
18 | }
19 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/Controllers/ProductController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Distribt.Services.Products.BusinessLogic.UseCases;
3 | using Distribt.Services.Products.Dtos;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace Distribt.Services.Products.Api.Write.Controllers;
7 |
8 | [ApiController]
9 | [Route("[controller]")]
10 | public class ProductController
11 | {
12 | private readonly IUpdateProductDetails _updateProductDetails;
13 | private readonly ICreateProductDetails _createProductDetails;
14 |
15 | public ProductController(IUpdateProductDetails updateProductDetails, ICreateProductDetails createProductDetails)
16 | {
17 | _updateProductDetails = updateProductDetails;
18 | _createProductDetails = createProductDetails;
19 | }
20 |
21 | [HttpPost(Name = "addproduct")]
22 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Created)]
23 | public async Task AddProduct(CreateProductRequest createProductRequest)
24 | {
25 | CreateProductResponse result = await _createProductDetails.Execute(createProductRequest);
26 |
27 | return result.Success().UseSuccessHttpStatusCode(HttpStatusCode.Created).ToActionResult();
28 | }
29 |
30 |
31 | [HttpPut("updateproductdetails/{id}")]
32 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.OK)]
33 | public async Task UpdateProductDetails(int id, ProductDetails productDetails)
34 | {
35 | bool result = await _updateProductDetails.Execute(id, productDetails);
36 |
37 | return result.Success().UseSuccessHttpStatusCode(HttpStatusCode.OK).ToActionResult();
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/Distribt - Backup.Services.Products.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | true
8 | Distribt.Services.Products
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/Distribt.Services.Products.Api.Write.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Products.Api.Write
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 | using Distribt.Services.Products.BusinessLogic.UseCases;
3 |
4 | WebApplication app = DefaultDistribtWebApplication.Create(args, builder =>
5 | {
6 | builder.Services.AddMySql("distribt")
7 | .AddScoped()
8 | .AddScoped()
9 | .AddScoped()
10 | .AddScoped() //testing purposes
11 | .AddScoped() //testing purposes
12 | .AddServiceBusDomainPublisher(builder.Configuration);
13 | });
14 |
15 | DefaultDistribtWebApplication.Run(app);
16 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:57020",
8 | "sslPort": 44392
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Products.Api.Write": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:60320;http://localhost:60310",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Api.Write/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Products.API.Write",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Bus": {
10 | "RabbitMQ": {
11 | "Publisher": {
12 | "DomainExchange": "products.exchange"
13 | }
14 | }
15 | },
16 | "Discovery": {
17 | "Address": "http://localhost:8500"
18 | },
19 | "AllowedHosts": "*"
20 | }
21 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.BusinessLogic/DataAccess/ProductsWriteStore.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.Dtos;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace Distribt.Services.Products.BusinessLogic.DataAccess;
5 |
6 |
7 | public interface IProductsWriteStore
8 | {
9 | Task UpdateProduct(int id, ProductDetails details);
10 | Task CreateRecord(ProductDetails details);
11 | }
12 |
13 | public class ProductsWriteStore : DbContext, IProductsWriteStore
14 | {
15 | private DbSet Products { get; set; } = null!;
16 |
17 | public ProductsWriteStore(DbContextOptions options) : base(options)
18 | {
19 | }
20 | public async Task UpdateProduct(int id, ProductDetails details)
21 | {
22 | var product = await Products.SingleAsync(a => a.Id == id);
23 | product.Description = details.Description;
24 | product.Name = details.Name;
25 |
26 | await SaveChangesAsync();
27 | }
28 |
29 | public async Task CreateRecord(ProductDetails details)
30 | {
31 | ProductDetailEntity newProduct = new ProductDetailEntity()
32 | {
33 | Description = details.Description,
34 | Name = details.Name
35 | };
36 |
37 | var result = await Products.AddAsync(newProduct);
38 | await SaveChangesAsync();
39 |
40 | return result.Entity.Id ?? throw new ApplicationException("the record has not been inserted in the db");
41 | }
42 |
43 |
44 | private class ProductDetailEntity
45 | {
46 | public int? Id { get; set; }
47 | public string? Name { get; set; }
48 | public string? Description { get; set; }
49 | }
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.BusinessLogic/Distribt.Services.Products.BusinessLogic.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Products.BusinessLogic
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.BusinessLogic/UseCases/CreateProductDetails.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 | using Distribt.Services.Products.Dtos;
3 | using Distribt.Shared.Communication.Publisher.Domain;
4 | using Distribt.Shared.Discovery;
5 |
6 | namespace Distribt.Services.Products.BusinessLogic.UseCases;
7 |
8 |
9 | public interface ICreateProductDetails
10 | {
11 | Task Execute(CreateProductRequest productRequest);
12 | }
13 |
14 | public class CreateProductDetails : ICreateProductDetails
15 | {
16 | private readonly IProductsWriteStore _writeStore;
17 | private readonly IDomainMessagePublisher _domainMessagePublisher;
18 | private readonly IServiceDiscovery _discovery;
19 | private readonly IStockApi _stockApi;
20 | private readonly IWarehouseApi _warehouseApi;
21 |
22 | public CreateProductDetails(IProductsWriteStore writeStore, IDomainMessagePublisher domainMessagePublisher, IServiceDiscovery discovery, IStockApi stockApi, IWarehouseApi warehouseApi)
23 | {
24 | _writeStore = writeStore;
25 | _domainMessagePublisher = domainMessagePublisher;
26 | _discovery = discovery;
27 | _stockApi = stockApi;
28 | _warehouseApi = warehouseApi;
29 | }
30 |
31 |
32 | public async Task Execute(CreateProductRequest productRequest)
33 | {
34 | int productId = await _writeStore.CreateRecord(productRequest.Details);
35 |
36 | await _stockApi.AddStockToProduct(productId, productRequest.Stock);
37 |
38 | await _warehouseApi.ModifySalesPrice(productId, productRequest.Price);
39 |
40 | await _domainMessagePublisher.Publish(new ProductCreated(productId, productRequest), routingKey: "internal");
41 |
42 | string getUrl = await _discovery.GetFullAddress(DiscoveryServices.Microservices.ProductsApi.ApiRead);
43 |
44 | return new CreateProductResponse($"{getUrl}/product/{productId}");
45 | }
46 | }
47 |
48 | public record CreateProductResponse(string Url);
49 |
50 |
51 |
52 |
53 |
54 | //The following two interfaces represent the two services that our product creation depends on.
55 | //Note: we will see sagas in the future.
56 | public interface IStockApi
57 | {
58 | Task AddStockToProduct(int id, int stock)
59 | {
60 | return Task.FromResult(true);
61 | }
62 | }
63 |
64 | public interface IWarehouseApi
65 | {
66 | Task ModifySalesPrice(int id, decimal price)
67 | {
68 | return Task.FromResult(true);
69 | }
70 |
71 | }
72 |
73 | public class ProductsDependencyFakeType : IWarehouseApi, IStockApi
74 | {
75 | //This is a fake type for the DI
76 | }
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.BusinessLogic/UseCases/UpdateProductDetails.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 | using Distribt.Services.Products.Dtos;
3 | using Distribt.Shared.Communication.Publisher.Domain;
4 |
5 | namespace Distribt.Services.Products.BusinessLogic.UseCases;
6 |
7 | public interface IUpdateProductDetails
8 | {
9 | Task Execute(int id, ProductDetails productDetails);
10 | }
11 |
12 | public class UpdateProductDetails : IUpdateProductDetails
13 | {
14 | private readonly IProductsWriteStore _writeStore;
15 | private readonly IDomainMessagePublisher _domainMessagePublisher;
16 |
17 | public UpdateProductDetails(IProductsWriteStore writeStore, IDomainMessagePublisher domainMessagePublisher)
18 | {
19 | _writeStore = writeStore;
20 | _domainMessagePublisher = domainMessagePublisher;
21 | }
22 |
23 | public async Task Execute(int id, ProductDetails productDetails)
24 | {
25 | await _writeStore.UpdateProduct(id, productDetails);
26 |
27 | await _domainMessagePublisher.Publish(new ProductUpdated(id, productDetails), routingKey: "internal");
28 |
29 | return true;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/Controllers/DomainConsumerController.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer.Host;
2 | using Distribt.Shared.Communication.Consumer.Manager;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace Distribt.Services.Products.Consumer.Controllers;
6 |
7 | [ApiController]
8 | [Route("[controller]")]
9 | public class DomainConsumerController : ConsumerController
10 | {
11 | public DomainConsumerController(IConsumerManager consumerManager) : base(consumerManager)
12 | {
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/Distribt.Services.Products.Consumer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Products.Consumer
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/Handlers/ProductCreatedHandler.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 | using Distribt.Services.Products.Dtos;
3 |
4 | namespace Distribt.Services.Products.Consumer.Handlers;
5 |
6 | public class ProductCreatedHandler : IDomainMessageHandler
7 | {
8 | private readonly IProductsReadStore _readStore;
9 | private readonly IIntegrationMessagePublisher _integrationMessagePublisher;
10 |
11 | public ProductCreatedHandler(IProductsReadStore readStore, IIntegrationMessagePublisher integrationMessagePublisher)
12 | {
13 | _readStore = readStore;
14 | _integrationMessagePublisher = integrationMessagePublisher;
15 | }
16 |
17 |
18 | public async Task Handle(DomainMessage message,
19 | CancellationToken cancelToken = default(CancellationToken))
20 | {
21 | await _readStore.UpsertProductViewDetails(message.Content.Id, message.Content.ProductRequest.Details,
22 | cancelToken);
23 | await _readStore.UpdateProductStock(message.Content.Id, message.Content.ProductRequest.Stock, cancelToken);
24 | await _readStore.UpdateProductPrice(message.Content.Id, message.Content.ProductRequest.Price, cancelToken);
25 |
26 | await _integrationMessagePublisher.Publish(
27 | new ProductUpdated(message.Content.Id, message.Content.ProductRequest.Details), routingKey: "external",
28 | cancellationToken: cancelToken);
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/Handlers/ProductUpdatedHandler.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 | using Distribt.Services.Products.Dtos;
3 |
4 | namespace Distribt.Services.Products.Consumer.Handlers;
5 |
6 | public class ProductUpdatedHandler : IDomainMessageHandler
7 | {
8 | private readonly IProductsReadStore _readStore;
9 | private readonly IIntegrationMessagePublisher _integrationMessagePublisher;
10 |
11 | public ProductUpdatedHandler(IProductsReadStore readStore, IIntegrationMessagePublisher integrationMessagePublisher)
12 | {
13 | _readStore = readStore;
14 | _integrationMessagePublisher = integrationMessagePublisher;
15 | }
16 |
17 | public async Task Handle(DomainMessage message, CancellationToken cancelToken = default(CancellationToken))
18 | {
19 |
20 | await _readStore.UpsertProductViewDetails(message.Content.ProductId, message.Content.Details, cancelToken);
21 |
22 | await _integrationMessagePublisher.Publish(
23 | new ProductUpdated(message.Content.ProductId, message.Content.Details), routingKey:"external", cancellationToken: cancelToken);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Products.BusinessLogic.DataAccess;
2 | using Distribt.Services.Products.Consumer.Handlers;
3 |
4 | WebApplication app = DefaultDistribtWebApplication.Create(args, builder =>
5 | {
6 | builder.Services.AddDistribtMongoDbConnectionProvider(builder.Configuration)
7 | .AddScoped();
8 | builder.Services.AddServiceBusIntegrationPublisher(builder.Configuration);
9 | builder.Services.AddHandlersInAssembly();
10 | builder.Services.AddServiceBusDomainConsumer(builder.Configuration);
11 | });
12 |
13 |
14 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:32958",
8 | "sslPort": 44349
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Products.Consumer": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:60322;http://localhost:60312",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Consumer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Products.Consumer",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Bus": {
10 | "RabbitMQ": {
11 | "Consumer": {
12 | "DomainQueue" : "product-domain-queue"
13 | },
14 | "Publisher": {
15 | "IntegrationExchange": "products.exchange"
16 | }
17 | }
18 | },
19 | "Discovery": {
20 | "Address": "http://localhost:8500"
21 | },
22 | "Database": {
23 | "MongoDb": {
24 | "DatabaseName" : "distribt"
25 | }
26 | },
27 | "AllowedHosts": "*"
28 | }
29 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Dtos/Distribt.Services.Products.Dtos.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | Distribt.Services.Products.Dtos
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/Services/Products/Distribt.Services.Products.Dtos/ProductDto.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Services.Products.Dtos;
2 |
3 | public record CreateProductRequest(ProductDetails Details, int Stock, decimal Price);
4 |
5 | public record ProductDetails(string Name, string Description);
6 |
7 | public record FullProductResponse(int Id, ProductDetails Details, int Stock, decimal Price);
8 |
9 | public record ProductUpdated(int ProductId, ProductDetails Details);
10 |
11 | public record ProductCreated(int Id, CreateProductRequest ProductRequest);
12 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/Controllers/IntegrationConsumerController.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer.Host;
2 | using Distribt.Shared.Communication.Consumer.Manager;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace Distribt.Services.Subscriptions.Consumer.Controllers;
6 |
7 | [ApiController]
8 | [Route("[controller]")]
9 | public class IntegrationConsumerController : ConsumerController
10 | {
11 | public IntegrationConsumerController(IConsumerManager consumerManager) : base(consumerManager)
12 | {
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/Distribt.Services.Subscriptions.Consumer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/Handler/SubscriptionHandler.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Subscriptions.Dtos;
2 |
3 |
4 | namespace Distribt.Services.Subscriptions.Consumer.Handler;
5 |
6 | public class SubscriptionHandler : IIntegrationMessageHandler
7 | {
8 | private readonly IDependenciaTest _dependencia;
9 |
10 | public SubscriptionHandler(IDependenciaTest dependencia)
11 | {
12 | _dependencia = dependencia;
13 | }
14 | public Task Handle(IntegrationMessage message, CancellationToken cancelToken = default(CancellationToken))
15 | {
16 | int result = _dependencia.Execute();
17 | Console.WriteLine($"Email {message.Content.Email} successfully subscribed. y la dependencia es {result}");
18 | return Task.CompletedTask;
19 | }
20 | }
21 |
22 | public interface IDependenciaTest
23 | {
24 | int Execute();
25 | }
26 |
27 | public class DependenciaTest : IDependenciaTest
28 | {
29 | public int Execute()
30 | {
31 | return 1;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/Handler/UnSubscriptionHandler.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Subscriptions.Dtos;
2 |
3 | namespace Distribt.Services.Subscriptions.Consumer.Handler;
4 |
5 | public class UnSubscriptionHandler : IIntegrationMessageHandler
6 | {
7 | public Task Handle(IntegrationMessage message, CancellationToken cancelToken = default(CancellationToken))
8 | {
9 | Console.WriteLine($"the email {message.Content.Email} has unsubscribed.");
10 | //TODO: Full use case
11 | return Task.CompletedTask;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/Program.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Services.Subscriptions.Consumer.Handler;
2 |
3 | WebApplication app = DefaultDistribtWebApplication.Create(args, x =>
4 | {
5 | x.Services.AddScoped();
6 | x.Services.AddHandlersInAssembly();
7 | x.Services.AddServiceBusIntegrationConsumer(x.Configuration);
8 | });
9 |
10 |
11 | DefaultDistribtWebApplication.Run(app);
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:16884",
8 | "sslPort": 44324
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Subscriptions.Consumer": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:7073;http://localhost:6073",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development",
20 | "VAULT-TOKEN": "vault-distribt-token"
21 | }
22 | },
23 | "IIS Express": {
24 | "commandName": "IISExpress",
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Consumer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Subscription.Consumer",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Bus": {
10 | "RabbitMQ": {
11 | "Consumer": {
12 | "IntegrationQueue" : "subscription-queue"
13 | }
14 | }
15 | },
16 | "Discovery": {
17 | "Address": "http://localhost:8500"
18 | },
19 | "AllowedHosts": "*"
20 | }
21 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Dtos/Distribt.Services.Subscriptions.Dtos.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Dtos/SubscriptionDto.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Services.Subscriptions.Dtos;
2 |
3 | public record SubscriptionDto(string Email);
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions.Dtos/UnSubscriptionDto.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Services.Subscriptions.Dtos;
2 |
3 | public record UnSubscriptionDto(string Email);
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions/Controllers/SubscriptionController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Distribt.Services.Subscriptions.Dtos;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace Distribt.Services.Subscriptions.Controllers;
6 |
7 | [ApiController]
8 | [Route("[controller]")]
9 | public class SubscriptionController
10 | {
11 | private readonly IIntegrationMessagePublisher _integrationMessagePublisher;
12 |
13 | public SubscriptionController(IIntegrationMessagePublisher integrationMessagePublisher)
14 | {
15 | _integrationMessagePublisher = integrationMessagePublisher;
16 | }
17 |
18 | [HttpPost(Name = "subscribe")]
19 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Accepted)]
20 | public async Task Subscribe(SubscriptionDto subscription)
21 | {
22 | await _integrationMessagePublisher.Publish(subscription);
23 | return true.Success().ToActionResult();
24 | }
25 |
26 | [HttpDelete(Name = "unsubscribe")]
27 | [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.Accepted)]
28 | public Task Unsubscribe(SubscriptionDto subscription)
29 | {
30 | return true.Success().Async().ToActionResult();
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions/Distribt.Services.Subscriptions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | Distribt.Services.Subscriptions
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions/Program.cs:
--------------------------------------------------------------------------------
1 | WebApplication app = DefaultDistribtWebApplication.Create(args, webappBuilder =>
2 | {
3 | webappBuilder.Services.AddServiceBusIntegrationPublisher(webappBuilder.Configuration);
4 | });
5 |
6 | DefaultDistribtWebApplication.Run(app);
7 |
8 | public partial class Program { }
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:25250",
8 | "sslPort": 44303
9 | }
10 | },
11 | "profiles": {
12 | "Distribt.Services.Subscriptions": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:60420;http://localhost:60410",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "IIS Express": {
23 | "commandName": "IISExpress",
24 | "launchBrowser": true,
25 | "launchUrl": "swagger",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Services/Subscriptions/Distribt.Services.Subscriptions/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppName": "Subscription.API",
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft.AspNetCore": "Warning"
7 | }
8 | },
9 | "Bus": {
10 | "RabbitMQ": {
11 | "Publisher": {
12 | "IntegrationExchange": "subscription.exchange"
13 | }
14 | }
15 | },
16 | "Discovery": {
17 | "Address": "http://localhost:8500"
18 | },
19 | "AllowedHosts": "*"
20 | }
21 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Api/Distribt.Shared.Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 | enable
7 | true
8 | Distribt.Shared.Api
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Discovery/ConsulServiceDiscovery.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Consul;
3 | using Microsoft.Extensions.Caching.Memory;
4 |
5 | namespace Distribt.Shared.Discovery;
6 |
7 | public record DiscoveryData(string Server, int Port);
8 |
9 | public interface IServiceDiscovery
10 | {
11 | Task GetFullAddress(string serviceKey, CancellationToken cancellationToken = default);
12 | Task GetDiscoveryData(string serviceKey, CancellationToken cancellationToken = default);
13 | }
14 |
15 | public class ConsulServiceDiscovery : IServiceDiscovery
16 | {
17 | private readonly IConsulClient _client;
18 | private readonly MemoryCache _cache;
19 |
20 | public ConsulServiceDiscovery(IConsulClient client)
21 | {
22 | _client = client;
23 | _cache = new MemoryCache(new MemoryCacheOptions());
24 | }
25 |
26 |
27 | public async Task GetFullAddress(string serviceKey, CancellationToken cancellationToken = default)
28 | {
29 | if (_cache.TryGetValue(serviceKey, out DiscoveryData cachedData))
30 | {
31 | return GetAddressFromData(cachedData);
32 | }
33 |
34 | DiscoveryData data = await GetDiscoveryData(serviceKey, cancellationToken);
35 | return GetAddressFromData(data);
36 | }
37 |
38 | public async Task GetDiscoveryData(string serviceKey, CancellationToken cancellationToken = default)
39 | {
40 | var services = await _client.Catalog.Service(serviceKey, cancellationToken);
41 | if (services.Response != null && services.Response.Any())
42 | {
43 | var service = services.Response.First();
44 |
45 | DiscoveryData data= new DiscoveryData(service.ServiceAddress, service.ServicePort);
46 |
47 | AddToCache(serviceKey, data);
48 |
49 | return data;
50 | }
51 |
52 | throw new ArgumentException($"seems like the service your are trying to access ({serviceKey}) does not exist ");
53 | }
54 |
55 |
56 | private string GetAddressFromData(DiscoveryData data)
57 | {
58 | StringBuilder serviceAddress = new StringBuilder();
59 | serviceAddress.Append(data.Server);
60 | if (data.Port != 0)
61 | {
62 | serviceAddress.Append($":{data.Port}");
63 | }
64 |
65 | string serviceAddressString = serviceAddress.ToString();
66 |
67 |
68 | return serviceAddressString;
69 | }
70 |
71 |
72 | private void AddToCache(string serviceKey, DiscoveryData serviceAddress)
73 | {
74 | _cache.Set(serviceKey, serviceAddress);
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Discovery/DiscoveryDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Consul;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace Distribt.Shared.Discovery;
6 |
7 | public static class DiscoveryDependencyInjection
8 | {
9 | public static IServiceCollection AddDiscovery(this IServiceCollection services, IConfiguration configuration)
10 | {
11 | return services.AddSingleton(provider => new ConsulClient(consulConfig =>
12 | {
13 | var address = configuration["Discovery:Address"];
14 | consulConfig.Address = new Uri(address);
15 | }))
16 | .AddSingleton();
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Discovery/DiscoveryServices.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.Discovery;
2 |
3 | public class DiscoveryServices
4 | {
5 | public const string RabbitMQ = "RabbitMQ";
6 | public const string Secrets = "SecretManager";
7 | public const string MySql = "MySql";
8 | public const string MongoDb = "MongoDb";
9 | public const string Graylog = "Graylog";
10 | public const string OpenTelemetry = "OpenTelemetryCollector";
11 |
12 | public class Microservices
13 | {
14 | public const string Emails = "EmailsApi";
15 | public const string Orders = "OrdersAPi";
16 | public const string Subscriptions = "SubscriptionsAPi";
17 |
18 | public class ProductsApi
19 | {
20 | public const string ApiRead = "ProductsApiRead";
21 | public const string ApiWrite = "ProductsApiWrite";
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Discovery/Distribt.Shared.Discovery.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Distribt.Shared.Discovery
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/Aggregate.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.EventSourcing.Extensions;
2 |
3 | namespace Distribt.Shared.EventSourcing;
4 |
5 |
6 | public class Aggregate
7 | {
8 | private List _changes = new List();
9 | public Guid Id { get; internal set; }
10 |
11 | private string AggregateType => GetType().Name;
12 | public int Version { get; set; } = 0;
13 |
14 | ///
15 | /// this flag is used to identify when an event is being loaded from the DB
16 | /// or when the event is being created as new
17 | ///
18 | private bool ReadingFromHistory { get; set; } = false;
19 |
20 | protected Aggregate(Guid id)
21 | {
22 | Id = id;
23 | }
24 |
25 | internal void Initialize(Guid id)
26 | {
27 | Id = id;
28 | _changes = new List();
29 | }
30 |
31 | public List GetUncommittedChanges()
32 | {
33 | return _changes.Where(a=>a.IsNew).ToList();
34 | }
35 |
36 | public void MarkChangesAsCommitted()
37 | {
38 | _changes.Clear();
39 | }
40 |
41 | protected void ApplyChange(T eventObject)
42 | {
43 | if (eventObject == null)
44 | throw new ArgumentException("you cannot pass a null object into the aggregate");
45 |
46 | Version++;
47 |
48 | AggregateChange change = new AggregateChange(
49 | eventObject,
50 | Id,
51 | eventObject.GetType(),
52 | $"{Id}:{Version}",
53 | Version,
54 | ReadingFromHistory != true
55 | );
56 | _changes.Add(change);
57 |
58 | }
59 |
60 |
61 | public void LoadFromHistory(IList history)
62 | {
63 | if (!history.Any())
64 | {
65 | return;
66 | }
67 |
68 | ReadingFromHistory = true;
69 | foreach (var e in history)
70 | {
71 | ApplyChanges(e.Content);
72 | }
73 | ReadingFromHistory = false;
74 |
75 | Version = history.Last().Version;
76 |
77 | void ApplyChanges(T eventObject)
78 | {
79 | this.AsDynamic()!.Apply(eventObject);
80 | }
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/AggregateChange.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.EventSourcing;
2 |
3 | public record AggregateChange(object Content, Guid Id, Type Type, string TransactionId, int Version, bool IsNew);
4 |
5 | //the dto is the one stored in the DB
6 | public class AggregateChangeDto
7 | {
8 | public object Content { get; set; }
9 | public Guid AggregateId { get; set; }
10 |
11 | public string AggregateType { get; set; }
12 | public string TransactionId { get; set; }
13 | public int AggregateVersion { get; set; }
14 |
15 | public AggregateChangeDto(object content, Guid aggregateId, string aggregateType, string transactionId, int aggregateVersion)
16 | {
17 | Content = content;
18 | AggregateId = aggregateId;
19 | AggregateType = aggregateType;
20 | TransactionId = transactionId;
21 | AggregateVersion = aggregateVersion;
22 | }
23 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/AggregateRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using Distribt.Shared.EventSourcing.EventStores;
3 |
4 | namespace Distribt.Shared.EventSourcing;
5 |
6 | public interface IAggregateRepository
7 | {
8 | Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default(CancellationToken));
9 | Task GetByIdOrDefaultAsync(Guid id, CancellationToken cancellationToken = default(CancellationToken));
10 | Task SaveAsync(TAggregate aggregate, CancellationToken cancellationToken = default(CancellationToken));
11 | }
12 |
13 | public class AggregateRepository : IAggregateRepository
14 | where TAggregate : Aggregate
15 | {
16 | private readonly IEventStore _eventStore;
17 | private string AggregateName => typeof(TAggregate).Name;
18 |
19 | public AggregateRepository(IEventStore eventStore)
20 | {
21 | _eventStore = eventStore;
22 | }
23 |
24 | public async Task GetByIdAsync(Guid id,
25 | CancellationToken cancellationToken = default(CancellationToken))
26 | => await GetByIdOrDefaultAsync(id, cancellationToken) ??
27 | throw new ApplicationException(
28 | "seems that the aggregate cannot be found, please use GetByIdOrDefaultAsync");
29 |
30 |
31 | public async Task GetByIdOrDefaultAsync(Guid id,
32 | CancellationToken cancellationToken = default(CancellationToken))
33 | {
34 | var events =
35 | (await _eventStore.GetEventsForAggregate(AggregateName, id, cancellationToken)).ToList();
36 | if (!events.Any())
37 | return null;
38 |
39 | #pragma warning disable SYSLIB0050
40 | //TODO: #39 remove the obsolete class.
41 | var obj = (TAggregate)FormatterServices.GetUninitializedObject(typeof(TAggregate));
42 | #pragma warning restore SYSLIB0050
43 | obj.Initialize(id);
44 | obj.LoadFromHistory(events);
45 | return obj;
46 | }
47 |
48 | public async Task SaveAsync(TAggregate aggregate, CancellationToken cancellationToken = default(CancellationToken))
49 | {
50 | var uncommittedEvents = aggregate.GetUncommittedChanges();
51 | if (uncommittedEvents.Count == 0) return;
52 |
53 | await _eventStore.SaveEvents(
54 | AggregateName,
55 | aggregate.Id,
56 | uncommittedEvents,
57 | aggregate.Version, cancellationToken);
58 | aggregate.MarkChangesAsCommitted();
59 | }
60 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/Distribt.Shared.EventSourcing.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Distribt.Shared.EventSourcing
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/EventSourcingDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Databases.MongoDb;
2 | using Distribt.Shared.EventSourcing.EventStores;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Options;
6 |
7 | namespace Distribt.Shared.EventSourcing;
8 |
9 | public static class EventSourcingDependencyInjection
10 | {
11 | public static void AddMongoEventSourcing(this IServiceCollection serviceCollection, IConfiguration configuration)
12 | {
13 | //TODO: probably here it should be the addmongodb thingy
14 | serviceCollection.AddTransient(typeof(IAggregateRepository<>), typeof(AggregateRepository<>));
15 | serviceCollection.AddTransient();
16 | serviceCollection.AddTransient();
17 | serviceCollection.Configure(configuration.GetSection("EventSourcing"));
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/EventStores/EventStore.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.EventSourcing.EventStores;
2 |
3 | public interface IEventStore
4 | {
5 | Task SaveEvents(string aggregateType, Guid aggregateId, IList events, int expectedVersion,
6 | CancellationToken cancellationToken = default(CancellationToken));
7 |
8 | Task> GetEventsForAggregate(string aggregateType, Guid aggregateId,
9 | CancellationToken cancellationToken = default(CancellationToken));
10 | }
11 |
12 | public class EventStore : IEventStore
13 | {
14 | private readonly IEventStoreManager _eventStoreManager;
15 |
16 | public EventStore(IEventStoreManager eventStoreManager)
17 | {
18 | _eventStoreManager = eventStoreManager;
19 | }
20 |
21 | public async Task SaveEvents(string aggregateType, Guid aggregateId, IList events,
22 | int expectedVersion, CancellationToken cancellationToken = default(CancellationToken))
23 | {
24 | await _eventStoreManager.SaveEvents(aggregateId, aggregateType, events, expectedVersion, cancellationToken);
25 | }
26 |
27 | public Task> GetEventsForAggregate(string aggregateType, Guid aggregateId,
28 | CancellationToken cancellationToken = default(CancellationToken))
29 | {
30 | return _eventStoreManager.GetEventsForAggregate(aggregateType, aggregateId, cancellationToken);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/EventStores/IEventStoreManager.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.EventSourcing.EventStores;
2 |
3 | public interface IEventStoreManager
4 | {
5 | Task SaveEvents(Guid id, string aggregateType, IEnumerable events, int expectedVersion, CancellationToken cancellationToken = default(CancellationToken));
6 | Task> GetEventsForAggregate(string aggregateType, Guid id, CancellationToken cancellationToken = default(CancellationToken));
7 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/EventStores/MongoEventStoreManager.cs:
--------------------------------------------------------------------------------
1 | using System.Data;
2 | using Distribt.Shared.Databases.MongoDb;
3 | using Distribt.Shared.EventSourcing.Extensions;
4 | using Microsoft.Extensions.Options;
5 | using MongoDB.Driver;
6 |
7 | namespace Distribt.Shared.EventSourcing.EventStores;
8 |
9 | public class MongoEventStoreManager : IEventStoreManager
10 | {
11 | private readonly IMongoDatabase _mongoDatabase;
12 | private readonly MongoEventStoreConfiguration _mongoDbMongoEventStoreConfiguration;
13 |
14 | private IMongoCollection _changes =>
15 | _mongoDatabase.GetCollection(_mongoDbMongoEventStoreConfiguration.CollectionName);
16 |
17 |
18 | public MongoEventStoreManager(IMongoDbConnectionProvider mongoDbConnectionProvider, IOptions mongoDbEventStoreOptions)
19 | {
20 | _mongoDbMongoEventStoreConfiguration = mongoDbEventStoreOptions.Value;
21 | //TODO: #29; investigate the usage of IMongoDatabase
22 | var mongoClient = new MongoClient(mongoDbConnectionProvider.GetMongoUrl());
23 | _mongoDatabase = mongoClient.GetDatabase(_mongoDbMongoEventStoreConfiguration.DatabaseName);;
24 |
25 | }
26 |
27 |
28 | public async Task SaveEvents(Guid id, string aggregateType, IEnumerable events, int expectedVersion, CancellationToken cancellationToken = default(CancellationToken))
29 | {
30 | var collection = _changes;
31 | await CreateIndex(collection);
32 | var latestAggregate = await collection
33 | .Find(d => d.AggregateType == aggregateType && d.AggregateId == id)
34 | .SortByDescending(d => d.AggregateVersion)
35 | .Limit(1)
36 | .FirstOrDefaultAsync(cancellationToken);
37 | var latestAggregateVersion = latestAggregate?.AggregateVersion;
38 |
39 | if (latestAggregateVersion.HasValue && latestAggregateVersion >= expectedVersion)
40 | throw new DBConcurrencyException("Concurrency exception");
41 |
42 | var dtos = events.Select(x =>
43 | AggregateMappers.ToTypedAggregateChangeDto(id, aggregateType, x)
44 | );
45 |
46 | await collection.InsertManyAsync(dtos, new InsertManyOptions() { IsOrdered = true }, cancellationToken);
47 | }
48 |
49 | public async Task> GetEventsForAggregate(string aggregateType, Guid id, CancellationToken cancellationToken = default(CancellationToken))
50 | {
51 | List? result = await _changes
52 | .Find(aggregate => aggregate.AggregateType == aggregateType && aggregate.AggregateId == id)
53 | .SortBy(a => a.AggregateVersion)
54 | .ToListAsync(cancellationToken);
55 |
56 | return result.Select(AggregateMappers.ToAggregateChange);
57 | }
58 |
59 | private static async Task CreateIndex(IMongoCollection collection)
60 | {
61 | await collection.Indexes.CreateOneAsync(new CreateIndexModel(
62 | Builders.IndexKeys
63 | .Ascending(i => i.AggregateType)
64 | .Ascending(i => i.AggregateId)
65 | .Ascending(i => i.AggregateVersion),
66 | new CreateIndexOptions { Unique = true, Name = "_Aggregate_Type_Id_Version_" }))
67 | .ConfigureAwait(false);
68 | }
69 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/Extensions/AggregateMappers.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.EventSourcing.Extensions;
2 |
3 | public static class AggregateMappers
4 | {
5 | public static AggregateChange ToAggregateChange(AggregateChangeDto change)
6 | {
7 | return new AggregateChange(
8 | change.Content,
9 | change.AggregateId,
10 | change.GetType(),
11 | change.TransactionId,
12 | change.AggregateVersion,
13 | false
14 | );
15 | }
16 |
17 | public static AggregateChangeDto ToTypedAggregateChangeDto(
18 | Guid Id,
19 | string aggregateType,
20 | AggregateChange change
21 | )
22 | {
23 | return new AggregateChangeDto(
24 | change.Content,
25 | Id,
26 | aggregateType,
27 | change.TransactionId,
28 | change.Version
29 | );
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.EventSourcing/IApply.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.EventSourcing;
2 |
3 | public interface IApply
4 | {
5 | void Apply(T ev);
6 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Logging/ConfigureLogger.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Discovery;
2 | using Distribt.Shared.Logging.Loggers;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.Hosting;
5 | using Serilog;
6 |
7 | namespace Distribt.Shared.Logging;
8 |
9 | public static class ConfigureLogger
10 | {
11 | public static IHostBuilder ConfigureSerilog(this IHostBuilder builder, IServiceDiscovery discovery)
12 | => builder.UseSerilog((context, loggerConfiguration)
13 | => ConfigureSerilogLogger(loggerConfiguration, context.Configuration, discovery));
14 |
15 | private static LoggerConfiguration ConfigureSerilogLogger(LoggerConfiguration loggerConfiguration,
16 | IConfiguration configuration, IServiceDiscovery discovery)
17 | {
18 | GraylogLoggerConfiguration graylogLogger = new GraylogLoggerConfiguration();
19 | configuration.GetSection("Logging:Graylog").Bind(graylogLogger);
20 | DiscoveryData discoveryData = discovery.GetDiscoveryData(DiscoveryServices.Graylog).Result;
21 | graylogLogger.Host = discoveryData.Server;
22 | graylogLogger.Port = discoveryData.Port;
23 | ConsoleLoggerConfiguration consoleLogger = new ConsoleLoggerConfiguration();
24 | configuration.GetSection("Logging:Console").Bind(consoleLogger);
25 |
26 | return loggerConfiguration
27 | .AddConsoleLogger(consoleLogger)
28 | .AddGraylogLogger(graylogLogger);
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Logging/Distribt.Shared.Logging.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Distribt.Shared.Logging
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Logging/Loggers/ConsoleLoggerConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Serilog.Events;
2 |
3 | namespace Distribt.Shared.Logging.Loggers;
4 |
5 | public class ConsoleLoggerConfiguration
6 | {
7 | public bool Enabled { get; set; } = false;
8 | public LogEventLevel MinimumLevel { get; set; }
9 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Logging/Loggers/GraylogLoggerConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Serilog.Events;
2 |
3 | namespace Distribt.Shared.Logging.Loggers;
4 |
5 | public class GraylogLoggerConfiguration
6 | {
7 | public bool Enabled { get; set; } = false;
8 | public string Host { get; set; } = "";
9 | public int Port { get; set; }
10 | public LogEventLevel MinimumLevel { get; set; }
11 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Logging/Loggers/LoggerConfigurationExtensions.cs:
--------------------------------------------------------------------------------
1 | using Serilog;
2 | using Serilog.Sinks.Graylog;
3 | using Serilog.Sinks.Graylog.Core.Transport;
4 |
5 | namespace Distribt.Shared.Logging.Loggers;
6 |
7 | public static class LoggerConfigurationExtensions
8 | {
9 | public static LoggerConfiguration AddConsoleLogger(this LoggerConfiguration loggerConfiguration,
10 | ConsoleLoggerConfiguration consoleLoggerConfiguration)
11 | {
12 | return consoleLoggerConfiguration.Enabled
13 | ? loggerConfiguration.WriteTo.Console(consoleLoggerConfiguration.MinimumLevel)
14 | : loggerConfiguration;
15 | }
16 |
17 | public static LoggerConfiguration AddGraylogLogger(this LoggerConfiguration loggerConfiguration,
18 | GraylogLoggerConfiguration graylogLoggerConfiguration)
19 | {
20 | return graylogLoggerConfiguration.Enabled
21 | ? loggerConfiguration.WriteTo.Graylog(graylogLoggerConfiguration.Host, graylogLoggerConfiguration.Port,
22 | TransportType.Udp, graylogLoggerConfiguration.MinimumLevel)
23 | : loggerConfiguration;
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Secrets/Distribt.Shared.Secrets.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Distribt.Shared.Secrets
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Secrets/Extensions/ObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.Secrets.Extensions;
2 |
3 | public static class ObjectExtensions
4 | {
5 | public static T ToObject(this IDictionary source) where T : new()
6 | {
7 | var someObject = new T();
8 | var someObjectType = someObject.GetType();
9 |
10 | foreach (var item in source)
11 | {
12 | someObjectType
13 | .GetProperty(item.Key)!
14 | .SetValue(someObject, item.Value, null);
15 | }
16 | return someObject;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Secrets/VaultDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace Distribt.Shared.Secrets;
5 |
6 | public static class VaultDependencyInjection
7 | {
8 | ///
9 | /// add vault service to the service collection
10 | /// discovered url is optional as it will be calculated on the setup project, but only if that project is in use.
11 | ///
12 | public static void AddVaultService(this IServiceCollection serviceCollection, IConfiguration configuration, string? discoveredUrl = null)
13 | {
14 | serviceCollection.Configure(configuration.GetSection("SecretManager"));
15 | serviceCollection.PostConfigure(settings =>
16 | {
17 | if(!string.IsNullOrWhiteSpace(discoveredUrl))
18 | settings.UpdateUrl(discoveredUrl);
19 | });
20 | serviceCollection.AddScoped();
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Secrets/VaultSecretManager.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Secrets.Extensions;
2 | using Microsoft.Extensions.Options;
3 | using VaultSharp;
4 | using VaultSharp.V1.AuthMethods.Token;
5 | using VaultSharp.V1.Commons;
6 | using VaultSharp.V1.SecretsEngines;
7 |
8 | namespace Distribt.Shared.Secrets;
9 |
10 | public interface ISecretManager
11 | {
12 | Task Get(string path) where T : new();
13 | Task GetRabbitMQCredentials(string roleName);
14 | }
15 |
16 | internal class VaultSecretManager : ISecretManager
17 | {
18 | private readonly VaultSettings _vaultSettings;
19 |
20 | public VaultSecretManager(IOptions vaultSettings)
21 | {
22 | _vaultSettings = vaultSettings.Value with { TokenApi = GetTokenFromEnvironmentVariable() };
23 | }
24 |
25 | public async Task Get(string path)
26 | where T : new()
27 | {
28 | VaultClient client = new VaultClient(new VaultClientSettings(_vaultSettings.VaultUrl,
29 | new TokenAuthMethodInfo(_vaultSettings.TokenApi)));
30 |
31 | Secret kv2Secret = await client.V1.Secrets.KeyValue.V2
32 | .ReadSecretAsync(path: path, mountPoint: "secret");
33 | var returnedData = kv2Secret.Data.Data;
34 |
35 | return returnedData.ToObject();
36 | }
37 |
38 | public async Task GetRabbitMQCredentials(string roleName)
39 | {
40 | VaultClient client = new VaultClient(new VaultClientSettings(_vaultSettings.VaultUrl,
41 | new TokenAuthMethodInfo(_vaultSettings.TokenApi)));
42 |
43 | Secret secret = await client.V1.Secrets.RabbitMQ
44 | .GetCredentialsAsync(roleName, "rabbitmq");
45 | return secret.Data;
46 | }
47 |
48 | private string GetTokenFromEnvironmentVariable()
49 | => Environment.GetEnvironmentVariable("VAULT-TOKEN")
50 | ?? throw new NotImplementedException("please specify the VAULT-TOKEN env_var");
51 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Secrets/VaultSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.Secrets;
2 |
3 | public record VaultSettings
4 | {
5 | public string? VaultUrl { get; private set; }
6 | public string? TokenApi { get; init; }
7 |
8 | public void UpdateUrl(string url)
9 | {
10 | VaultUrl = url;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Serialization/Distribt.Shared.Serialization.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Distribt.Shared.Serialization
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Serialization/ISerializer.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.Serialization;
2 |
3 | public interface ISerializer
4 | {
5 | T DeserializeObject(string input);
6 | string SerializeObject(T obj);
7 | T DeserializeObject(byte[] input) where T : class;
8 | byte[] SerializeObjectToByteArray(T obj);
9 | object? DeserializeObject(byte[] input, Type myType);
10 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Serialization/SerializationDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Distribt.Shared.Serialization;
4 |
5 | public static class SerializationDependencyInjection
6 | {
7 | public static void AddSerializer(this IServiceCollection serviceCollection)
8 | {
9 | serviceCollection.AddTransient();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Serialization/Serializer.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Newtonsoft.Json;
3 | using JsonSerializer = System.Text.Json.JsonSerializer;
4 |
5 | namespace Distribt.Shared.Serialization;
6 |
7 | public class Serializer : ISerializer
8 | {
9 | private static readonly Encoding Encoding = new UTF8Encoding(false);
10 |
11 | private static readonly JsonSerializerSettings DefaultSerializerSettings =
12 | new JsonSerializerSettings
13 | {
14 | TypeNameHandling = TypeNameHandling.Auto
15 | };
16 |
17 | private const int DefaultBufferSize = 1024;
18 |
19 | private readonly Newtonsoft.Json.JsonSerializer _jsonSerializer;
20 |
21 | public Serializer() : this(DefaultSerializerSettings)
22 | {
23 | }
24 |
25 | public Serializer(JsonSerializerSettings serializerSettings)
26 | {
27 | _jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(serializerSettings);
28 | }
29 |
30 | public T DeserializeObject(string input)
31 | {
32 | return JsonSerializer.Deserialize(input) ?? throw new InvalidOperationException();
33 | }
34 |
35 | public T DeserializeObject(byte[] input) where T : class
36 | {
37 | return (DeserializeByteArrayToObject(input) as T)!;
38 | }
39 |
40 | public object DeserializeObject(byte[] input, Type type)
41 | {
42 | using var memoryStream = new MemoryStream(input, false);
43 | using var streamReader = new StreamReader(memoryStream, Encoding, false, DefaultBufferSize, true);
44 | using var reader = new JsonTextReader(streamReader);
45 | return _jsonSerializer.Deserialize(reader, type) ?? throw new InvalidOperationException();
46 | }
47 |
48 | private object DeserializeByteArrayToObject(byte[] input)
49 | {
50 | using var memoryStream = new MemoryStream(input, false);
51 | using var streamReader = new StreamReader(memoryStream, Encoding, false, DefaultBufferSize, true);
52 | using var reader = new JsonTextReader(streamReader);
53 | return _jsonSerializer.Deserialize(reader, typeof(T)) ?? throw new InvalidOperationException();
54 | }
55 |
56 |
57 | public string SerializeObject(T obj)
58 | {
59 | return JsonSerializer.Serialize(obj);
60 | }
61 |
62 | public byte[] SerializeObjectToByteArray(T obj)
63 | {
64 | using var memoryStream = new MemoryStream(DefaultBufferSize);
65 | using (var streamWriter = new StreamWriter(memoryStream, Encoding, DefaultBufferSize, true))
66 | using (var jsonWriter = new JsonTextWriter(streamWriter))
67 | {
68 | jsonWriter.Formatting = _jsonSerializer.Formatting;
69 | _jsonSerializer.Serialize(jsonWriter, obj, obj!.GetType());
70 | }
71 |
72 | return memoryStream.ToArray();
73 | }
74 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/API/DefaultDistribtWebApplication.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Logging;
2 | using Distribt.Shared.Setup.Observability;
3 | using HealthChecks.UI.Client;
4 | using Microsoft.AspNetCore.Diagnostics.HealthChecks;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.Hosting;
7 | using Serilog;
8 |
9 |
10 | namespace Distribt.Shared.Setup.API;
11 |
12 | public static class DefaultDistribtWebApplication
13 | {
14 | public static WebApplication Create(string[] args, Action? webappBuilder = null)
15 | {
16 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
17 |
18 | builder.Configuration.AddConfiguration(HealthCheckHelper.BuildBasicHealthCheck());
19 | builder.Services.AddHealthChecks();
20 | builder.Services.AddHealthChecksUI().AddInMemoryStorage();
21 | builder.Services.AddControllers();
22 | builder.Services.AddEndpointsApiExplorer();
23 | builder.Services.AddSwaggerGen();
24 | builder.Services.AddRouting(x => x.LowercaseUrls = true);
25 | builder.Services.AddSerializer();
26 |
27 | builder.Services.AddServiceDiscovery(builder.Configuration);
28 | builder.Services.AddSecretManager(builder.Configuration);
29 | builder.Services.AddLogging(logger => logger.AddSerilog());
30 | builder.Services.AddTracing(builder.Configuration);
31 | builder.Services.AddMetrics(builder.Configuration);
32 |
33 | builder.Host.ConfigureSerilog(builder.Services.BuildServiceProvider().GetRequiredService());
34 |
35 | if (webappBuilder != null)
36 | {
37 | webappBuilder.Invoke(builder);
38 | }
39 |
40 | return builder.Build();
41 | }
42 |
43 | public static void Run(WebApplication webApp)
44 | {
45 | if (webApp.Environment.IsDevelopment())
46 | {
47 | webApp.UseSwagger();
48 | webApp.UseSwaggerUI();
49 | }
50 |
51 | webApp.MapHealthChecks("/health");
52 |
53 | webApp.UseHealthChecks("/health", new HealthCheckOptions()
54 | {
55 | Predicate = _ => true,
56 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
57 | });
58 |
59 | webApp.UseHealthChecksUI(config =>
60 | {
61 | config.UIPath = "/health-ui";
62 | });
63 |
64 |
65 | webApp.UseHttpsRedirection();
66 | webApp.UseAuthorization();
67 | webApp.MapControllers();
68 | webApp.Run();
69 | }
70 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/API/HealthCheckHelper.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace Distribt.Shared.Setup.API;
4 |
5 | public static class HealthCheckHelper
6 | {
7 | public static IConfiguration BuildBasicHealthCheck()
8 | {
9 | var myConfiguration = new Dictionary
10 | {
11 | {"HealthChecksUI:HealthChecks:0:Name", "self"},
12 | {"HealthChecksUI:HealthChecks:0:Uri", "/health"},
13 | };
14 |
15 | return new ConfigurationBuilder()
16 | .AddInMemoryCollection(myConfiguration)
17 | .Build();
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/API/Key/ApiKeyConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.Setup.API.Key;
2 |
3 | public class ApiKeyConfiguration
4 | {
5 | public string? ClientId { get; init; }
6 | public string? Value { get; init; }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/API/Key/ApiKeyDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace Distribt.Shared.Setup.API.Key;
4 |
5 | public static class ApiKeyDependencyInjection
6 | {
7 | public static IServiceCollection AddApiToken(this IServiceCollection services, IConfiguration configuration)
8 | {
9 | return services.Configure(configuration.GetSection("ApiKey"));
10 | }
11 |
12 | public static void UseApiTokenMiddleware(this WebApplication webApp)
13 | {
14 | //Do not act on /health or /health-ui
15 | webApp.UseWhen(context => !context.Request.Path.StartsWithSegments("/health"),
16 | appBuilder => appBuilder.UseMiddleware()
17 | );
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/API/Key/ApiKeyMiddleware.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.Extensions.Options;
3 | using Microsoft.Extensions.Primitives;
4 |
5 | namespace Distribt.Shared.Setup.API.Key;
6 |
7 | public class ApiKeyMiddleware
8 | {
9 | private readonly RequestDelegate _next;
10 |
11 |
12 | public ApiKeyMiddleware(RequestDelegate next)
13 | {
14 | _next = next;
15 | }
16 |
17 | public async Task Invoke(HttpContext context, IOptions apiToken)
18 | {
19 |
20 | if (context.Request.Headers.TryGetValue("apiKey", out StringValues apiKey))
21 | {
22 | if (apiKey == apiToken.Value.Value)
23 | await _next(context);
24 | else
25 | ReturnApKeyNotfound();
26 | }
27 | else
28 | {
29 | ReturnApKeyNotfound();
30 | }
31 |
32 | void ReturnApKeyNotfound()
33 | {
34 | throw new UnauthorizedAccessException("The API Key is missing");
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/API/RateLimiting/DistribtRateLimiterPolicy.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.RateLimiting;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.RateLimiting;
4 |
5 | namespace Distribt.Shared.Setup.API.RateLimiting;
6 |
7 | public class DistribtRateLimiterPolicy : IRateLimiterPolicy
8 | {
9 | public RateLimitPartition GetPartition(HttpContext httpContext)
10 | {
11 | return RateLimitPartition.GetFixedWindowLimiter(
12 | partitionKey: httpContext.Request.Headers["apiKey"].ToString(),
13 | partition => new FixedWindowRateLimiterOptions
14 | {
15 | PermitLimit = 2,
16 | Window = TimeSpan.FromMinutes(60),
17 | });
18 | }
19 |
20 | public Func? OnRejected { get; } =
21 | (context, _) =>
22 | {
23 | context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
24 | context.HttpContext.Response.WriteAsync("Lots of calls, please try later");
25 | return new ValueTask();
26 | };
27 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Databases/MongoDb.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Databases.MongoDb;
2 | using Microsoft.Extensions.Configuration;
3 |
4 | namespace Distribt.Shared.Setup.Databases;
5 |
6 | public static class MongoDb
7 | {
8 | public static IServiceCollection AddDistribtMongoDbConnectionProvider(this IServiceCollection serviceCollection,
9 | IConfiguration configuration, string name = "mongodb")
10 | {
11 | return serviceCollection
12 | .AddMongoDbConnectionProvider()
13 | .AddMongoDbDatabaseConfiguration(configuration)
14 | .AddMongoHealthCheck(name);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Databases/MySql.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Distribt.Shared.Databases.MySql;
3 |
4 |
5 | namespace Distribt.Shared.Setup.Databases;
6 |
7 | public static class MySql
8 | {
9 | public static IServiceCollection AddMySql(this IServiceCollection serviceCollection, string databaseName)
10 | where T : DbContext
11 | {
12 | return serviceCollection
13 | .AddMySqlDbContext(serviceProvider => GetConnectionString(serviceProvider, databaseName))
14 | .AddMysqlHealthCheck(serviceProvider => GetConnectionString(serviceProvider, databaseName));
15 | }
16 |
17 | private static async Task GetConnectionString(IServiceProvider serviceProvider, string databaseName)
18 | {
19 | ISecretManager secretManager = serviceProvider.GetRequiredService();
20 | IServiceDiscovery serviceDiscovery = serviceProvider.GetRequiredService();
21 |
22 | DiscoveryData mysqlData = await serviceDiscovery.GetDiscoveryData(DiscoveryServices.MySql);
23 | MySqlCredentials credentials = await secretManager.Get("mysql");
24 |
25 | return
26 | $"Server={mysqlData.Server};Port={mysqlData.Port};Database={databaseName};Uid={credentials.username};password={credentials.password};";
27 | }
28 |
29 |
30 | private record MySqlCredentials
31 | {
32 | public string username { get; init; } = null!;
33 | public string password { get; init; } = null!;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Distribt.Shared.Setup.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Distribt.Shared.Setup
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Extensions/LinqExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Distribt.Shared.Setup.Extensions;
2 |
3 | public static class LinqExtensions
4 | {
5 | //Source: https://stackoverflow.com/a/51964200/2320094
6 | public static async Task> SelectAsync(this IEnumerable source,
7 | Func> method, int concurrency = int.MaxValue)
8 | {
9 | var semaphore = new SemaphoreSlim(concurrency);
10 | try
11 | {
12 | return await Task.WhenAll(source.Select(async s =>
13 | {
14 | try
15 | {
16 | await semaphore.WaitAsync();
17 | return await method(s);
18 | }
19 | finally
20 | {
21 | semaphore.Release();
22 | }
23 | }));
24 | }
25 | finally
26 | {
27 | semaphore.Dispose();
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Distribt.Shared.Setup;
2 | global using Distribt.Shared.Serialization;
3 | global using Distribt.Shared.Secrets;
4 | global using Distribt.Shared.Communication.Consumer.Handler;
5 | global using Distribt.Shared.Communication.Messages;
6 | global using Distribt.Shared.Communication.Publisher.Integration;
7 | global using Distribt.Shared.Communication.Publisher.Domain;
8 | global using Distribt.Shared.Setup.Services;
9 | global using Distribt.Shared.Discovery;
10 | global using Distribt.Shared.Setup.API;
11 | global using Microsoft.AspNetCore.Builder;
12 | global using System;
13 | global using System.Threading;
14 | global using System.Threading.Tasks;
15 | global using Microsoft.Extensions.Logging;
16 | global using Microsoft.Extensions.DependencyInjection;
17 | global using Distribt.Shared.Setup.Databases;
18 | global using ROP;
19 | global using ROP.APIExtensions;
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Observability/OpenTelemetry.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using Microsoft.Extensions.Hosting;
3 | using OpenTelemetry.Logs;
4 | using OpenTelemetry.Metrics;
5 | using OpenTelemetry.Resources;
6 | using OpenTelemetry.Trace;
7 |
8 | namespace Distribt.Shared.Setup.Observability;
9 |
10 | public static class OpenTelemetry
11 | {
12 | private static string? _openTelemetryUrl;
13 |
14 | public static void AddTracing(this IServiceCollection serviceCollection, IConfiguration configuration)
15 | {
16 | serviceCollection.AddOpenTelemetryTracing(builder => builder
17 | .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(configuration["AppName"]))
18 | .AddAspNetCoreInstrumentation()
19 | .AddOtlpExporter(exporter =>
20 | {
21 | string url = GetOpenTelemetryCollectorUrl(serviceCollection.BuildServiceProvider()).Result;
22 | exporter.Endpoint = new Uri(url);
23 | })
24 | );
25 | ;
26 | }
27 |
28 | public static void AddMetrics(this IServiceCollection serviceCollection, IConfiguration configuration)
29 | {
30 | serviceCollection.AddOpenTelemetryMetrics(builder => builder
31 | .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(configuration["AppName"]))
32 | .AddAspNetCoreInstrumentation()
33 | .AddOtlpExporter(exporter =>
34 | {
35 | string url = GetOpenTelemetryCollectorUrl(serviceCollection.BuildServiceProvider()).Result;
36 | exporter.Endpoint = new Uri(url);
37 | }));
38 | }
39 |
40 | // Not used in distribt, added here because of the blogpost.
41 | public static void AddLogging(this IHostBuilder builder, IConfiguration configuration)
42 | {
43 | builder.ConfigureLogging(logging => logging
44 | //Next line optional to remove other providers
45 | .ClearProviders()
46 | .AddOpenTelemetry(options =>
47 | {
48 | options.IncludeFormattedMessage = true;
49 | options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(configuration["AppName"]));
50 | options.AddConsoleExporter();
51 | }));
52 | }
53 |
54 | private static async Task GetOpenTelemetryCollectorUrl(IServiceProvider serviceProvider)
55 | {
56 | if (_openTelemetryUrl != null)
57 | return _openTelemetryUrl;
58 |
59 |
60 | var serviceDiscovery = serviceProvider.GetService();
61 | string openTelemetryLocation = await serviceDiscovery?.GetFullAddress(DiscoveryServices.OpenTelemetry)!;
62 | _openTelemetryUrl = $"http://{openTelemetryLocation}";
63 | return _openTelemetryUrl;
64 | }
65 |
66 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Services/EventSourcing.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.EventSourcing;
2 | using Microsoft.Extensions.Configuration;
3 |
4 | namespace Distribt.Shared.Setup.Services;
5 |
6 | public static class EventSourcing
7 | {
8 | public static void AddEventSourcing(this IServiceCollection serviceCollection, IConfiguration configuration)
9 | {
10 | serviceCollection.AddMongoEventSourcing(configuration);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Services/SecretManager.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace Distribt.Shared.Setup.Services;
4 |
5 | public static class SecretManager
6 | {
7 | public static void AddSecretManager(this IServiceCollection serviceCollection, IConfiguration configuration)
8 | {
9 | //TODO: create an awaiter project instead of .result everywhere in the config
10 | string discoveredUrl = GetVaultUrl(serviceCollection.BuildServiceProvider()).Result;
11 | serviceCollection.AddVaultService(configuration, discoveredUrl);
12 | }
13 |
14 | private static async Task GetVaultUrl(IServiceProvider serviceProvider)
15 | {
16 | var serviceDiscovery = serviceProvider.GetService();
17 | return await serviceDiscovery?.GetFullAddress(DiscoveryServices.Secrets)!;
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Services/ServiceBus.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.RabbitMQ;
2 | using Microsoft.Extensions.Configuration;
3 |
4 | namespace Distribt.Shared.Setup.Services;
5 |
6 | public static class ServiceBus
7 | {
8 | public static void AddServiceBusIntegrationPublisher(this IServiceCollection serviceCollection,
9 | IConfiguration configuration)
10 | {
11 | serviceCollection.AddRabbitMQ(GetRabbitMqSecretCredentials, GetRabbitMQHostName,
12 | configuration, "IntegrationPublisher");
13 | serviceCollection.AddRabbitMQPublisher();
14 | }
15 |
16 | ///
17 | /// default option (KeyValue) to get credentials using Vault
18 | ///
19 | ///
20 | ///
21 | private static async Task GetRabbitMqSecretCredentials(IServiceProvider serviceProvider)
22 | {
23 | var secretManager = serviceProvider.GetService();
24 | return await secretManager!.Get("rabbitmq");
25 | }
26 |
27 | ///
28 | /// this option is used to show the usage of different engines on Vault
29 | ///
30 | private static async Task GetRabbitMqSecretCredentialsfromRabbitMQEngine(
31 | IServiceProvider serviceProvider)
32 | {
33 | var secretManager = serviceProvider.GetService();
34 | var credentials = await secretManager!.GetRabbitMQCredentials("distribt-role");
35 | return new RabbitMQCredentials() { password = credentials.Password, username = credentials.Username };
36 | }
37 |
38 | public static void AddServiceBusIntegrationConsumer(this IServiceCollection serviceCollection,
39 | IConfiguration configuration)
40 | {
41 | serviceCollection.AddRabbitMQ(GetRabbitMqSecretCredentials, GetRabbitMQHostName, configuration,
42 | "IntegrationConsumer");
43 | serviceCollection.AddRabbitMqConsumer();
44 | }
45 |
46 | public static void AddServiceBusDomainPublisher(this IServiceCollection serviceCollection,
47 | IConfiguration configuration)
48 | {
49 | serviceCollection.AddRabbitMQ(GetRabbitMqSecretCredentials, GetRabbitMQHostName, configuration,
50 | "DomainPublisher");
51 | serviceCollection.AddRabbitMQPublisher();
52 | }
53 |
54 | public static void AddServiceBusDomainConsumer(this IServiceCollection serviceCollection,
55 | IConfiguration configuration)
56 | {
57 | serviceCollection.AddRabbitMQ(GetRabbitMqSecretCredentials, GetRabbitMQHostName, configuration,
58 | "DomainConsumer");
59 | serviceCollection.AddRabbitMqConsumer();
60 | }
61 |
62 | public static void AddHandlersInAssembly(this IServiceCollection serviceCollection)
63 | {
64 | serviceCollection.Scan(scan => scan.FromAssemblyOf()
65 | .AddClasses(classes => classes.AssignableTo())
66 | .AsImplementedInterfaces()
67 | .WithTransientLifetime());
68 |
69 | ServiceProvider sp = serviceCollection.BuildServiceProvider();
70 | var listHandlers = sp.GetServices();
71 | serviceCollection.AddConsumerHandlers(listHandlers);
72 | }
73 |
74 | private static async Task GetRabbitMQHostName(IServiceProvider serviceProvider)
75 | {
76 | var serviceDiscovery = serviceProvider.GetService();
77 | return await serviceDiscovery?.GetFullAddress(DiscoveryServices.RabbitMQ)!;
78 | }
79 | }
--------------------------------------------------------------------------------
/src/Shared/Distribt.Shared.Setup/Services/ServiceDiscovery.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace Distribt.Shared.Setup.Services;
4 |
5 | public static class ServiceDiscovery
6 | {
7 | public static void AddServiceDiscovery(this IServiceCollection serviceCollection, IConfiguration configuration)
8 | {
9 | serviceCollection.AddDiscovery(configuration);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication.RabbitMQ/Consumer/RabbitMQMessageConsumer.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer;
2 | using Distribt.Shared.Communication.Consumer.Handler;
3 | using Distribt.Shared.Communication.Messages;
4 | using Microsoft.Extensions.Options;
5 | using RabbitMQ.Client;
6 | using ISerializer = Distribt.Shared.Serialization.ISerializer;
7 |
8 | namespace Distribt.Shared.Communication.RabbitMQ.Consumer;
9 |
10 | public class RabbitMQMessageConsumer : IMessageConsumer
11 | {
12 | private readonly ISerializer _serializer;
13 | private readonly RabbitMQSettings _settings;
14 | private readonly ConnectionFactory _connectionFactory;
15 | private readonly IHandleMessage _handleMessage;
16 |
17 |
18 | public RabbitMQMessageConsumer(ISerializer serializer, IOptions settings, IHandleMessage handleMessage)
19 | {
20 | _settings = settings.Value;
21 | _serializer = serializer;
22 | _handleMessage = handleMessage;
23 | _connectionFactory = new ConnectionFactory()
24 | {
25 | HostName = _settings.Hostname,
26 | Password = _settings.Credentials!.password,
27 | UserName = _settings.Credentials.username
28 | };
29 | }
30 |
31 | public Task StartAsync(CancellationToken cancelToken = default)
32 | {
33 | return Task.Run(async () => await Consume(), cancelToken);
34 | }
35 |
36 | private Task Consume()
37 | {
38 | //I had to remove the usings in the next two statements
39 | //because the basicACk on the handler was giving "already disposed"
40 | IConnection connection = _connectionFactory.CreateConnection(); // #6 using (implement it correctly)
41 | IModel channel = connection.CreateModel(); // #6 using (implement it correctly)
42 | RabbitMQMessageReceiver receiver = new RabbitMQMessageReceiver(channel, _serializer, _handleMessage);
43 | string queue = GetCorrectQueue();
44 |
45 | channel.BasicConsume(queue, false, receiver);
46 |
47 | // #5 this should be here await consumer.HandleMessage();
48 | return Task.CompletedTask;
49 | }
50 |
51 | private string GetCorrectQueue()
52 | {
53 | return (typeof(TMessage) == typeof(IntegrationMessage)
54 | ? _settings.Consumer?.IntegrationQueue
55 | : _settings.Consumer?.DomainQueue)
56 | ?? throw new ArgumentException("please configure the queues on the appsettings");
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication.RabbitMQ/Consumer/RabbitMQMessageReceiver.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer.Handler;
2 | using Distribt.Shared.Communication.Messages;
3 | using Distribt.Shared.Serialization;
4 | using RabbitMQ.Client;
5 |
6 | namespace Distribt.Shared.Communication.RabbitMQ.Consumer;
7 |
8 | public class RabbitMQMessageReceiver : DefaultBasicConsumer
9 | {
10 | private readonly IModel _channel;
11 | private readonly ISerializer _serializer;
12 | private byte[]? MessageBody { get; set; }
13 | private Type? MessageType { get; set; }
14 | private ulong DeliveryTag { get; set; }
15 | private readonly IHandleMessage _handleMessage;
16 |
17 | public RabbitMQMessageReceiver(IModel channel, ISerializer serializer, IHandleMessage handleMessage)
18 | {
19 | _channel = channel;
20 | _serializer = serializer;
21 | _handleMessage = handleMessage;
22 | }
23 |
24 | public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange,
25 | string routingKey, IBasicProperties properties, ReadOnlyMemory body)
26 | {
27 | MessageType = Type.GetType(properties.Type)!;
28 | MessageBody = body.ToArray();
29 | DeliveryTag = deliveryTag; // Used to delete the message from rabbitMQ
30 |
31 | // #5 not ideal solution, but seems that this HandleBasicDeliver needs to be like this as its not async
32 | var t = Task.Run(HandleMessage);
33 | t.Wait();
34 | }
35 |
36 | private async Task HandleMessage()
37 | {
38 | if (MessageBody == null || MessageType == null)
39 | {
40 | throw new ArgumentException("Neither the body or the messageType has been populated");
41 | }
42 |
43 | IMessage message = (_serializer.DeserializeObject(MessageBody, MessageType) as IMessage)
44 | ?? throw new ArgumentException("The message did not deserialized properly");
45 |
46 | await _handleMessage.Handle(message, CancellationToken.None);
47 |
48 | _channel.BasicAck(DeliveryTag, false);
49 | }
50 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication.RabbitMQ/Distribt.Shared.Communication.RabbitMQ.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Distribt.Shared.Communication.RabbitMQ
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication.RabbitMQ/Publisher/RabbitMQMessagePublisher.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Distribt.Shared.Communication.Messages;
3 | using Distribt.Shared.Communication.Publisher;
4 | using Distribt.Shared.Serialization;
5 | using Microsoft.Extensions.Options;
6 | using RabbitMQ.Client;
7 |
8 | namespace Distribt.Shared.Communication.RabbitMQ.Publisher;
9 |
10 | public class RabbitMQMessagePublisher : IExternalMessagePublisher
11 | where TMessage : IMessage
12 | {
13 | private readonly ISerializer _serializer;
14 | private readonly RabbitMQSettings _settings;
15 | private readonly ConnectionFactory _connectionFactory;
16 |
17 | public RabbitMQMessagePublisher(ISerializer serializer, IOptions settings)
18 | {
19 | _settings = settings.Value;
20 | _serializer = serializer;
21 | _connectionFactory = new ConnectionFactory()
22 | {
23 | HostName = _settings.Hostname,
24 | Password = _settings.Credentials!.password,
25 | UserName = _settings.Credentials.username
26 | };
27 | }
28 |
29 | public Task Publish(TMessage message, string? routingKey = null, CancellationToken cancellationToken = default)
30 | {
31 | using IConnection connection = _connectionFactory.CreateConnection();
32 | using IModel model = connection.CreateModel();
33 |
34 | PublishSingle(message, model, routingKey);
35 |
36 | return Task.CompletedTask;
37 | }
38 |
39 | public Task PublishMany(IEnumerable messages, string? routingKey = null, CancellationToken cancellationToken = default)
40 | {
41 | using IConnection connection = _connectionFactory.CreateConnection();
42 | using IModel model = connection.CreateModel();
43 | foreach (TMessage message in messages)
44 | {
45 | PublishSingle(message, model, routingKey);
46 | }
47 |
48 | return Task.CompletedTask;
49 | }
50 |
51 |
52 |
53 | private void PublishSingle(TMessage message, IModel model, string? routingKey)
54 | {
55 | var properties = model.CreateBasicProperties();
56 | properties.Persistent = true;
57 | properties.Type = RemoveVersion(message.GetType());
58 |
59 | model.BasicPublish(exchange: GetCorrectExchange(),
60 | routingKey: routingKey ?? "",
61 | basicProperties: properties,
62 | body: _serializer.SerializeObjectToByteArray(message));
63 | }
64 |
65 | private string GetCorrectExchange()
66 | {
67 | return (typeof(TMessage) == typeof(IntegrationMessage)
68 | ? _settings.Publisher?.IntegrationExchange
69 | : _settings.Publisher?.DomainExchange)
70 | ?? throw new ArgumentException("please configure the Exchanges on the appsettings");
71 | }
72 |
73 | ///
74 | /// there is a limit of 255 characters on the type in RabbitMQ.
75 | /// in top of that the version will cause issues if it gets updated and the payload contains the old and so on.
76 | ///
77 | private string RemoveVersion(Type type)
78 | {
79 | return RemoveVersionFromQualifiedName(type.AssemblyQualifiedName ?? "", 0);
80 | }
81 |
82 | private string RemoveVersionFromQualifiedName(string assemblyQualifiedName, int indexStart)
83 | {
84 | var stringBuilder = new StringBuilder();
85 | var indexOfGenericClose = assemblyQualifiedName.IndexOf("]]", indexStart + 1, StringComparison.Ordinal);
86 | var indexOfVersion = assemblyQualifiedName.IndexOf(", Version", indexStart + 1, StringComparison.Ordinal);
87 |
88 | if (indexOfVersion < 0)
89 | return assemblyQualifiedName;
90 |
91 | stringBuilder.Append(assemblyQualifiedName.Substring(indexStart, indexOfVersion - indexStart));
92 |
93 | if (indexOfGenericClose > 0)
94 | stringBuilder.Append(RemoveVersionFromQualifiedName(assemblyQualifiedName, indexOfGenericClose));
95 |
96 | return stringBuilder.ToString();
97 | }
98 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication.RabbitMQ/RabbitMQDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer;
2 | using Distribt.Shared.Communication.Consumer.Handler;
3 | using Distribt.Shared.Communication.Messages;
4 | using Distribt.Shared.Communication.Publisher;
5 | using Distribt.Shared.Communication.RabbitMQ.Consumer;
6 | using Distribt.Shared.Communication.RabbitMQ.Publisher;
7 | using Microsoft.AspNetCore.JsonPatch.Adapters;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.Extensions.Diagnostics.HealthChecks;
11 | using Microsoft.Extensions.Options;
12 | using RabbitMQ.Client;
13 |
14 | namespace Distribt.Shared.Communication.RabbitMQ;
15 |
16 | public static class RabbitMQDependencyInjection
17 | {
18 | public static void AddRabbitMQ(this IServiceCollection serviceCollection,
19 | Func> rabbitMqCredentialsFactory,
20 | Func> rabbitMqHostName,
21 | IConfiguration configuration, string name)
22 | {
23 | serviceCollection.AddRabbitMQ(configuration);
24 | serviceCollection.PostConfigure(x =>
25 | {
26 | ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
27 | x.SetCredentials(rabbitMqCredentialsFactory.Invoke(serviceProvider).Result);
28 | x.SetHostName(rabbitMqHostName.Invoke(serviceProvider).Result);
29 | });
30 |
31 | serviceCollection.AddHealthChecks()
32 | .AddRabbitMQ(AddRabbitMqHealthCheck, name: name, failureStatus: HealthStatus.Unhealthy);
33 | }
34 |
35 | private static IConnection AddRabbitMqHealthCheck(IServiceProvider serviceProvider)
36 | {
37 | RabbitMQSettings settings = serviceProvider.GetRequiredService>().Value;
38 | ConnectionFactory factory = new ConnectionFactory();
39 | factory.UserName = settings.Credentials?.username;
40 | factory.Password = settings.Credentials?.password;
41 | factory.VirtualHost = "/";
42 | factory.HostName = settings.Hostname;
43 | factory.Port = AmqpTcpEndpoint.UseDefaultPort;
44 | return factory.CreateConnection();
45 | }
46 |
47 | ///
48 | /// this method is used when the credentials are inside the configuration. not recommended.
49 | ///
50 | public static void AddRabbitMQ(this IServiceCollection serviceCollection, IConfiguration configuration)
51 | {
52 | serviceCollection.Configure(configuration.GetSection("Bus:RabbitMQ"));
53 | }
54 |
55 | public static void AddConsumerHandlers(this IServiceCollection serviceCollection,
56 | IEnumerable handlers)
57 | {
58 | serviceCollection.AddSingleton(new MessageHandlerRegistry(handlers));
59 | serviceCollection.AddSingleton();
60 | }
61 |
62 | public static void AddRabbitMqConsumer(this IServiceCollection serviceCollection)
63 | {
64 | serviceCollection.AddConsumer();
65 | serviceCollection.AddSingleton, RabbitMQMessageConsumer>();
66 | }
67 |
68 | public static void AddRabbitMQPublisher(this IServiceCollection serviceCollection)
69 | where TMessage : IMessage
70 | {
71 | serviceCollection.AddPublisher();
72 | serviceCollection.AddSingleton, RabbitMQMessagePublisher>();
73 | }
74 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication.RabbitMQ/RabbitMQSettings.cs:
--------------------------------------------------------------------------------
1 | using RabbitMQ.Client;
2 |
3 | namespace Distribt.Shared.Communication.RabbitMQ;
4 |
5 | public class RabbitMQSettings
6 | {
7 | public string Hostname { get; private set; } = null!;
8 | public RabbitMQCredentials? Credentials { get; private set; }
9 | public PublisherSettings? Publisher { get; init; }
10 | public ConsumerSettings? Consumer { get; init; }
11 |
12 | public void SetCredentials(RabbitMQCredentials credentials)
13 | {
14 | Credentials = credentials;
15 | }
16 |
17 | public void SetHostName(string hostname)
18 | {
19 | Hostname = hostname;
20 | }
21 | }
22 |
23 | public record RabbitMQCredentials
24 | {
25 | public string username { get; init; } = null!;
26 | public string password { get; init; } = null!;
27 | }
28 |
29 | public record PublisherSettings
30 | {
31 | public string? IntegrationExchange { get; init; }
32 | public string? DomainExchange { get; init; }
33 | }
34 |
35 | public record ConsumerSettings
36 | {
37 | public string? IntegrationQueue { get; init; }
38 | public string? DomainQueue { get; init; }
39 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.Communication/Distribt.Shared.Communication/CommunicationDependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Distribt.Shared.Communication.Consumer.Host;
2 | using Distribt.Shared.Communication.Consumer.Manager;
3 | using Distribt.Shared.Communication.Messages;
4 | using Distribt.Shared.Communication.Publisher.Domain;
5 | using Distribt.Shared.Communication.Publisher.Integration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Hosting;
8 |
9 | namespace Distribt.Shared.Communication;
10 |
11 | public static class CommunicationDependencyInjection
12 | {
13 | public static void AddConsumer(this IServiceCollection serviceCollection)
14 | {
15 | serviceCollection.AddSingleton