feat: add .net aspire integrations

This commit is contained in:
Meysam Hadeli 2025-07-22 01:32:05 +03:30
parent 11e3bb3904
commit ab512476d0
55 changed files with 860 additions and 874 deletions

View File

@ -69,6 +69,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Ser
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Services\Passenger\tests\IntegrationTest\Integration.Test.csproj", "{A85AE27D-81ED-485A-BA4B-161B25BEB8A5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C4287034-6833-4505-A6EB-704A86392ECB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "src\Aspire\src\AppHost\AppHost.csproj", "{490BCB11-314C-473C-9B85-A32164783507}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -271,6 +277,18 @@ Global
{A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x64.Build.0 = Release|Any CPU
{A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x86.ActiveCfg = Release|Any CPU
{A85AE27D-81ED-485A-BA4B-161B25BEB8A5}.Release|x86.Build.0 = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Debug|Any CPU.Build.0 = Debug|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Debug|x64.ActiveCfg = Debug|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Debug|x64.Build.0 = Debug|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Debug|x86.ActiveCfg = Debug|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Debug|x86.Build.0 = Debug|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|Any CPU.ActiveCfg = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|Any CPU.Build.0 = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|x64.ActiveCfg = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|x64.Build.0 = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|x86.ActiveCfg = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -308,5 +326,8 @@ Global
{B52D6341-AAD9-43CB-82AF-2DBE39CBF1DB} = {51D8F471-B8EB-AD1C-0E89-AA84C5D0C759}
{0DAACE48-4EA6-4DB7-8A5C-99B86BCB1E01} = {4B043475-1AFA-C467-FE09-A46D09CD6936}
{A85AE27D-81ED-485A-BA4B-161B25BEB8A5} = {54BCCDE8-25E6-6FCB-4A9E-D5D2AF76D352}
{D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938}
{C4287034-6833-4505-A6EB-704A86392ECB} = {D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA}
{490BCB11-314C-473C-9B85-A32164783507} = {C4287034-6833-4505-A6EB-704A86392ECB}
EndGlobalSection
EndGlobal

View File

@ -358,4 +358,5 @@ networks:
driver: bridge
volumes:
postgres-data:
elastic-data:
postgres-data:

View File

@ -347,16 +347,16 @@ services:
######################################################
# Gateway
# Api-Gateway
######################################################
gateway:
image: gateway
api-gateway:
image: api-gateway
build:
args:
Version: "1"
context: ../../
dockerfile: src/ApiGateway/dev.Dockerfile
container_name: gateway
container_name: api-gateway
ports:
- "5001:80"
- "5000:443"

View File

@ -1,7 +1,5 @@
using BuildingBlocks.Logging;
using BuildingBlocks.Web;
using Figgle;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
var env = builder.Environment;
@ -10,7 +8,6 @@ Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.AddCustomSerilog(env);
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
@ -18,7 +15,6 @@ builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSecti
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseCorrelationId();
app.UseRouting();
app.UseHttpsRedirection();

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.SDK" Version="9.3.1"/>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>bde28db3-85ba-4201-b889-0f3faba24169</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.3.1" />
<PackageReference Include="Aspire.Hosting.MongoDB" Version="9.3.1" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.3.1" />
<PackageReference Include="Aspire.Hosting.RabbitMQ" Version="9.3.1" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.3.1" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.EventStore" Version="9.6.0" />
<PackageReference Include="Elastic.Aspire.Hosting.Elasticsearch" Version="9.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\ApiGateway\src\ApiGateway.csproj" />
<ProjectReference Include="..\..\..\Services\Booking\src\Booking.Api\Booking.Api.csproj" />
<ProjectReference Include="..\..\..\Services\Flight\src\Flight.Api\Flight.Api.csproj" />
<ProjectReference Include="..\..\..\Services\Identity\src\Identity.Api\Identity.Api.csproj" />
<ProjectReference Include="..\..\..\Services\Passenger\src\Passenger.Api\Passenger.Api.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,216 @@
using System.Net.Sockets;
var builder = DistributedApplication.CreateBuilder(args);
// 1. Database Services
var username = builder.AddParameter("username", "postgres", secret: true);
var password = builder.AddParameter("password", "postgres", secret: true);
var postgres = builder.AddPostgres("postgres", username, password)
.WithImage("postgres:latest")
.WithEndpoint(port: 5432, targetPort: 5432, name: "postgres")
.WithArgs(
"-c", "wal_level=logical",
"-c", "max_prepared_transactions=10"
)
.WithDataVolume("postgres-data")
.WithLifetime(ContainerLifetime.Persistent);
var flightDb = postgres.AddDatabase("flight");
var passengerDb = postgres.AddDatabase("passenger");
var identityDb = postgres.AddDatabase("identity");
var persistMessageDb = postgres.AddDatabase("persist-message");
var mongoUsername = builder.AddParameter("mongo-username", "root");
var mongoPassword = builder.AddParameter("mongo-password", "secret", secret: true);
var mongo = builder.AddMongoDB("mongo", userName: mongoUsername, password: mongoPassword)
.WithImage("mongo:latest")
.WithEndpoint(port: 27017, targetPort: 27017, name: "mongo")
.WithDataVolume("mongo-data")
.WithLifetime(ContainerLifetime.Persistent);
var redis = builder.AddRedis("redis")
.WithImage("redis:latest")
.WithEndpoint(port: 6379, targetPort: 6379, name: "redis")
.WithDataVolume("redis-data")
.WithLifetime(ContainerLifetime.Persistent);
var eventstore = builder.AddEventStore("eventstore")
.WithImage("eventstore/eventstore")
.WithEnvironment("EVENTSTORE_CLUSTER_SIZE", "1")
.WithEnvironment("EVENTSTORE_RUN_PROJECTIONS", "All")
.WithEnvironment("EVENTSTORE_START_STANDARD_PROJECTIONS", "True")
.WithEnvironment("EVENTSTORE_INSECURE", "True")
.WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "True")
.WithHttpEndpoint(port: 2113, targetPort: 2113, name: "eventstore-http")
.WithDataVolume("eventstore-data")
.WithLifetime(ContainerLifetime.Persistent);
// 2. Messaging Services
var rabbitmq = builder.AddRabbitMQ("rabbitmq")
.WithImage("rabbitmq:management")
.WithEndpoint(port: 5672, targetPort: 5672, name: "rabbitmq-amqp")
.WithEndpoint(port: 15672, targetPort: 15672, name: "rabbitmq-management")
.WithLifetime(ContainerLifetime.Persistent);
// 3. Observability Services
var jaeger = builder.AddContainer("jaeger-all-in-one", "jaegertracing/all-in-one")
.WithEndpoint(port: 6831, targetPort: 6831, name: "jaeger-udp", protocol: ProtocolType.Udp)
.WithEndpoint(port: 16686, targetPort: 16686, name: "jaeger-ui")
.WithEndpoint(port: 14268, targetPort: 14268, name: "jaeger-api")
.WithEndpoint(port: 14317, targetPort: 4317, name: "jaeger-otlp-grpc")
.WithEndpoint(port: 14318, targetPort: 4318, name: "jaeger-otlp-http")
.WithLifetime(ContainerLifetime.Persistent);
var zipkin = builder.AddContainer("zipkin-all-in-one", "openzipkin/zipkin")
.WithEndpoint(port: 9411, targetPort: 9411, name: "zipkin-api")
.WithLifetime(ContainerLifetime.Persistent);
var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector-contrib")
.WithBindMount("../../../../deployments/configs/otel-collector-config.yaml", "/etc/otelcol-contrib/config.yaml", isReadOnly: true)
.WithArgs("--config=/etc/otelcol-contrib/config.yaml")
.WithEndpoint(port: 11888, targetPort: 1888, name: "otel-pprof")
.WithEndpoint(port: 8888, targetPort: 8888, name: "otel-metrics")
.WithEndpoint(port: 8889, targetPort: 8889, name: "otel-exporter-metrics")
.WithEndpoint(port: 13133, targetPort: 13133, name: "otel-health")
.WithEndpoint(port: 4317, targetPort: 4317, name: "otel-grpc")
.WithEndpoint(port: 4318, targetPort: 4318, name: "otel-http")
.WithEndpoint(port: 55679, targetPort: 55679, name: "otel-zpages")
.WithLifetime(ContainerLifetime.Persistent);
var prometheus = builder.AddContainer("prometheus", "prom/prometheus")
.WithBindMount("../../../../deployments/configs/prometheus.yaml", "/etc/prometheus/prometheus.yml")
.WithArgs(
"--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.path=/prometheus",
"--web.console.libraries=/usr/share/prometheus/console_libraries",
"--web.console.templates=/usr/share/prometheus/consoles",
"--web.enable-remote-write-receiver")
.WithEndpoint(port: 9090, targetPort: 9090, name: "prometheus-web")
.WithLifetime(ContainerLifetime.Persistent);
var grafana = builder.AddContainer("grafana", "grafana/grafana")
.WithEnvironment("GF_INSTALL_PLUGINS", "grafana-clock-panel,grafana-simple-json-datasource")
.WithEnvironment("GF_SECURITY_ADMIN_USER", "admin")
.WithEnvironment("GF_SECURITY_ADMIN_PASSWORD", "admin")
.WithEnvironment("GF_FEATURE_TOGGLES_ENABLE", "traceqlEditor")
.WithBindMount("../../../../deployments/configs/grafana/provisioning", "/etc/grafana/provisioning")
.WithBindMount("../../../../deployments/configs/grafana/dashboards", "/var/lib/grafana/dashboards")
.WithEndpoint(port: 3000, targetPort: 3000, name: "grafana-web")
.WithLifetime(ContainerLifetime.Persistent);
var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter")
.WithBindMount("/proc", "/host/proc", isReadOnly: true)
.WithBindMount("/sys", "/host/sys", isReadOnly: true)
.WithBindMount("/", "/rootfs", isReadOnly: true)
.WithArgs(
"--path.procfs=/host/proc",
"--path.rootfs=/rootfs",
"--path.sysfs=/host/sys")
.WithEndpoint(port: 9101, targetPort: 9100, name: "node-exporter")
.WithLifetime(ContainerLifetime.Persistent);
var tempo = builder.AddContainer("tempo", "grafana/tempo")
.WithBindMount("../../../../deployments/configs/tempo.yaml", "/etc/tempo.yaml", isReadOnly: true)
.WithArgs("--config.file=/etc/tempo.yaml")
.WithEndpoint(port: 3200, targetPort: 3200, name: "tempo")
.WithEndpoint(port: 24317, targetPort: 4317, name: "tempo-otlp-grpc")
.WithEndpoint(port: 24318, targetPort: 4318, name: "tempo-otlp-http")
.WithLifetime(ContainerLifetime.Persistent);
var loki = builder.AddContainer("loki", "grafana/loki")
.WithBindMount("../../../../deployments/configs/loki-config.yaml", "/etc/loki/local-config.yaml", isReadOnly: true)
.WithArgs("-config.file=/etc/loki/local-config.yaml")
.WithEndpoint(port: 3100, targetPort: 3100, name: "loki")
.WithLifetime(ContainerLifetime.Persistent);
var elasticsearch = builder.AddElasticsearch("elasticsearch")
.WithImage("docker.elastic.co/elasticsearch/elasticsearch:8.17.0")
.WithEnvironment("discovery.type", "single-node")
.WithEnvironment("cluster.name", "docker-cluster")
.WithEnvironment("node.name", "docker-node")
.WithEnvironment("ES_JAVA_OPTS", "-Xms512m -Xmx512m")
.WithEnvironment("xpack.security.enabled", "false")
.WithEnvironment("xpack.security.http.ssl.enabled", "false")
.WithEnvironment("xpack.security.transport.ssl.enabled", "false")
.WithEnvironment("network.host", "0.0.0.0")
.WithEnvironment("http.port", "9200")
.WithEnvironment("transport.host", "localhost")
.WithEnvironment("bootstrap.memory_lock", "true")
.WithEnvironment("cluster.routing.allocation.disk.threshold_enabled", "false")
.WithEndpoint(port: 9200, targetPort: 9200, name: "elasticsearch-http")
.WithEndpoint(port: 9300, targetPort: 9300, name: "elasticsearch-transport")
.WithDataVolume("elastic-data")
.WithLifetime(ContainerLifetime.Persistent);
var kibana = builder.AddContainer("kibana", "docker.elastic.co/kibana/kibana:8.17.0")
.WithEnvironment("ELASTICSEARCH_HOSTS", "http://elasticsearch:9200")
.WithEndpoint(port: 5601, targetPort: 5601, name: "kibana")
.WithReference(elasticsearch)
.WaitFor(elasticsearch)
.WithLifetime(ContainerLifetime.Persistent);
// 5. Application Services
var identity = builder.AddProject<Projects.Identity_Api>("identity-service")
.WithReference(persistMessageDb)
.WaitFor(persistMessageDb)
.WithReference(identityDb)
.WaitFor(identityDb)
.WithReference(mongo)
.WaitFor(mongo)
.WithReference(rabbitmq)
.WaitFor(rabbitmq)
.WithHttpEndpoint(port: 6005, name: "identity-http")
.WithHttpsEndpoint(port: 5005, name: "identity-https");
var passenger = builder.AddProject<Projects.Passenger_Api>("passenger-service")
.WithReference(persistMessageDb)
.WaitFor(persistMessageDb)
.WithReference(passengerDb)
.WaitFor(passengerDb)
.WithReference(mongo)
.WaitFor(mongo)
.WithReference(rabbitmq)
.WaitFor(rabbitmq)
.WithHttpEndpoint(port: 6012, name: "passenger-http")
.WithHttpsEndpoint(port: 5012, name: "passenger-https");
var flight = builder.AddProject<Projects.Flight_Api>("flight-service")
.WithReference(persistMessageDb)
.WaitFor(persistMessageDb)
.WithReference(flightDb)
.WaitFor(flightDb)
.WithReference(mongo)
.WaitFor(mongo)
.WithReference(rabbitmq)
.WaitFor(rabbitmq)
.WithHttpEndpoint(port: 5004, name: "flight-http")
.WithHttpsEndpoint(port: 5003, name: "flight-https");
var booking = builder.AddProject<Projects.Booking_Api>("booking-service")
.WithReference(persistMessageDb)
.WaitFor(persistMessageDb)
.WithReference(eventstore)
.WaitFor(eventstore)
.WithReference(mongo)
.WaitFor(mongo)
.WithReference(rabbitmq)
.WaitFor(rabbitmq)
.WithHttpEndpoint(port: 6010, name: "booking-http")
.WithHttpsEndpoint(port: 5010, name: "booking-https");
var gateway = builder.AddProject<Projects.ApiGateway>("api-gateway")
.WithReference(flight)
.WaitFor(flight)
.WithReference(passenger)
.WaitFor(passenger)
.WithReference(identity)
.WaitFor(identity)
.WithReference(booking)
.WaitFor(booking)
.WithHttpEndpoint(port: 5001, name: "gateway-http")
.WithHttpsEndpoint(port: 5000, name: "gateway-https");
builder.Build().Run();

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7000;http://localhost:7001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://otel-collector:4317",
"ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://otel-collector:4318"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:7001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://otel-collector:4317",
"ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://otel-collector:4318",
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -6,16 +6,6 @@
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Elasticsearch" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.EventStore" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.UI.SQLite.Storage" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="8.1.0" />
<PackageReference Include="Grpc.Core.Testing" Version="2.46.6" />
<PackageReference Include="EasyCaching.Core" Version="1.9.2" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
@ -27,6 +17,8 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.1" />
<PackageReference Include="Npgsql" Version="9.0.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Polly" Version="8.5.0" />
@ -50,16 +42,6 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.64" />
<PackageReference Include="Scrutor" Version="5.0.2" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
<PackageReference Include="Serilog.Formatting.Elasticsearch" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.SpectreConsole" Version="0.3.3" />
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.5" />
<PackageReference Include="Sieve" Version="2.5.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0" />
@ -79,7 +61,18 @@
<PackageReference Include="Testcontainers.PostgreSql" Version="4.0.0" />
<PackageReference Include="Testcontainers.RabbitMq" Version="4.0.0" />
<PackageReference Include="Unchase.Swashbuckle.AspNetCore.Extensions" Version="2.7.1" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
<PackageReference Include="Xunit.Extensions.Logging" Version="1.1.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.EventStore" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="9.0.0" />
<PackageReference Include="Npgsql.OpenTelemetry" Version="9.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1" />

View File

@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -14,7 +15,7 @@ namespace BuildingBlocks.EFCore;
public static class Extensions
{
public static IServiceCollection AddCustomDbContext<TContext>(this WebApplicationBuilder builder, string connectionName = "")
public static IServiceCollection AddCustomDbContext<TContext>(this WebApplicationBuilder builder, string? connectionName = "")
where TContext : DbContext, IDbContext
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
@ -24,9 +25,8 @@ public static class Extensions
builder.Services.AddDbContext<TContext>(
(sp, options) =>
{
string? connectionString = string.IsNullOrEmpty(connectionName) ?
sp.GetRequiredService<PostgresOptions>().ConnectionString :
builder.Configuration?.GetSection("PostgresOptions:ConnectionString")[connectionName];
var aspireConnectionString = builder.Configuration.GetConnectionString(connectionName.Kebaberize());
var connectionString = aspireConnectionString ?? sp.GetRequiredService<PostgresOptions>().ConnectionString;
ArgumentException.ThrowIfNullOrEmpty(connectionString);

View File

@ -24,15 +24,16 @@ public record EventStoreDBOptions(
public static class EventStoreDBConfigExtensions
{
public static IServiceCollection AddEventStoreDB(this IServiceCollection services, IConfiguration config,
public static IServiceCollection AddEventStoreDB(this IServiceCollection services, IConfiguration configuration,
EventStoreDBOptions? options = null)
{
services
.AddSingleton(x =>
{
var aspireConnectionString = configuration.GetConnectionString("eventstore");
var eventStoreOptions = services.GetOptions<EventStoreOptions>(nameof(EventStoreOptions));
return new EventStoreClient(EventStoreClientSettings.Create(eventStoreOptions.ConnectionString));
return new EventStoreClient(EventStoreClientSettings.Create(aspireConnectionString ?? eventStoreOptions.ConnectionString));
})
.AddScoped(typeof(IEventStoreDBRepository<>), typeof(EventStoreDBRepository<>))
.AddTransient<EventStoreDBSubscriptionToAll, EventStoreDBSubscriptionToAll>();

View File

@ -3,8 +3,6 @@ using BuildingBlocks.Core.Model;
namespace BuildingBlocks.EventStoreDB.Events
{
using Microsoft.FSharp.Control;
public interface IAggregateEventSourcing : IProjection, IEntity
{
IReadOnlyList<IDomainEvent> DomainEvents { get; }

View File

@ -1,49 +1,71 @@
using BuildingBlocks.EFCore;
using BuildingBlocks.Logging;
using BuildingBlocks.EventStoreDB;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
using BuildingBlocks.OpenTelemetryCollector;
using BuildingBlocks.Web;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using MongoDB.Driver;
using RabbitMQ.Client;
namespace BuildingBlocks.HealthCheck;
public static class Extensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services)
{
var healthOptions = services.GetOptions<HealthOptions>(nameof(HealthOptions));
if (!healthOptions.Enabled)
return services;
var appOptions = services.GetOptions<AppOptions>(nameof(AppOptions));
var postgresOptions = services.GetOptions<PostgresOptions>(nameof(PostgresOptions));
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>(nameof(RabbitMqOptions));
var mongoOptions = services.GetOptions<MongoOptions>(nameof(MongoOptions));
var healthChecksBuilder = services.AddHealthChecks()
.AddRabbitMQ(
rabbitConnectionString:
$"amqp://{rabbitMqOptions.UserName}:{rabbitMqOptions.Password}@{rabbitMqOptions.HostName}");
if (mongoOptions.ConnectionString is not null)
healthChecksBuilder.AddMongoDb(mongoOptions.ConnectionString);
if (postgresOptions.ConnectionString is not null)
healthChecksBuilder.AddNpgSql(postgresOptions.ConnectionString);
services.AddHealthChecksUI(setup =>
if (healthOptions.Enabled)
{
setup.SetEvaluationTimeInSeconds(60); // time in seconds between check
setup.AddHealthCheckEndpoint($"Basic Health Check - {appOptions.Name}", "/healthz");
}).AddInMemoryStorage();
var appOptions = services.GetOptions<AppOptions>(nameof(AppOptions));
var postgresOptions = services.GetOptions<PostgresOptions>(nameof(PostgresOptions));
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>(nameof(RabbitMqOptions));
var eventStoreOptions = services.GetOptions<EventStoreOptions>(nameof(EventStoreOptions));
var mongoOptions = services.GetOptions<MongoOptions>(nameof(MongoOptions));
var healthChecksBuilder = services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"])
.AddRabbitMQ(
serviceProvider =>
{
var factory = new ConnectionFactory
{
Uri = new Uri($"amqp://{rabbitMqOptions.UserName}:{rabbitMqOptions.Password}@{rabbitMqOptions.HostName}"),
};
return factory.CreateConnectionAsync();
});
if (!string.IsNullOrEmpty(mongoOptions.ConnectionString))
{
healthChecksBuilder.AddMongoDb(
clientFactory: _ => new MongoClient(mongoOptions.ConnectionString),
name: "MongoDB-Health",
failureStatus: HealthStatus.Unhealthy,
timeout: TimeSpan.FromSeconds(10));
}
if (!string.IsNullOrEmpty(postgresOptions.ConnectionString))
healthChecksBuilder.AddNpgSql(postgresOptions.ConnectionString);
if (!string.IsNullOrEmpty(eventStoreOptions.ConnectionString))
healthChecksBuilder.AddEventStore(eventStoreOptions.ConnectionString);
services.AddHealthChecksUI(setup =>
{
setup.SetEvaluationTimeInSeconds(60); // time in seconds between check
setup.AddHealthCheckEndpoint($"Self Check - {appOptions.Name}", HealthEndpointPath);
}).AddInMemoryStorage();
}
services.AddHealthChecks().AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return services;
}
@ -51,26 +73,17 @@ public static class Extensions
{
var healthOptions = app.Configuration.GetOptions<HealthOptions>(nameof(HealthOptions));
if (!healthOptions.Enabled)
return app;
app.UseHealthChecks("/healthz",
new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status500InternalServerError,
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
}
})
.UseHealthChecksUI(options =>
if (app.Environment.IsDevelopment())
{
app.MapHealthChecks(HealthEndpointPath);
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
options.ApiPath = "/healthcheck";
options.UIPath = "/healthcheck-ui";
Predicate = r => r.Tags.Contains("live"),
});
}
if (healthOptions.Enabled)
app.MapHealthChecksUI(options => options.UIPath = "/health-ui");
return app;
}

View File

@ -19,7 +19,7 @@ namespace BuildingBlocks.Jwt
{
options.Authority = jwtOptions.Authority;
options.Audience = jwtOptions.Audience;
options.RequireHttpsMetadata = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{

View File

@ -1,56 +0,0 @@
using System.Globalization;
using System.Text;
using BuildingBlocks.Web;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
using Serilog.Exceptions;
using Serilog.Sinks.SpectreConsole;
namespace BuildingBlocks.Logging
{
public static class Extensions
{
public static WebApplicationBuilder AddCustomSerilog(this WebApplicationBuilder builder, IWebHostEnvironment env)
{
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
{
var logOptions = context.Configuration.GetSection(nameof(LogOptions)).Get<LogOptions>();
var logLevel = Enum.TryParse<LogEventLevel>(logOptions.Level, true, out var level)
? level
: LogEventLevel.Information;
loggerConfiguration
.MinimumLevel.Is(logLevel)
.WriteTo.SpectreConsole(logOptions.LogTemplate, logLevel)
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
// Only show ef-core information in error level
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Error)
// Filter out ASP.NET Core infrastructure logs that are Information and below
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.WithExceptionDetails()
.Enrich.FromLogContext()
.ReadFrom.Configuration(context.Configuration);
if (logOptions.File is { Enabled: true })
{
var root = env.ContentRootPath;
Directory.CreateDirectory(Path.Combine(root, "logs"));
var path = string.IsNullOrWhiteSpace(logOptions.File.Path) ? "logs/.txt" : logOptions.File.Path;
if (!Enum.TryParse<RollingInterval>(logOptions.File.Interval, true, out var interval))
{
interval = RollingInterval.Day;
}
loggerConfiguration.WriteTo.File(path, rollingInterval: interval, encoding: Encoding.UTF8, outputTemplate: logOptions.LogTemplate);
}
});
return builder;
}
}
}

View File

@ -1,8 +0,0 @@
namespace BuildingBlocks.Logging;
public class FileOptions
{
public bool Enabled { get; set; }
public string Path { get; set; }
public string Interval { get; set; }
}

View File

@ -1,39 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Serilog;
namespace BuildingBlocks.Logging;
public static class LogEnrichHelper
{
//ref: https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-logging-the-selected-endpoint-name-with-serilog/
public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
var request = httpContext.Request;
// Set all the common properties available for every request
diagnosticContext.Set("Host", request.Host);
diagnosticContext.Set("Protocol", request.Protocol);
diagnosticContext.Set("Scheme", request.Scheme);
// Only set it if available. You're not sending sensitive data in a querystring right?!
if (request.QueryString.HasValue)
{
diagnosticContext.Set("QueryString", request.QueryString.Value);
}
// Set the content-type of the Response at this point
diagnosticContext.Set("ContentType", httpContext.Response.ContentType);
// Retrieve the IEndpointFeature selected for the request
var endpoint = httpContext.GetEndpoint();
if (endpoint is object) // endpoint != null
{
diagnosticContext.Set("EndpointName", endpoint.DisplayName);
}
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress);
diagnosticContext.Set("UserId", request.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
}
}

View File

@ -1,9 +0,0 @@
namespace BuildingBlocks.Logging
{
public class LogOptions
{
public string Level { get; set; }
public FileOptions File { get; set; }
public string LogTemplate { get; set; }
}
}

View File

@ -2,6 +2,7 @@ using System.Reflection;
using BuildingBlocks.Web;
using MassTransit;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -58,18 +59,30 @@ public static class Extensions
configure.UsingRabbitMq(
(context, configurator) =>
{
var rabbitMqOptions =
services.GetOptions<RabbitMqOptions>(nameof(RabbitMqOptions));
var configuration = context.GetRequiredService<IConfiguration>();
configurator.Host(
rabbitMqOptions?.HostName,
rabbitMqOptions?.Port ?? 5672,
"/",
h =>
{
h.Username(rabbitMqOptions?.UserName);
h.Password(rabbitMqOptions?.Password);
});
var aspireConnectionString = configuration.GetConnectionString("rabbitmq");
if (!string.IsNullOrEmpty(aspireConnectionString))
{
configurator.Host(new Uri(aspireConnectionString));
}
else
{
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>(nameof(RabbitMqOptions));
ArgumentNullException.ThrowIfNull(rabbitMqOptions);
configurator.Host(
rabbitMqOptions?.HostName,
rabbitMqOptions?.Port ?? 5672,
"/",
h =>
{
h.Username(rabbitMqOptions.UserName);
h.Password(rabbitMqOptions.Password);
});
}
configurator.ConfigureEndpoints(context);

View File

@ -20,7 +20,14 @@ namespace BuildingBlocks.Mongo
where TContextService : IMongoDbContext
where TContextImplementation : MongoDbContext, TContextService
{
services.Configure<MongoOptions>(configuration.GetSection(nameof(MongoOptions)));
// Configure MongoOptions with Aspire-aware defaults
services.AddOptions<MongoOptions>()
.Bind(configuration.GetSection(nameof(MongoOptions)))
.PostConfigure(options =>
{
var aspireConnectionString = configuration.GetConnectionString("mongo");
options.ConnectionString = aspireConnectionString ?? options.ConnectionString;
});
if (configurator is { })
{

View File

@ -29,9 +29,11 @@ namespace BuildingBlocks.OpenTelemetryCollector;
// https://blog.codingmilitia.com/2023/09/05/observing-dotnet-microservices-with-opentelemetry-logs-traces-metrics/
public static class Extensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
public static WebApplicationBuilder AddCustomObservability(this WebApplicationBuilder builder)
{
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
builder.Services.AddSingleton<IDiagnosticsProvider, CustomeDiagnosticsProvider>();
builder.AddCoreDiagnostics();
@ -147,6 +149,13 @@ public static class Extensions
.AddAspNetCoreInstrumentation(options =>
{
options.RecordException = true;
// Don't trace requests to the health endpoint to avoid filling the dashboard with noise
options.Filter = httpContext =>
!(httpContext.Request.Path.StartsWithSegments(
HealthEndpointPath, StringComparison.OrdinalIgnoreCase) ||
httpContext.Request.Path.StartsWithSegments(
AlivenessEndpointPath, StringComparison.OrdinalIgnoreCase
));
})
.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation(instrumentationOptions =>

View File

@ -1,26 +1,32 @@
using BuildingBlocks.Web;
using Microsoft.AspNetCore.Hosting;
using Humanizer;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace BuildingBlocks.PersistMessageProcessor;
public static class Extensions
{
public static IServiceCollection AddPersistMessageProcessor(this IServiceCollection services)
public static IServiceCollection AddPersistMessageProcessor(this WebApplicationBuilder builder, string? connectionName = "persist-message")
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
services.AddValidateOptions<PersistMessageOptions>();
builder.Services.AddValidateOptions<PersistMessageOptions>();
services.AddDbContext<PersistMessageDbContext>(
builder.Services.AddDbContext<PersistMessageDbContext>(
(sp, options) =>
{
var persistMessageOptions = sp.GetRequiredService<PersistMessageOptions>();
var aspireConnectionString = builder.Configuration.GetConnectionString(connectionName.Kebaberize());
var connectionString = aspireConnectionString ?? sp.GetRequiredService<PersistMessageOptions>().ConnectionString;
ArgumentException.ThrowIfNullOrEmpty(connectionString);
options.UseNpgsql(
persistMessageOptions.ConnectionString,
connectionString,
dbOptions =>
{
dbOptions.MigrationsAssembly(
@ -34,7 +40,7 @@ public static class Extensions
w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
});
services.AddScoped<IPersistMessageDbContext>(
builder.Services.AddScoped<IPersistMessageDbContext>(
provider =>
{
var persistMessageDbContext =
@ -46,10 +52,10 @@ public static class Extensions
return persistMessageDbContext;
});
services.AddScoped<IPersistMessageProcessor, PersistMessageProcessor>();
builder.Services.AddScoped<IPersistMessageProcessor, PersistMessageProcessor>();
services.AddHostedService<PersistMessageBackgroundService>();
builder.Services.AddHostedService<PersistMessageBackgroundService>();
return services;
return builder.Services;
}
}

View File

@ -1,7 +0,0 @@
namespace BuildingBlocks.Polly;
public class CircuitBreakerOptions
{
public int RetryCount { get; set; }
public int BreakDuration { get; set; }
}

View File

@ -1,20 +1,23 @@
using Microsoft.Extensions.Logging;
namespace BuildingBlocks.Polly;
using global::Polly;
using Serilog;
using Exception = System.Exception;
public static class Extensions
{
public static ILogger Logger { get; set; } = null!;
public static T RetryOnFailure<T>(this object retrySource, Func<T> action, int retryCount = 3)
{
var retryPolicy = Policy
.Handle<Exception>()
.Retry(retryCount, (exception, retryAttempt, context) =>
{
Log.Information($"Retry attempt: {retryAttempt}");
Log.Error($"Exception: {exception.Message}");
});
{
Logger.LogInformation($"Retry attempt: {retryAttempt}");
Logger.LogError($"Exception: {exception.Message}");
});
return retryPolicy.Execute(action);
}

View File

@ -1,46 +0,0 @@
namespace BuildingBlocks.Polly;
using System.Net;
using Ardalis.GuardClauses;
using BuildingBlocks.Web;
using global::Polly;
using Grpc.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public static class GrpcCircuitBreaker
{
//ref: https://anthonygiretti.com/2020/03/31/grpc-asp-net-core-3-1-resiliency-with-polly/
public static IHttpClientBuilder AddGrpcCircuitBreakerPolicyHandler(this IHttpClientBuilder httpClientBuilder)
{
return httpClientBuilder.AddPolicyHandler((sp, _) =>
{
var options = sp.GetRequiredService<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
Guard.Against.Null(options, nameof(options));
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("PollyGrpcCircuitBreakerPoliciesLogger");
return Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: options.CircuitBreaker.RetryCount,
durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreaker.BreakDuration),
onBreak: (response, breakDuration) =>
{
if (response?.Exception != null)
{
logger.LogError(response.Exception,
"Service shutdown during {BreakDuration} after {RetryCount} failed retries",
breakDuration,
options.CircuitBreaker.RetryCount);
}
},
onReset: () =>
{
logger.LogInformation("Service restarted");
});
});
}
}

View File

@ -1,43 +0,0 @@
namespace BuildingBlocks.Polly;
using System.Net;
using Ardalis.GuardClauses;
using BuildingBlocks.Web;
using global::Polly;
using Grpc.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public static class GrpcRetry
{
//ref: https://anthonygiretti.com/2020/03/31/grpc-asp-net-core-3-1-resiliency-with-polly/
public static IHttpClientBuilder AddGrpcRetryPolicyHandler(this IHttpClientBuilder httpClientBuilder)
{
return httpClientBuilder.AddPolicyHandler((sp, _) =>
{
var options = sp.GetRequiredService<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
Guard.Against.Null(options, nameof(options));
return Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(options.Retry.RetryCount,
retryAttempt => TimeSpan.FromSeconds(options.Retry.SleepDuration),
onRetry: (response, timeSpan, retryCount, context) =>
{
if (response?.Exception != null)
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("PollyGrpcRetryPoliciesLogger");
logger.LogError(response.Exception,
"Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.",
response.Result.StatusCode,
timeSpan,
retryCount);
}
});
});
}
}

View File

@ -1,48 +0,0 @@
namespace BuildingBlocks.Polly;
using System.Net;
using Ardalis.GuardClauses;
using BuildingBlocks.Web;
using global::Polly;
using global::Polly.Extensions.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Exception = System.Exception;
public static class HttpClientCircuitBreaker
{
// ref: https://anthonygiretti.com/2019/03/26/best-practices-with-httpclient-and-retry-policies-with-polly-in-net-core-2-part-2/
public static IHttpClientBuilder AddHttpClientCircuitBreakerPolicyHandler(this IHttpClientBuilder httpClientBuilder)
{
return httpClientBuilder.AddPolicyHandler((sp, _) =>
{
var options = sp.GetRequiredService<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
Guard.Against.Null(options, nameof(options));
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("PollyHttpClientCircuitBreakerPoliciesLogger");
return HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.BadRequest)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: options.CircuitBreaker.RetryCount,
durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreaker.BreakDuration),
onBreak: (response, breakDuration) =>
{
if (response?.Exception != null)
{
logger.LogError(response.Exception,
"Service shutdown during {BreakDuration} after {RetryCount} failed retries",
breakDuration,
options.CircuitBreaker.RetryCount);
}
},
onReset: () =>
{
logger.LogInformation("Service restarted");
});
});
}
}

View File

@ -1,44 +0,0 @@
namespace BuildingBlocks.Polly;
using System.Net;
using Ardalis.GuardClauses;
using BuildingBlocks.Web;
using global::Polly;
using global::Polly.Extensions.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public static class HttpClientRetry
{
// ref: https://anthonygiretti.com/2019/03/26/best-practices-with-httpclient-and-retry-policies-with-polly-in-net-core-2-part-2/
public static IHttpClientBuilder AddHttpClientRetryPolicyHandler(this IHttpClientBuilder httpClientBuilder)
{
return httpClientBuilder.AddPolicyHandler((sp, _) =>
{
var options = sp.GetRequiredService<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
Guard.Against.Null(options, nameof(options));
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("PollyHttpClientRetryPoliciesLogger");
return HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.BadRequest)
.OrResult(msg => msg.StatusCode == HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(retryCount: options.Retry.RetryCount,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(options.Retry.SleepDuration),
onRetry: (response, timeSpan, retryCount, context) =>
{
if (response?.Exception != null)
{
logger.LogError(response.Exception,
"Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.",
response.Result.StatusCode,
timeSpan,
retryCount);
}
});
});
}
}

View File

@ -1,7 +0,0 @@
namespace BuildingBlocks.Polly;
public class PolicyOptions
{
public RetryOptions Retry { get; set; }
public CircuitBreakerOptions CircuitBreaker { get; set; }
}

View File

@ -1,7 +0,0 @@
namespace BuildingBlocks.Polly;
public class RetryOptions
{
public int RetryCount { get; set; }
public int SleepDuration { get; set; }
}

View File

@ -20,14 +20,13 @@ using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using NSubstitute;
using Respawn;
using Serilog;
using WebMotions.Fake.Authentication.JwtBearer;
using Xunit;
using Xunit.Abstractions;
using ILogger = Serilog.ILogger;
namespace BuildingBlocks.TestBase;
@ -154,14 +153,15 @@ where TEntryPoint : class
// ref: https://github.com/trbenning/serilog-sinks-xunit
public ILogger CreateLogger(ITestOutputHelper output)
{
if (output != null)
{
return new LoggerConfiguration()
.WriteTo.TestOutput(output)
.CreateLogger();
}
if (output == null)
return null;
return null;
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXunit(output);
builder.SetMinimumLevel(LogLevel.Debug);
});
return loggerFactory.CreateLogger("TestLogger");
}
protected async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)

View File

@ -4,22 +4,12 @@ using BuildingBlocks.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider((context, options) =>
{
// Service provider validation
// ref: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
options.ValidateScopes = context.HostingEnvironment.IsDevelopment() || context.HostingEnvironment.IsStaging() || context.HostingEnvironment.IsEnvironment("tests");
options.ValidateOnBuild = true;
});
builder.AddMinimalEndpoints(assemblies: typeof(BookingRoot).Assembly);
builder.AddInfrastructure();
var app = builder.Build();
app.MapMinimalEndpoints();
app.UseAuthentication();
app.UseAuthorization();
app.UseInfrastructure();
app.Run();

View File

@ -1,46 +1,36 @@
{
"App": "Booking-Service",
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"EventStoreOptions": {
"ConnectionString": "esdb://eventstore:2113?tls=false"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "booking-db"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "booking",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "booking-api"
},
"Grpc": {
"FlightAddress": "flight:5003",
"PassengerAddress": "passenger:5003"
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
"App": "Booking-Service",
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
},
"AllowedHosts": "*"
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"EventStoreOptions": {
"ConnectionString": "esdb://eventstore:2113?tls=false"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "booking-db"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "booking",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "booking-api"
},
"Grpc": {
"FlightAddress": "flight:5003",
"PassengerAddress": "passenger:5003"
},
"AllowedHosts": "*"
}

View File

@ -2,14 +2,10 @@
"AppOptions": {
"Name": "Booking-Service"
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"Jwt": {
"Authority": "http://localhost:6005",
@ -43,14 +39,14 @@
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "booking-db"
},
"HealthOptions": {
"Enabled": false
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"HealthOptions": {
"Enabled": false
},
"ObservabilityOptions": {
"InstrumentationName": "booking_service",
"OTLPOptions": {

View File

@ -1,26 +1,23 @@
{
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "booking",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "booking",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"MongoOptions": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "booking-db-test"
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
},
"MongoOptions": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "booking-db-test"
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
}

View File

@ -2,31 +2,76 @@ using Booking.Configuration;
using BuildingBlocks.Web;
using Flight;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Passenger;
using Polly;
namespace Booking.Extensions.Infrastructure;
using BuildingBlocks.Polly;
public static class GrpcClientExtensions
{
public static IServiceCollection AddGrpcClients(this IServiceCollection services)
{
var grpcOptions = services.GetOptions<GrpcOptions>("Grpc");
var resilienceOptions = services.GetOptions<HttpStandardResilienceOptions>(nameof(HttpStandardResilienceOptions));
services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.FlightAddress);
})
.AddGrpcRetryPolicyHandler()
.AddGrpcCircuitBreakerPolicyHandler();
{
o.Address = new Uri(grpcOptions.FlightAddress);
})
.AddResilienceHandler(
"grpc-flight-resilience",
options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry(
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = timeSpan * 2,
});
options.AddTimeout(
new HttpTimeoutStrategyOptions
{
Timeout = timeSpan * 3,
});
});
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddGrpcRetryPolicyHandler()
.AddGrpcCircuitBreakerPolicyHandler();
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry(
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = timeSpan * 2,
});
options.AddTimeout(
new HttpTimeoutStrategyOptions
{
Timeout = timeSpan * 3,
});
});
return services;
}

View File

@ -1,10 +1,8 @@
using System.Threading.RateLimiting;
using Booking.Data;
using BuildingBlocks.Core;
using BuildingBlocks.EventStoreDB;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Jwt;
using BuildingBlocks.Logging;
using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
@ -20,7 +18,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Booking.Extensions.Infrastructure;
@ -31,6 +28,26 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
builder.Services.AddCustomHealthCheck();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, BookingEventMapper>();
builder.Services.AddScoped<IEventDispatcher, EventDispatcher>();
@ -44,25 +61,10 @@ public static class InfrastructureExtensions
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
builder.Services.AddPersistMessageProcessor();
builder.AddPersistMessageProcessor(nameof(PersistMessage));
builder.AddMongoDbContext<BookingReadDbContext>();
builder.Services.AddEndpointsApiExplorer();
builder.AddCustomSerilog(env);
builder.Services.AddJwt();
builder.Services.AddHttpContextAccessor();
builder.Services.AddAspnetOpenApi();
@ -71,9 +73,7 @@ public static class InfrastructureExtensions
builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly);
builder.Services.AddProblemDetails();
builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly);
builder.Services.AddCustomHealthCheck();
builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(BookingRoot).Assembly);
builder.AddCustomObservability();
builder.Services.AddTransient<AuthHeaderHandler>();
// ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce
@ -91,15 +91,14 @@ public static class InfrastructureExtensions
var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;
});
app.UseCorrelationId();
app.UseCustomHealthCheck();
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));
if (env.IsDevelopment())

