├── .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, ConsumerManager>(); 16 | serviceCollection.AddSingleton>(); 17 | } 18 | 19 | public static void AddPublisher(this IServiceCollection serviceCollection) 20 | { 21 | if (typeof(TMessage) == typeof(IntegrationMessage)) 22 | { 23 | serviceCollection.AddIntegrationBusPublisher(); 24 | } 25 | else if (typeof(TMessage) == typeof(DomainMessage)) 26 | { 27 | serviceCollection.AddDomainBusPublisher(); 28 | } 29 | } 30 | 31 | private static void AddIntegrationBusPublisher(this IServiceCollection serviceCollection) 32 | { 33 | serviceCollection.AddTransient(); 34 | } 35 | 36 | 37 | private static void AddDomainBusPublisher(this IServiceCollection serviceCollection) 38 | { 39 | serviceCollection.AddTransient(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Handler/HandleMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Distribt.Shared.Communication.Messages; 3 | 4 | namespace Distribt.Shared.Communication.Consumer.Handler; 5 | 6 | public interface IHandleMessage 7 | { 8 | Task Handle(IMessage message, CancellationToken cancellationToken = default); 9 | } 10 | 11 | public class HandleMessage : IHandleMessage 12 | { 13 | private readonly IMessageHandlerRegistry _messageHandlerRegistry; 14 | 15 | public HandleMessage(IMessageHandlerRegistry messageHandlerRegistry) 16 | { 17 | _messageHandlerRegistry = messageHandlerRegistry; 18 | } 19 | 20 | public Task Handle(IMessage message, CancellationToken cancellationToken = default) 21 | { 22 | if (message == null) throw new ArgumentNullException(nameof(message)); 23 | 24 | Type messageType = message.GetType(); 25 | var handlerType = typeof(IMessageHandler<>).MakeGenericType(messageType); 26 | List handlers = _messageHandlerRegistry.GetMessageHandlerForType(handlerType, messageType).ToList(); 27 | 28 | foreach (IMessageHandler handler in handlers) 29 | { 30 | Type messageHandlerType = handler.GetType(); 31 | 32 | MethodInfo? handle = messageHandlerType.GetMethods() 33 | .Where(methodInfo => methodInfo.Name == nameof(IMessageHandler.Handle)) 34 | .FirstOrDefault(info => info.GetParameters() 35 | .Select(parameter => parameter.ParameterType) 36 | .Contains(message.GetType())); 37 | 38 | if (handle != null) 39 | return (Task) handle.Invoke(handler, new object[] {message, cancellationToken})!; 40 | } 41 | return Task.CompletedTask; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Handler/IMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Communication.Messages; 2 | 3 | namespace Distribt.Shared.Communication.Consumer.Handler; 4 | 5 | public interface IMessageHandler 6 | { 7 | } 8 | 9 | public interface IMessageHandler : IMessageHandler 10 | { 11 | Task Handle(TMessage message, CancellationToken cancelToken = default(CancellationToken)); 12 | } 13 | 14 | public interface IIntegrationMessageHandler : IMessageHandler 15 | { 16 | } 17 | 18 | public interface IIntegrationMessageHandler 19 | : IMessageHandler>, IIntegrationMessageHandler 20 | { 21 | } 22 | 23 | public interface IDomainMessageHandler : IMessageHandler 24 | { 25 | } 26 | 27 | public interface IDomainMessageHandler 28 | : IMessageHandler>, IDomainMessageHandler 29 | { 30 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Handler/MessageHandlerRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Distribt.Shared.Communication.Consumer.Handler; 4 | 5 | public interface IMessageHandlerRegistry 6 | { 7 | IEnumerable GetMessageHandlerForType(Type messageHandlerType, Type messageType); 8 | } 9 | 10 | public class MessageHandlerRegistry : IMessageHandlerRegistry 11 | { 12 | private readonly IEnumerable _messageHandlers; 13 | 14 | private readonly ConcurrentDictionary> _cachedHandlers = 15 | new ConcurrentDictionary>(); 16 | 17 | public MessageHandlerRegistry(IEnumerable messageHandlers) 18 | { 19 | _messageHandlers = messageHandlers; 20 | } 21 | 22 | public IEnumerable GetMessageHandlerForType(Type messageHandlerType, Type messageType) 23 | { 24 | var key = $"{messageHandlerType}-{messageType}"; 25 | if (_cachedHandlers.TryGetValue(key, out var existingHandlers)) 26 | { 27 | return existingHandlers; 28 | } 29 | 30 | IList handlers = 31 | GetMessageHandlersInternal(messageHandlerType, messageType); 32 | 33 | _cachedHandlers.AddOrUpdate(key, handlers.Distinct(), (_, __) => handlers); 34 | if (handlers.Count == 0) 35 | { 36 | // #4 add logging and specify no handlers found. 37 | } 38 | 39 | return handlers; 40 | } 41 | 42 | private IList GetMessageHandlersInternal(Type messageHandlerType, Type messageType) 43 | { 44 | return 45 | _messageHandlers.Where( 46 | h => h.GetType() 47 | .GetInterfaces() 48 | .Contains(messageHandlerType)) 49 | .Distinct() 50 | .ToList(); 51 | } 52 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Host/ConsumerController.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Communication.Consumer.Manager; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Distribt.Shared.Communication.Consumer.Host; 6 | 7 | public class ConsumerController : Controller 8 | { 9 | private readonly IConsumerManager _consumerManager; 10 | 11 | public ConsumerController(IConsumerManager consumerManager) 12 | { 13 | _consumerManager = consumerManager; 14 | } 15 | 16 | [HttpPut] 17 | [ProducesResponseType(StatusCodes.Status200OK)] 18 | [Route("start")] 19 | public virtual IActionResult Start() 20 | { 21 | _consumerManager.RestartExecution(); 22 | return Ok(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Host/ConsumerHostedService.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Communication.Consumer.Manager; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace Distribt.Shared.Communication.Consumer.Host; 5 | 6 | public class ConsumerHostedService : IHostedService 7 | { 8 | private readonly IConsumerManager _consumerManager; 9 | private readonly IMessageConsumer _messageConsumer; 10 | private readonly CancellationTokenSource _stoppingCancellationTokenSource = 11 | new CancellationTokenSource(); 12 | private Task? _executingTask; 13 | 14 | public ConsumerHostedService(IConsumerManager consumerManager, IMessageConsumer messageConsumer) 15 | { 16 | _consumerManager = consumerManager; 17 | _messageConsumer = messageConsumer; 18 | } 19 | 20 | public Task StartAsync(CancellationToken cancellationToken) 21 | { 22 | _executingTask = ConsumeMessages(_stoppingCancellationTokenSource.Token); 23 | 24 | return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask; 25 | } 26 | 27 | public Task StopAsync(CancellationToken cancellationToken) 28 | { 29 | _stoppingCancellationTokenSource.Cancel(); 30 | _consumerManager.StopExecution(); 31 | return Task.CompletedTask; 32 | } 33 | 34 | private async Task ConsumeMessages(CancellationToken cancellationToken) 35 | { 36 | while (!cancellationToken.IsCancellationRequested) 37 | { 38 | var ct = _consumerManager.GetCancellationToken(); 39 | if (ct.IsCancellationRequested) break; 40 | try 41 | { 42 | await _messageConsumer.StartAsync(cancellationToken); 43 | }catch (OperationCanceledException) 44 | { 45 | // ignore, the operation is getting cancelled 46 | } 47 | //#3 investigate if an exception on the process breaks the consumer. 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/IMessageConsumer.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Consumer; 2 | 3 | public interface IMessageConsumer 4 | { 5 | Task StartAsync(CancellationToken cancelToken = default); 6 | } 7 | 8 | public interface IMessageConsumer : IMessageConsumer 9 | { 10 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Manager/ConsumerManager.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Consumer.Manager; 2 | 3 | public class ConsumerManager : IConsumerManager 4 | { 5 | private CancellationTokenSource _cancellationTokenSource; 6 | 7 | public ConsumerManager() 8 | { 9 | _cancellationTokenSource = new CancellationTokenSource(); 10 | } 11 | 12 | public void RestartExecution() 13 | { 14 | var cancellationTokenSource = _cancellationTokenSource; 15 | _cancellationTokenSource = new CancellationTokenSource(); 16 | cancellationTokenSource.Cancel(); 17 | } 18 | 19 | public void StopExecution() 20 | { 21 | _cancellationTokenSource.Cancel(); 22 | } 23 | 24 | public CancellationToken GetCancellationToken() 25 | { 26 | return _cancellationTokenSource.Token; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Consumer/Manager/IConsumerManager.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Consumer.Manager; 2 | 3 | public interface IConsumerManager 4 | { 5 | void RestartExecution(); 6 | void StopExecution(); 7 | CancellationToken GetCancellationToken(); 8 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Distribt.Shared.Communication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Distribt.Shared.Communication 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Messages/DomainMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Messages; 2 | 3 | public record DomainMessage : IMessage 4 | { 5 | public string MessageIdentifier { get; } 6 | public string Name { get; } 7 | 8 | public DomainMessage(string messageIdentifier, string name) 9 | { 10 | MessageIdentifier = messageIdentifier; 11 | Name = name; 12 | } 13 | } 14 | 15 | public record DomainMessage : DomainMessage 16 | { 17 | public T Content { get; } 18 | public Metadata Metadata { get; } 19 | 20 | public DomainMessage(string messageIdentifier, string name, T content, Metadata metadata) 21 | : base(messageIdentifier, name) 22 | { 23 | Content = content; 24 | Metadata = metadata; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Messages/IMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Messages; 2 | 3 | public interface IMessage 4 | { 5 | /// 6 | /// Must be unique; 7 | /// 8 | public string MessageIdentifier { get; } 9 | /// 10 | /// Name for the message, useful in logs/databases, etc 11 | /// 12 | public string Name { get; } 13 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Messages/IntegrationMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Messages; 2 | 3 | public record IntegrationMessage : IMessage 4 | { 5 | public string MessageIdentifier { get; } 6 | public string Name { get; } 7 | public IntegrationMessage(string messageIdentifier, string name) 8 | { 9 | MessageIdentifier = messageIdentifier; 10 | Name = name; 11 | } 12 | } 13 | 14 | public record IntegrationMessage : IntegrationMessage 15 | { 16 | public T Content { get; } 17 | public Metadata Metadata { get; } 18 | 19 | public IntegrationMessage(string messageIdentifier, string name, T content, Metadata metadata) 20 | : base(messageIdentifier, name) 21 | { 22 | Content = content; 23 | Metadata = metadata; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Messages/Metadata.cs: -------------------------------------------------------------------------------- 1 | namespace Distribt.Shared.Communication.Messages; 2 | 3 | public record Metadata 4 | { 5 | public string CorrelationId { get; } 6 | public DateTime CreatedUtc { get; } 7 | 8 | public Metadata(string correlationId, DateTime createdUtc) 9 | { 10 | CorrelationId = correlationId; 11 | CreatedUtc = createdUtc; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Publisher/Domain/DefaultDomainMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Communication.Messages; 2 | 3 | namespace Distribt.Shared.Communication.Publisher.Domain; 4 | 5 | public interface IDomainMessagePublisher 6 | { 7 | Task Publish(object message, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default); 8 | Task PublishMany(IEnumerable messages, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default); 9 | } 10 | 11 | public class DefaultDomainMessagePublisher : IDomainMessagePublisher 12 | { 13 | 14 | private readonly IExternalMessagePublisher _externalPublisher; 15 | 16 | public DefaultDomainMessagePublisher(IExternalMessagePublisher externalPublisher) 17 | { 18 | _externalPublisher = externalPublisher; 19 | } 20 | 21 | public Task Publish(object message, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default) 22 | { 23 | Metadata calculatedMetadata = CalculateMetadata(metadata); 24 | var domainMessage = DomainMessageMapper.MapToMessage(message, calculatedMetadata); 25 | return _externalPublisher.Publish(domainMessage, routingKey, cancellationToken); 26 | } 27 | 28 | public Task PublishMany(IEnumerable messages, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default) 29 | { 30 | var domainMessages = 31 | messages.Select(a => DomainMessageMapper.MapToMessage(a, CalculateMetadata(metadata))); 32 | return _externalPublisher.PublishMany(domainMessages, routingKey, cancellationToken); 33 | } 34 | 35 | private Metadata CalculateMetadata(Metadata? metadata) 36 | { 37 | return metadata ?? new Metadata(Guid.NewGuid().ToString(), DateTime.UtcNow); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Publisher/Domain/DomainMessageMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Distribt.Shared.Communication.Messages; 3 | 4 | namespace Distribt.Shared.Communication.Publisher.Domain; 5 | 6 | public class DomainMessageMapper 7 | { 8 | public static DomainMessage MapToMessage(object message, Metadata metadata) 9 | { 10 | if (message is IntegrationMessage) 11 | throw new ArgumentException("Message should not be of type DomainMessage, it should be a plain type"); 12 | 13 | var buildWrapperMethodInfo = typeof(DomainMessageMapper).GetMethod( 14 | nameof(ToTypedIntegrationEvent), 15 | BindingFlags.Static | BindingFlags.NonPublic 16 | ); 17 | 18 | var buildWrapperGenericMethodInfo = buildWrapperMethodInfo?.MakeGenericMethod(new[] {message.GetType()}); 19 | var wrapper = buildWrapperGenericMethodInfo?.Invoke( 20 | null, 21 | new[] 22 | { 23 | message, 24 | metadata 25 | } 26 | ); 27 | return (wrapper as DomainMessage)!; 28 | } 29 | 30 | 31 | private static DomainMessage ToTypedIntegrationEvent(T message, Metadata metadata) 32 | { 33 | return new DomainMessage(Guid.NewGuid().ToString(), typeof(T).Name, message, metadata); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Publisher/IExternalMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Communication.Messages; 2 | 3 | namespace Distribt.Shared.Communication.Publisher; 4 | 5 | public interface IExternalMessagePublisher 6 | where TMessage : IMessage 7 | { 8 | Task Publish(TMessage message, string? routingKey = null, CancellationToken cancellationToken = default); 9 | Task PublishMany(IEnumerable messages, string? routingKey = null, CancellationToken cancellationToken = default); 10 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Publisher/Integration/DefaultIntegrationMessagePublisher.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Communication.Messages; 2 | 3 | namespace Distribt.Shared.Communication.Publisher.Integration; 4 | 5 | public interface IIntegrationMessagePublisher 6 | { 7 | Task Publish(object message, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default); 8 | Task PublishMany(IEnumerable messages, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default); 9 | } 10 | 11 | public class DefaultIntegrationMessagePublisher : IIntegrationMessagePublisher 12 | { 13 | private readonly IExternalMessagePublisher _externalPublisher; 14 | 15 | public DefaultIntegrationMessagePublisher(IExternalMessagePublisher externalPublisher) 16 | { 17 | _externalPublisher = externalPublisher; 18 | } 19 | 20 | public Task Publish(object message, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default) 21 | { 22 | Metadata calculatedMetadata = CalculateMetadata(metadata); 23 | var integrationMessage = IntegrationMessageMapper.MapToMessage(message, calculatedMetadata); 24 | return _externalPublisher.Publish(integrationMessage, routingKey, cancellationToken); 25 | } 26 | 27 | public Task PublishMany(IEnumerable messages, Metadata? metadata = null, string? routingKey = null, CancellationToken cancellationToken = default) 28 | { 29 | var integrationMessages = 30 | messages.Select(a => IntegrationMessageMapper.MapToMessage(a, CalculateMetadata(metadata))); 31 | return _externalPublisher.PublishMany(integrationMessages, routingKey, cancellationToken); 32 | } 33 | 34 | private Metadata CalculateMetadata(Metadata? metadata) 35 | { 36 | return metadata ?? new Metadata(Guid.NewGuid().ToString(), DateTime.UtcNow); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Communication/Distribt.Shared.Communication/Publisher/Integration/IntegrationMessageMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Distribt.Shared.Communication.Messages; 3 | 4 | namespace Distribt.Shared.Communication.Publisher.Integration; 5 | 6 | public static class IntegrationMessageMapper 7 | { 8 | public static IntegrationMessage MapToMessage(object message, Metadata metadata) 9 | { 10 | if (message is IntegrationMessage) 11 | throw new ArgumentException("Message should not be of type IntegrationMessage, it should be a plain type"); 12 | 13 | var buildWrapperMethodInfo = typeof(IntegrationMessageMapper).GetMethod( 14 | nameof(ToTypedIntegrationEvent), 15 | BindingFlags.Static | BindingFlags.NonPublic 16 | ); 17 | 18 | var buildWrapperGenericMethodInfo = buildWrapperMethodInfo?.MakeGenericMethod(new[] {message.GetType()}); 19 | var wrapper = buildWrapperGenericMethodInfo?.Invoke( 20 | null, 21 | new[] 22 | { 23 | message, 24 | metadata 25 | } 26 | ); 27 | return (wrapper as IntegrationMessage)!; 28 | } 29 | 30 | 31 | private static IntegrationMessage ToTypedIntegrationEvent(T message, Metadata metadata) 32 | { 33 | return new IntegrationMessage(Guid.NewGuid().ToString(), typeof(T).Name, message, metadata); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Databases/Distribt.Shared.Databases.MongoDb/Distribt.Shared.Databases.MongoDb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Distribt.Shared.Databases.MongoDb 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Shared/Shared.Databases/Distribt.Shared.Databases.MongoDb/MongoDbConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | using Distribt.Shared.Discovery; 2 | using Distribt.Shared.Secrets; 3 | using MongoDB.Driver; 4 | 5 | namespace Distribt.Shared.Databases.MongoDb; 6 | 7 | public interface IMongoDbConnectionProvider 8 | { 9 | MongoUrl GetMongoUrl(); 10 | string GetMongoConnectionString(); 11 | } 12 | 13 | public class MongoDbConnectionProvider : IMongoDbConnectionProvider 14 | { 15 | private readonly ISecretManager _secretManager; 16 | private readonly IServiceDiscovery _serviceDiscovery; 17 | 18 | private MongoUrl? MongoUrl { get; set; } 19 | private string? MongoConnectionString { get; set; } 20 | 21 | public MongoDbConnectionProvider(ISecretManager secretManager, IServiceDiscovery serviceDiscovery) 22 | { 23 | _secretManager = secretManager; 24 | _serviceDiscovery = serviceDiscovery; 25 | } 26 | 27 | 28 | public MongoUrl GetMongoUrl() 29 | { 30 | if (MongoUrl is not null) 31 | return MongoUrl; 32 | 33 | MongoConnectionString = RetrieveMongoUrl().Result; 34 | MongoUrl = new MongoUrl(MongoConnectionString); 35 | 36 | return MongoUrl; 37 | } 38 | 39 | public string GetMongoConnectionString() 40 | { 41 | if (MongoConnectionString is null) 42 | GetMongoUrl(); 43 | 44 | return MongoConnectionString ?? throw new Exception("Mongo connection string cannot be retrieved"); 45 | } 46 | 47 | private async Task RetrieveMongoUrl() 48 | { 49 | DiscoveryData mongoData = await _serviceDiscovery.GetDiscoveryData(DiscoveryServices.MongoDb); 50 | MongoDbCredentials credentials = await _secretManager.Get("mongodb"); 51 | 52 | return $"mongodb://{credentials.username}:{credentials.password}@{mongoData.Server}:{mongoData.Port}"; 53 | } 54 | 55 | 56 | private record MongoDbCredentials 57 | { 58 | public string username { get; init; } = null!; 59 | public string password { get; init; } = null!; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Shared/Shared.Databases/Distribt.Shared.Databases.MongoDb/MongoDbDependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | using MongoDB.Bson.Serialization.Conventions; 5 | 6 | namespace Distribt.Shared.Databases.MongoDb; 7 | 8 | public static class MongoDbDependencyInjection 9 | { 10 | public static IServiceCollection AddMongoDbConnectionProvider(this IServiceCollection serviceCollection) 11 | { 12 | var conventionPack = new ConventionPack { new IgnoreExtraElementsConvention(true) }; 13 | ConventionRegistry.Register("IgnoreExtraElements", conventionPack, type => true); 14 | 15 | return serviceCollection.AddScoped(); 16 | } 17 | 18 | public static IServiceCollection AddMongoDbDatabaseConfiguration(this IServiceCollection serviceCollection, IConfiguration configuration) 19 | { 20 | serviceCollection.Configure(configuration.GetSection("Database:MongoDb")); 21 | return serviceCollection; 22 | } 23 | 24 | 25 | public static IServiceCollection AddMongoHealthCheck(this IServiceCollection serviceCollection, string name) 26 | { 27 | ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); 28 | string mongoConnectionString = serviceProvider.GetRequiredService().GetMongoConnectionString(); 29 | 30 | serviceCollection.AddHealthChecks().AddMongoDb(mongoConnectionString, name, HealthStatus.Unhealthy); 31 | 32 | return serviceCollection; 33 | } 34 | 35 | } 36 | 37 | //nullables are a pain in the ass for the configuration files 38 | public class DatabaseConfiguration 39 | { 40 | public string DatabaseName { get; set; } = default!; 41 | } 42 | 43 | public class MongoEventStoreConfiguration 44 | { 45 | public string DatabaseName { get; set; } = default!; 46 | public string CollectionName { get; set; } = default!; 47 | } 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/Shared/Shared.Databases/Distribt.Shared.Databases.MySql/Distribt.Shared.Databases.MySql.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Distribt.Shared.Databases.MySql 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Shared/Shared.Databases/Distribt.Shared.Databases.MySql/MySqlDependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Distribt.Shared.Databases.MySql; 5 | 6 | public static class MySqlDependencyInjection 7 | { 8 | public static IServiceCollection AddMySqlDbContext(this IServiceCollection serviceCollection, 9 | Func> connectionString) 10 | where T : DbContext 11 | { 12 | return serviceCollection.AddDbContext((serviceProvider, builder) => 13 | { 14 | builder.UseMySQL(connectionString.Invoke(serviceProvider).Result); 15 | }); 16 | } 17 | 18 | public static IServiceCollection AddMysqlHealthCheck(this IServiceCollection serviceCollection, 19 | Func> connectionString) 20 | { 21 | ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); 22 | string mySqlConnectionString = connectionString.Invoke(serviceProvider).Result; 23 | serviceCollection.AddHealthChecks().AddMySql(mySqlConnectionString); 24 | return serviceCollection; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Tests/Services/Orders/Distribt.Tests.Services.Orders.BusinessLogicTests/Distribt.Tests.Services.Orders.BusinessLogicTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | Distribt.Tests.Services.Orders.BusinessLogic 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Tests/Services/Subscriptions/Distribt.Tests.Services.Subscriptions.ApiTests/Distribt.Tests.Services.Subscriptions.ApiTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Tests/Services/Subscriptions/Distribt.Tests.Services.Subscriptions.ApiTests/SubscriptionControllerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Json; 6 | using System.Threading; 7 | using Microsoft.AspNetCore.Mvc.Testing; 8 | using Microsoft.Extensions.Hosting; 9 | using System.Threading.Tasks; 10 | using Distribt.Services.Subscriptions.Dtos; 11 | using Distribt.Shared.Communication.Messages; 12 | using Distribt.Shared.Communication.Publisher.Integration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Xunit; 15 | 16 | 17 | namespace Distribt.Tests.Services.Subscriptions.ApiTests; 18 | 19 | public class SubscriptionControllerTest 20 | { 21 | [Fact] 22 | public async Task WhenSubscriptionApi_Then_EventPublished() 23 | { 24 | SubscriptionApi subscriptionApi = new SubscriptionApi(); 25 | HttpClient client = subscriptionApi.CreateClient(); 26 | 27 | SubscriptionDto subscriptionDto = new("Email"); 28 | JsonContent dtoAsJson = JsonContent.Create(subscriptionDto); 29 | var response = await client.PostAsync("/subscription", dtoAsJson); 30 | response.EnsureSuccessStatusCode(); 31 | 32 | Assert.Single(subscriptionApi.FakeIntegrationPublisher.Objects); 33 | SubscriptionDto dtoSent = subscriptionApi.FakeIntegrationPublisher.Objects.First() as SubscriptionDto ?? throw new InvalidOperationException(); 34 | 35 | Assert.Equal(subscriptionDto.Email, dtoSent.Email); 36 | } 37 | 38 | class SubscriptionApi : WebApplicationFactory 39 | { 40 | public FakeIntegrationPublisher FakeIntegrationPublisher; 41 | 42 | public SubscriptionApi() 43 | { 44 | FakeIntegrationPublisher = new FakeIntegrationPublisher(); 45 | } 46 | 47 | protected override IHost CreateHost(IHostBuilder builder) 48 | { 49 | builder.ConfigureServices(services => 50 | { 51 | services.AddSingleton(FakeIntegrationPublisher); 52 | }); 53 | return base.CreateHost(builder); 54 | } 55 | } 56 | 57 | 58 | public class FakeIntegrationPublisher :IIntegrationMessagePublisher 59 | { 60 | public List Objects = new List(); 61 | public Task Publish(object message, Metadata? metadata = null, string? routingKey = null, 62 | CancellationToken cancellationToken = default) 63 | { 64 | Objects.Add(message); 65 | return Task.CompletedTask; 66 | } 67 | 68 | public Task PublishMany(IEnumerable messages, Metadata? metadata = null, string? routingKey = null, 69 | CancellationToken cancellationToken = default) 70 | { 71 | throw new System.NotImplementedException(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Tests/Shared/Discovery/Distribt.Test.Shared.Discovery.Tests/Distribt.Test.Shared.Discovery.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tools/consul/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Infrastructure 3 | docker exec -it consul consul services register -name=RabbitMQ -address=localhost 4 | docker exec -it consul consul services register -name=SecretManager -address=http://localhost -port=8200 5 | docker exec -it consul consul services register -name=MySql -address=localhost -port=3307 6 | docker exec -it consul consul services register -name=MongoDb -address=localhost -port=27017 7 | docker exec -it consul consul services register -name=Graylog --address=localhost -port=12201 8 | docker exec -it consul consul services register -name=OpenTelemetryCollector --address=localhost -port=4317 9 | 10 | # Services 11 | docker exec -it consul consul services register -name=EmailsApi -address=http://localhost -port=60120 12 | docker exec -it consul consul services register -name=ProductsApiWrite -address=https://localhost -port=60320 13 | docker exec -it consul consul services register -name=ProductsApiRead -address=https://localhost -port=60321 14 | docker exec -it consul consul services register -name=OrdersApi -address=http://localhost -port=60220 15 | docker exec -it consul consul services register -name=SubscriptionsApi -address=http://localhost -port=60420 16 | -------------------------------------------------------------------------------- /tools/local-development/up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose up -d 4 | 5 | sleep 15 # sleep 10 seconds to give time to docker to finish the setup 6 | echo setup vault configuration 7 | ./tools/vault/config.sh 8 | echo setup consul configuration 9 | ./tools/consul/config.sh 10 | echo completed 11 | -------------------------------------------------------------------------------- /tools/mongodb/mongo-init.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: "distribtUser", 3 | pwd: "distribtPassword", 4 | roles: [{ 5 | role: "readWrite", 6 | db: "distribt" 7 | } 8 | ], 9 | mechanisms: ["SCRAM-SHA-1"] 10 | }); 11 | 12 | db.createCollection("Products"); 13 | db.Products.insertOne({"Id": 1, "Details": {"Name": "Producto 1", "Description": "La descripción dice qu es el primer producto"}, "Stock": 32, "Price": 10 }); 14 | db.Products.insertOne({"Id": 2, "Details": {"Name": "Segundo producto", "Description": "Este es el producto numero 2"}, "Stock": 3, "Price": 120 }); 15 | db.Products.insertOne({"Id": 3, "Details": {"Name": "Tercer", "Description": "Terceras Partes nunca fueron buenas"}, "Stock": 10, "Price": 15 }); 16 | 17 | //This is called like this because it is the same database. 18 | //on nomral circumstances (outside of local development) this will be in a separated database and just called "Events" 19 | db.createCollection("EventsOrders"); 20 | //Same, on normal cirmumstances this will be "Products". 21 | db.createCollection("ProductName"); 22 | -------------------------------------------------------------------------------- /tools/mysql/init.sql: -------------------------------------------------------------------------------- 1 | USE `distribt`; 2 | 3 | CREATE TABLE `Products` ( 4 | `Id` int NOT NULL AUTO_INCREMENT, 5 | `Name` VARCHAR(150) NOT NULL, 6 | `Description` VARCHAR(150) NOT NULL, 7 | PRIMARY KEY (`Id`) 8 | ) AUTO_INCREMENT = 1; 9 | 10 | INSERT INTO `distribt`.`Products` (`Id`, `Name`, `Description`) VALUES ('1', 'Producto 1', 'La descripción dice qu es el primer producto'); 11 | INSERT INTO `distribt`.`Products` (`Id`, `Name`, `Description`) VALUES ('2', 'Segundo producto', 'Este es el producto numero 2'); 12 | INSERT INTO `distribt`.`Products` (`Id`, `Name`, `Description`) VALUES ('3', 'Tercer', 'Terceras Partes nunca fueron buenas'); 13 | -------------------------------------------------------------------------------- /tools/rabbitmq/definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [], 3 | "vhosts": [ 4 | { 5 | "name": "/" 6 | } 7 | ], 8 | "permissions": [], 9 | "parameters": [], 10 | "policies": [ 11 | {"vhost":"/","name":"DLX","pattern":".*","apply-to":"queues","definition":{"dead-letter-exchange":"dead-letter.exchange"},"priority":0} 12 | ], 13 | "queues": [ 14 | {"name":"subscription-queue","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 15 | {"name":"subscription-queue.dead-letter","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 16 | {"name":"order-domain-queue","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 17 | {"name":"order-domain-queue.dead-letter","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 18 | {"name":"order-queue","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 19 | {"name":"order-queue.dead-letter","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 20 | {"name":"product-queue","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 21 | {"name":"product-queue.dead-letter","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 22 | {"name":"product-domain-queue","vhost":"/","durable":true,"auto_delete":false,"arguments":{}}, 23 | {"name":"product-domain-queue.dead-letter","vhost":"/","durable":true,"auto_delete":false,"arguments":{}} 24 | ], 25 | "exchanges": [ 26 | {"name":"api.public.exchange","vhost":"/","type":"direct","durable":true,"auto_delete":false,"internal":false,"arguments":{}}, 27 | {"name":"subscription.exchange","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":false,"arguments":{}}, 28 | {"name":"dead-letter.exchange","vhost":"/","type":"direct","durable":true,"auto_delete":false,"internal":false,"arguments":{}}, 29 | {"name":"order.exchange","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":false,"arguments":{}}, 30 | {"name":"products.exchange","vhost":"/","type":"topic","durable":true,"auto_delete":false,"internal":false,"arguments":{}} 31 | ], 32 | "bindings": [ 33 | {"source":"api.public.exchange","vhost":"/","destination":"subscription.exchange","destination_type":"exchange","routing_key":"subscription","arguments":{}}, 34 | {"source":"subscription.exchange","vhost":"/","destination":"subscription-queue","destination_type":"queue","routing_key":"subscription","arguments":{}}, 35 | {"source":"dead-letter.exchange","vhost":"/","destination":"subscription-queue.dead-letter","destination_type":"queue","routing_key":"subscription","arguments":{}}, 36 | {"source":"order.exchange","vhost":"/","destination":"order-domain-queue","destination_type":"queue","routing_key":"order","arguments":{}}, 37 | {"source":"order.exchange","vhost":"/","destination":"order-queue","destination_type":"queue","routing_key":"external","arguments":{}}, 38 | {"source":"products.exchange","vhost":"/","destination":"product-queue","destination_type":"queue","routing_key":"external","arguments":{}}, 39 | {"source":"products.exchange","vhost":"/","destination":"product-domain-queue","destination_type":"queue","routing_key":"internal","arguments":{}}, 40 | {"source":"products.exchange","vhost":"/","destination":"order.exchange","destination_type":"exchange","routing_key":"external","arguments":{}} 41 | ] 42 | } -------------------------------------------------------------------------------- /tools/rabbitmq/enabled_plugins: -------------------------------------------------------------------------------- 1 | [rabbitmq_prometheus, rabbitmq_amqp1_0, rabbitmq_management, rabbitmq_web_dispatch, rabbitmq_management_agent, rabbitmq_stomp]. -------------------------------------------------------------------------------- /tools/rabbitmq/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | # example file from https://github.com/rabbitmq/rabbitmq-server/blob/master/deps/rabbit/docs/rabbitmq.conf.example 2 | 3 | loopback_users.DistribtAdmin = false 4 | 5 | default_vhost = / 6 | default_user = DistribtAdmin 7 | default_pass = DistribtPass 8 | 9 | management.load_definitions = /etc/rabbitmq/definitions.json -------------------------------------------------------------------------------- /tools/telemetry/grafana_datasources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: 'prometheus' 5 | type: 'prometheus' 6 | access: 'proxy' 7 | url: 'http://prometheus:9090' -------------------------------------------------------------------------------- /tools/telemetry/otel-collector-config.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver 2 | receivers: 3 | otlp: 4 | protocols: 5 | grpc: 6 | 7 | # Configure exporters 8 | exporters: 9 | # Export prometheus endpoint 10 | prometheus: 11 | endpoint: "0.0.0.0:8889" 12 | 13 | # log to the console 14 | logging: 15 | 16 | # Export to zipkin 17 | zipkin: 18 | endpoint: "http://zipkin:9411/api/v2/spans" 19 | format: proto 20 | 21 | # Export to a file 22 | file: 23 | path: /etc/output/logs.json 24 | 25 | # https://opentelemetry.io/docs/collector/configuration/#processors 26 | processors: 27 | batch: 28 | 29 | # https://opentelemetry.io/docs/collector/configuration/#service 30 | # https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#pipelines 31 | service: 32 | pipelines: 33 | traces: 34 | receivers: [otlp] 35 | processors: [batch] 36 | exporters: [logging, zipkin] 37 | metrics: 38 | receivers: [otlp] 39 | processors: [batch] 40 | exporters: [logging, prometheus] 41 | logs: 42 | receivers: [otlp] 43 | processors: [] 44 | exporters: [logging, file] -------------------------------------------------------------------------------- /tools/telemetry/prometheus.yaml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: 'collect-metrics' 3 | scrape_interval: 10s 4 | static_configs: 5 | - targets: ['opentelemetry-collector:8889'] 6 | - targets: ['opentelemetry-collector:8888'] 7 | - targets: [ 'rabbitmq:15692' ] -------------------------------------------------------------------------------- /tools/vault/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## this is the code for the first part of the tutorial. 4 | ## example code for get KeyValue secrets. 5 | docker exec -it vault vault kv put secret/rabbitmq username=DistribtAdmin password=DistribtPass 6 | 7 | 8 | ## this is the code for the rabbitmq integration with vault 9 | docker exec -it vault vault secrets enable rabbitmq 10 | 11 | docker exec -it vault vault write rabbitmq/config/connection \ 12 | connection_uri="http://rabbitmq:15672" \ 13 | username="DistribtAdmin" \ 14 | password="DistribtPass" 15 | 16 | docker exec -it vault vault write rabbitmq/roles/distribt-role \ 17 | vhosts='{"/":{"write": ".*", "read": ".*"}}' 18 | 19 | 20 | 21 | ## User&Pass for mongoDb 22 | docker exec -it vault vault kv put secret/mongodb username=distribtUser password=distribtPassword 23 | 24 | ##User&Pass for MySql 25 | docker exec -it vault vault kv put secret/mysql username=distribtUser password=distribtPassword 26 | --------------------------------------------------------------------------------