View File

@ -4,14 +4,6 @@ using Flight.Extensions.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider((context, options) =>
{
// Service provider validation
// ref: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
options.ValidateScopes = context.HostingEnvironment.IsDevelopment() || context.HostingEnvironment.IsStaging() || context.HostingEnvironment.IsEnvironment("tests");
options.ValidateOnBuild = true;
});
builder.AddMinimalEndpoints(assemblies: typeof(FlightRoot).Assembly);
builder.AddInfrastructure();
@ -19,8 +11,6 @@ var app = builder.Build();
// ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-7.0#routing-basics
app.MapMinimalEndpoints();
app.UseAuthentication();
app.UseAuthorization();
app.UseInfrastructure();
app.Run();

View File

@ -1,36 +1,32 @@
{
"App": "Flight-Service",
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
}
},
"PostgresOptions": {
"ConnectionString": "Server=postgres;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "flight-api"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "flight",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "flight-db"
},
"AllowedHosts": "*"
"App": "Flight-Service",
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=postgres;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "flight-api"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "flight",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "flight-db"
},
"AllowedHosts": "*"
}

View File

@ -2,13 +2,9 @@
"AppOptions": {
"Name": "Flight-Service"
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {

View File

@ -1,25 +1,22 @@
{
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=flight_test;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "flight",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=flight_test;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "flight",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"PersistMessageOptions": {
"Interval": 2,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
},
"PersistMessageOptions": {
"Interval": 2,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
}

View File

@ -1,10 +1,8 @@
using System.Threading.RateLimiting;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
using BuildingBlocks.Exception;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Jwt;
using BuildingBlocks.Logging;
using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
@ -23,7 +21,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Flight.Extensions.Infrastructure;
@ -35,6 +32,26 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
builder.Services.AddCustomHealthCheck();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, FlightEventMapper>();
@ -50,27 +67,12 @@ public static class InfrastructureExtensions
var appOptions = builder.Services.GetOptions<AppOptions>(nameof(AppOptions));
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
builder.AddCustomDbContext<FlightDbContext>();
builder.AddCustomDbContext<FlightDbContext>(nameof(Flight));
builder.Services.AddScoped<IDataSeeder, FlightDataSeeder>();
builder.AddMongoDbContext<FlightReadDbContext>();
builder.Services.AddPersistMessageProcessor();
builder.AddPersistMessageProcessor(nameof(PersistMessage));
builder.Services.AddEndpointsApiExplorer();
builder.AddCustomSerilog(env);
builder.Services.AddJwt();
builder.Services.AddAspnetOpenApi();
builder.Services.AddCustomVersioning();
@ -78,8 +80,6 @@ public static class InfrastructureExtensions
builder.Services.AddCustomMapster(typeof(FlightRoot).Assembly);
builder.Services.AddHttpContextAccessor();
builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(FlightRoot).Assembly);
builder.AddCustomObservability();
builder.Services.AddCustomHealthCheck();
builder.Services.AddGrpc(options =>
{
@ -97,18 +97,17 @@ public static class InfrastructureExtensions
var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;
});
app.UseCorrelationId();
app.UseMigration<FlightDbContext>();
app.UseCustomHealthCheck();
app.MapGrpcService<FlightGrpcServices>();
app.UseRateLimiter();
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));
if (env.IsDevelopment())

View File

@ -1,26 +1,15 @@
using BuildingBlocks.Web;
using Identity;
using Identity.Configurations;
using Identity.Extensions.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider((context, options) =>
{
// Service provider validation
// ref: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
options.ValidateScopes = context.HostingEnvironment.IsDevelopment() || context.HostingEnvironment.IsStaging() || context.HostingEnvironment.IsEnvironment("tests");
options.ValidateOnBuild = true;
});
builder.AddMinimalEndpoints(assemblies: typeof(IdentityRoot).Assembly);
builder.AddInfrastructure();
var app = builder.Build();
app.MapMinimalEndpoints();
app.UseAuthentication();
app.UseAuthorization();
app.UseInfrastructure();
app.Run();

View File

@ -1,31 +1,27 @@
{
"App": "Identity-Service",
"PostgresOptions": {
"ConnectionString": "Server=postgres;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"AuthOptions": {
"IssuerUri": "http://identity:80"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "identity",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
}
},
"AllowedHosts": "*"
"App": "Identity-Service",
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=postgres;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"AuthOptions": {
"IssuerUri": "http://identity:80"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "identity",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"AllowedHosts": "*"
}

View File

@ -2,6 +2,11 @@
"AppOptions": {
"Name": "Identity-Service"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true"
},
@ -15,23 +20,14 @@
"Password": "guest",
"Port": 5672
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
}
},
"HealthOptions": {
"Enabled": false
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"HealthOptions": {
"Enabled": false
},
"ObservabilityOptions": {
"InstrumentationName": "identity_service",
"OTLPOptions": {

View File

@ -1,28 +1,25 @@
{
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=identity_test;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"AuthOptions": {
"IssuerUri": "http://localhost:6005"
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "identity",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=identity_test;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"AuthOptions": {
"IssuerUri": "http://localhost:6005"
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "identity",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
}

View File

@ -0,0 +1 @@
{"Version":1,"Id":"E1668D5B7CCDD18C610506FCA7C5D194","Created":"2025-07-21T17:14:15.7672364Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Ai8D9hfDhJIplL3Rv2bStBd2WatjNqn9OUgTfnOqxduEkXmsnrgWiHbuP-AYTE2ZeIxoDSDbZWBD8dJbAMe5PfH-FC7E5njE18xMIRyRxxvLHBcINyJ3fAP7lk5-uTl_F2DhoY6S_DCtbgbXCoB57FPr8DJzMPN0q37yuC34ZQYhfgLbkfydesVGqW1DF2_a5pd2KAFK_m8_FVy1GfHvTFnLjuURASp8eKEDJrtii4683uplUkycVJRHfHHQta1PuJW3KOLcX9jSePWDEBnQkjsiHm2O3BQFDdsMHbr8F2cLGW9uMIYNd96QyiH7nUgVZ0i5R-sHpbnGzUZFnZvpWLJFZrT3269nBLL2-t-7Sh99laIIT54cdZsW_uGFZ7r1MqyEPt9MoHqXXuOuhrn22QQeQURGmyUt4YyxdKCozsEPs7MR4IR_1iY93Rv4ThEn24vq1-guK-58pu3DurgigbTB7hFToOtqBHEhiVyXKgoNGOXH7UW5bJLFj4JtOIN_2SaTohhw9pSf8yH7gf6Chp2aVGUQt0OnVLbLu__EbB9XzJblQHLi6Ddm8uXdrJYXW17iBdEUHL8xD4hOiE4SR-bhD_d8c9yD7ydSyIwdistrVFRYpeDc6n-kQLWC6r1Z1oxIQte7ph6n3ygAWSwyCzSkI1GKJoh6QdR6rrs4EN9jUdFN4NgjQqftlLaE6xKq3f82wbCTk5-Iy8jJlr-xdDue_myN6Z_XOJ01pyjDkX9u3Aa0UuNFlvGYde9bJa6R7VxE5ggVozjpB_3FNUc91_3merwjq3K5-3S3EnhU8Nxi44Yjsw6-pVLbyF0Tgbh3uGUWI4EVv_nUC94cLaftuutq6rApSs1lF1jq_RK2P9YBQyCNl23JYMWEkPscMGIrx0306eLpWelYu_TpLk2lj5JfM7ep4uQ714bQXMLAdNX9GttAOOKlTrb4--uz8kjHaOSaJ01tvutQpog-RrudUVHX46_EA43U0jazAVQJXX7uDC5CEiSfCXozk7Gvw56gk72Ha1Ygz_qfaSn1hv6Q4cHhuhHzRV_aGui5oztal-oSh-zd_6BobstL1iZHhukHrUq_gVcFhYnqNDgzKT1L21AV9d0LbqONkFZ6kBGZ7ZIE2ymIqRx4DgBCdkmgZZUAZRqK3wkYW3oKI18l1aF7dGrrUnRSFVg5Jr0BYSmVPBVpfGLUHb3VHpjPVxPMV2W-ak0JG01jL7OQBuKXUW115Qd89GE2rj_SnInDg6ljW94zDHbqb_8gH3WOSWawM6021yQlRpMe7xqv6bYVeAS6WZBphKPxLV3BtG0_23IOiEbvsreT6UvIn9hx-VkJ_CADmlKQ8uNFxUIDvop6vGq5zjPgQz2-kJ529MUrp-ayn-oXTOrRgrYzLsUAH-gzf_osIMKsGPQakQMmnL7jsyX0z7Ci2wKuZ7pgxw8DQk8PKrk_yNuTMRQN0etUvv-nSCtPbcHALqMN9VgcHgCEEZTTP4A0pUUNAWewd2hJoh12H4_JlyJirHFDK5ZyM_uWKjAfVbh4RFLnxPaEEdQsew-WgXXE4Ne965OG283Q7TCv2MUCh-UynOz2ySw1FKD5XeqkOYmIXlrGf7DuhXQcam1SW9_8N5k9_lIdG_QPW6cM7HR3amBiHXnR7I-MBmTf3CLirEu_CrP6fdcGjrWBtbpfHCOPXNUseltmEWc0Ph3FoIYSR7F8o7cuYHWm0cuK3yC_73OXSDuDWmkl79qhyy3FJHV-6RTlsiyIZiOZ6Sxg24NKr5lp8FoqEZ2iQIKPfIPIYuD4YGmRKkk4WTV_slY68upm06xojpzQLs_resmmSc-WmkkRMgmuXaOxQr5oqZEFFqC3XLU3LAQkmOjMfJk4DWYWBrT-WzkfRJLmXhi3_DH7xh4IQ6t6Patumr-DT70FLySntRsvGS-wcI9U_w39gfGupOU6ytZFN_0lcNrYWR_0K_IjD_lmEUPdywFNs1_EbrIFSPo9pKZRveo1HLcSEatZju_JGqIT0lUQ0nZDIximnODr7w217fA_Vq9pLoMZw3AvT42vQqeg3jqoROF_fkz43fMmCNdZlhLuRUQ_IjZ5qmgKEaodtM8ZtgvB0qJNn_5NzX7ArreV-XQpOf8TxOvIWwmM4C9Ra3-Uo0CDrgIwxxtW9QjnlLx__Nb_Pcvf63dTtvt8y6FLFki3eeA_Q0IC4v0oKRQAVd1BtybbTMYMojXsrc1WElijhfDvybG04y6oWvh0GWx97FYk2bx3bTTi5EB-tXmnD12V2Jigw1e1x4rWpFh28A5eO6o5ymX1KRFdgG2nV45__NmJoZxmgWFFmTkNQSpHgl05NRRzhbcIrAzfAFnMQxdzrOCg4omRzf3yygQEBvuzpfThoZr3eaDdMCtaUuS_eUABIFXAYsRQpI33nhK4ac11S_m7k_vUCc1OWZ7mVLodyVqF3b3vqDIHw-EFNppSUUlpUzeLN2Q","DataProtected":true}

View File

@ -1,8 +1,6 @@
using System.Threading.RateLimiting;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Logging;
using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit;
using BuildingBlocks.OpenApi;
@ -12,7 +10,6 @@ using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web;
using Figgle;
using FluentValidation;
using Identity.Configurations;
using Identity.Data;
using Identity.Data.Seed;
using Microsoft.AspNetCore.Builder;
@ -21,11 +18,9 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Identity.Extensions.Infrastructure;
public static class InfrastructureExtensions
{
public static WebApplicationBuilder AddInfrastructure(this WebApplicationBuilder builder)
@ -33,6 +28,26 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
builder.Services.AddCustomHealthCheck();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, IdentityEventMapper>();
builder.Services.AddScoped<IEventDispatcher, EventDispatcher>();
@ -45,36 +60,19 @@ public static class InfrastructureExtensions
var appOptions = builder.Services.GetOptions<AppOptions>(nameof(AppOptions));
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
builder.Services.AddPersistMessageProcessor();
builder.AddCustomDbContext<IdentityContext>();
builder.AddPersistMessageProcessor();
builder.AddCustomDbContext<IdentityContext>(nameof(Identity));
builder.Services.AddScoped<IDataSeeder, IdentityDataSeeder>();
builder.AddCustomSerilog(env);
builder.Services.AddAspnetOpenApi();
builder.Services.AddCustomVersioning();
builder.Services.AddCustomMediatR();
builder.Services.AddValidatorsFromAssembly(typeof(IdentityRoot).Assembly);
builder.Services.AddProblemDetails();
builder.Services.AddCustomMapster(typeof(IdentityRoot).Assembly);
builder.Services.AddCustomHealthCheck();
builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(IdentityRoot).Assembly);
builder.AddCustomObservability();
builder.AddCustomIdentityServer();
@ -93,18 +91,17 @@ public static class InfrastructureExtensions
var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseForwardedHeaders();
app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;
});
app.UseCorrelationId();
app.UseMigration<IdentityContext>();
app.UseCustomHealthCheck();
app.UseIdentityServer();
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));

View File

@ -1,4 +1,3 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using FluentAssertions;

View File

@ -4,22 +4,12 @@ using Passenger.Extensions.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider((context, options) =>
{
// Service provider validation
// ref: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
options.ValidateScopes = context.HostingEnvironment.IsDevelopment() || context.HostingEnvironment.IsStaging() || context.HostingEnvironment.IsEnvironment("tests");
options.ValidateOnBuild = true;
});
builder.AddMinimalEndpoints(assemblies: typeof(PassengerRoot).Assembly);
builder.AddInfrastructure();
var app = builder.Build();
app.MapMinimalEndpoints();
app.UseAuthentication();
app.UseAuthorization();
app.UseInfrastructure();
app.Run();

View File

@ -1,36 +1,32 @@
{
"App": "Passenger-Service",
"PostgresOptions": {
"ConnectionString": "Server=postgres;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "passenger-api"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "passenger-db"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "passenger",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
}
},
"AllowedHosts": "*"
"App": "Passenger-Service",
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=postgres;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=postgres;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "passenger-api"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",
"DatabaseName": "passenger-db"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",
"ExchangeName": "passenger",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"AllowedHosts": "*"
}

View File

@ -2,6 +2,11 @@
"AppOptions": {
"Name": "Passenger-Service"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true"
},
@ -20,23 +25,14 @@
"Password": "guest",
"Port": 5672
},
"LogOptions": {
"Level": "information",
"LogTemplate": "{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}",
"File": {
"Enabled": false,
"Path": "logs/logs.txt",
"Interval": "day"
}
},
"HealthOptions": {
"Enabled": false
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"HealthOptions": {
"Enabled": false
},
"ObservabilityOptions": {
"InstrumentationName": "passenger_service",
"OTLPOptions": {

View File

@ -1,25 +1,22 @@
{
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=passenger_test;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "passenger",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"PostgresOptions": {
"ConnectionString": "Server=localhost;Port=5432;Database=passenger_test;User Id=postgres;Password=postgres;Include Error Detail=true"
},
"RabbitMqOptions": {
"HostName": "localhost",
"ExchangeName": "passenger",
"UserName": "guest",
"Password": "guest",
"Port": 5672
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
},
"PersistMessageOptions": {
"Interval": 30,
"Enabled": true,
"ConnectionString": "Server=localhost;Port=5432;Database=persist_message_test;User Id=postgres;Password=postgres;Include Error Detail=true"
}
}

View File

@ -1,10 +1,8 @@
using System.Threading.RateLimiting;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
using BuildingBlocks.Exception;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Jwt;
using BuildingBlocks.Logging;
using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
@ -22,7 +20,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Passenger.Data;
using Passenger.GrpcServer.Services;
using Serilog;
namespace Passenger.Extensions.Infrastructure;
@ -33,6 +30,27 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
builder.Services.AddCustomHealthCheck();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, PassengerEventMapper>();
builder.Services.AddScoped<IEventDispatcher, EventDispatcher>();
@ -46,28 +64,10 @@ public static class InfrastructureExtensions
var appOptions = builder.Services.GetOptions<AppOptions>(nameof(AppOptions));
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddRateLimiter(
options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ??
httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
builder.Services.AddPersistMessageProcessor();
builder.AddCustomDbContext<PassengerDbContext>();
builder.AddPersistMessageProcessor(nameof(PersistMessage));
builder.AddCustomDbContext<PassengerDbContext>(nameof(Passenger));
builder.AddMongoDbContext<PassengerReadDbContext>();
builder.AddCustomSerilog(env);
builder.Services.AddJwt();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddAspnetOpenApi();
@ -77,9 +77,7 @@ public static class InfrastructureExtensions
builder.Services.AddProblemDetails();
builder.Services.AddCustomMapster(typeof(PassengerRoot).Assembly);
builder.Services.AddHttpContextAccessor();
builder.Services.AddCustomHealthCheck();
builder.Services.AddCustomMassTransit(env, TransportType.RabbitMq, typeof(PassengerRoot).Assembly);
builder.AddCustomObservability();
builder.Services.AddGrpc(
options =>
@ -96,19 +94,16 @@ public static class InfrastructureExtensions
var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(
options =>
{
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;
});
app.UseCorrelationId();
app.UseMigration<PassengerDbContext>();
app.UseCustomHealthCheck();
app.MapGrpcService<PassengerGrpcServices>();
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));