0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +00:00

feat: Aspire (#80)

This commit is contained in:
Alex 2024-11-22 16:26:11 +01:00 committed by GitHub
commit 5e8d6ad45f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 401 additions and 59 deletions

View File

@ -32,6 +32,7 @@
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" /> <ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" /> <ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" />
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" /> <ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
<ProjectReference Include="..\CleanArchitecture.ServiceDefaults\CleanArchitecture.ServiceDefaults.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,41 @@
using System;
using CleanArchitecture.Domain.Rabbitmq;
using Microsoft.Extensions.Configuration;
namespace CleanArchitecture.Api.Extensions;
public static class ConfigurationExtensions
{
public static RabbitMqConfiguration GetRabbitMqConfiguration(
this IConfiguration configuration)
{
var isAspire = configuration["ASPIRE_ENABLED"] == "true";
var rabbitEnabled = configuration["RabbitMQ:Enabled"];
var rabbitHost = configuration["RabbitMQ:Host"];
var rabbitPort = configuration["RabbitMQ:Port"];
var rabbitUser = configuration["RabbitMQ:Username"];
var rabbitPass = configuration["RabbitMQ:Password"];
if (isAspire)
{
rabbitEnabled = "true";
var connectionString = configuration["ConnectionStrings:RabbitMq"];
var rabbitUri = new Uri(connectionString!);
rabbitHost = rabbitUri.Host;
rabbitPort = rabbitUri.Port.ToString();
rabbitUser = rabbitUri.UserInfo.Split(':')[0];
rabbitPass = rabbitUri.UserInfo.Split(':')[1];
}
return new RabbitMqConfiguration()
{
Host = rabbitHost ?? "",
Port = int.Parse(rabbitPort ?? "0"),
Enabled = bool.Parse(rabbitEnabled ?? "false"),
Username = rabbitUser ?? "",
Password = rabbitPass ?? ""
};
}
}

View File

@ -6,18 +6,20 @@ using CleanArchitecture.Domain.Extensions;
using CleanArchitecture.Domain.Rabbitmq.Extensions; using CleanArchitecture.Domain.Rabbitmq.Extensions;
using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions; using CleanArchitecture.Infrastructure.Extensions;
using CleanArchitecture.ServiceDefaults;
using HealthChecks.ApplicationStatus.DependencyInjection; using HealthChecks.ApplicationStatus.DependencyInjection;
using HealthChecks.UI.Client; using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddGrpc(); builder.Services.AddGrpc();
builder.Services.AddGrpcReflection(); builder.Services.AddGrpcReflection();
@ -28,32 +30,36 @@ builder.Services
.AddDbContextCheck<ApplicationDbContext>() .AddDbContextCheck<ApplicationDbContext>()
.AddApplicationStatus(); .AddApplicationStatus();
var isAspire = builder.Configuration["ASPIRE_ENABLED"] == "true";
var rabbitConfiguration = builder.Configuration.GetRabbitMqConfiguration();
var redisConnectionString =
isAspire ? builder.Configuration["ConnectionStrings:Redis"] : builder.Configuration["RedisHostName"];
var dbConnectionString = isAspire
? builder.Configuration["ConnectionStrings:Database"]
: builder.Configuration["ConnectionStrings:DefaultConnection"];
if (builder.Environment.IsProduction()) if (builder.Environment.IsProduction())
{ {
var rabbitHost = builder.Configuration["RabbitMQ:Host"];
var rabbitPort = builder.Configuration["RabbitMQ:Port"];
var rabbitUser = builder.Configuration["RabbitMQ:Username"];
var rabbitPass = builder.Configuration["RabbitMQ:Password"];
builder.Services builder.Services
.AddHealthChecks() .AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")!) .AddSqlServer(dbConnectionString!)
.AddRedis(builder.Configuration["RedisHostName"]!, "Redis") .AddRedis(redisConnectionString!, "Redis")
.AddRabbitMQ( .AddRabbitMQ(
$"amqp://{rabbitUser}:{rabbitPass}@{rabbitHost}:{rabbitPort}", rabbitConfiguration.ConnectionString,
name: "RabbitMQ"); name: "RabbitMQ");
} }
builder.Services.AddDbContext<ApplicationDbContext>(options => builder.Services.AddDbContext<ApplicationDbContext>(options =>
{ {
options.UseLazyLoadingProxies(); options.UseLazyLoadingProxies();
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), options.UseSqlServer(dbConnectionString,
b => b.MigrationsAssembly("CleanArchitecture.Infrastructure")); b => b.MigrationsAssembly("CleanArchitecture.Infrastructure"));
}); });
builder.Services.AddSwagger(); builder.Services.AddSwagger();
builder.Services.AddAuth(builder.Configuration); builder.Services.AddAuth(builder.Configuration);
builder.Services.AddInfrastructure(builder.Configuration, "CleanArchitecture.Infrastructure"); builder.Services.AddInfrastructure("CleanArchitecture.Infrastructure", dbConnectionString!);
builder.Services.AddQueryHandlers(); builder.Services.AddQueryHandlers();
builder.Services.AddServices(); builder.Services.AddServices();
builder.Services.AddSortProviders(); builder.Services.AddSortProviders();
@ -61,7 +67,7 @@ builder.Services.AddCommandHandlers();
builder.Services.AddNotificationHandlers(); builder.Services.AddNotificationHandlers();
builder.Services.AddApiUser(); builder.Services.AddApiUser();
builder.Services.AddRabbitMqHandler(builder.Configuration, "RabbitMQ"); builder.Services.AddRabbitMqHandler(rabbitConfiguration);
builder.Services.AddHostedService<SetInactiveUsersService>(); builder.Services.AddHostedService<SetInactiveUsersService>();
@ -73,11 +79,11 @@ builder.Services.AddLogging(x => x.AddSimpleConsole(console =>
console.IncludeScopes = true; console.IncludeScopes = true;
})); }));
if (builder.Environment.IsProduction() || !string.IsNullOrWhiteSpace(builder.Configuration["RedisHostName"])) if (builder.Environment.IsProduction() || !string.IsNullOrWhiteSpace(redisConnectionString))
{ {
builder.Services.AddStackExchangeRedisCache(options => builder.Services.AddStackExchangeRedisCache(options =>
{ {
options.Configuration = builder.Configuration["RedisHostName"]; options.Configuration = redisConnectionString;
options.InstanceName = "clean-architecture"; options.InstanceName = "clean-architecture";
}); });
} }
@ -88,6 +94,8 @@ else
var app = builder.Build(); var app = builder.Build();
app.MapDefaultEndpoints();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var services = scope.ServiceProvider; var services = scope.ServiceProvider;

View File

@ -1,13 +1,5 @@
{ {
"$schema": "https://json.schemastore.org/launchsettings.json", "$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:38452",
"sslPort": 44309
}
},
"profiles": { "profiles": {
"CleanArchitecture.Api": { "CleanArchitecture.Api": {
"commandName": "Project", "commandName": "Project",
@ -18,14 +10,6 @@
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
} }
} }
} }

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>e7ec3788-69e9-4631-b350-d59657ddd747</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
<PackageReference Include="Aspire.Hosting.RabbitMQ" Version="9.0.0" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.0.0" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Api\CleanArchitecture.Api.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("Redis").WithRedisInsight();
var rabbitPasswordRessource = new ParameterResource("password", _ => "guest");
var rabbitPasswordParameter =
builder.AddParameter("username", rabbitPasswordRessource.Value);
var rabbitMq = builder
.AddRabbitMQ("RabbitMq", null, rabbitPasswordParameter, 5672)
.WithManagementPlugin();
var sqlServer = builder.AddSqlServer("SqlServer");
var db = sqlServer.AddDatabase("Database", "clean-architecture");
builder.AddProject<Projects.CleanArchitecture_Api>("CleanArchitecture-Api")
.WithOtlpExporter()
.WithHttpHealthCheck("/health")
.WithReference(redis)
.WaitFor(redis)
.WithReference(rabbitMq)
.WaitFor(rabbitMq)
.WithReference(db)
.WaitFor(sqlServer);
builder.Build().Run();

View File

@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"Aspire": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17270;http://localhost:15188",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21200",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22111",
"ASPIRE_ENABLED": "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",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@ -1,4 +1,3 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Domain.Rabbitmq.Extensions; namespace CleanArchitecture.Domain.Rabbitmq.Extensions;
@ -7,12 +6,9 @@ public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddRabbitMqHandler( public static IServiceCollection AddRabbitMqHandler(
this IServiceCollection services, this IServiceCollection services,
IConfiguration configuration, RabbitMqConfiguration configuration)
string rabbitMqConfigSection)
{ {
var rabbitMq = new RabbitMqConfiguration(); services.AddSingleton(configuration);
configuration.Bind(rabbitMqConfigSection, rabbitMq);
services.AddSingleton(rabbitMq);
services.AddSingleton<RabbitMqHandler>(); services.AddSingleton<RabbitMqHandler>();
services.AddHostedService(serviceProvider => serviceProvider.GetService<RabbitMqHandler>()!); services.AddHostedService(serviceProvider => serviceProvider.GetService<RabbitMqHandler>()!);

View File

@ -7,4 +7,6 @@ public sealed class RabbitMqConfiguration
public bool Enabled { get; set; } public bool Enabled { get; set; }
public string Username { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
public string ConnectionString => $"amqp://{Username}:{Password}@{Host}:{Port}";
} }

View File

@ -13,7 +13,6 @@ namespace CleanArchitecture.Domain.Rabbitmq;
public sealed class RabbitMqHandler : BackgroundService public sealed class RabbitMqHandler : BackgroundService
{ {
private IChannel? _channel;
private readonly RabbitMqConfiguration _configuration; private readonly RabbitMqConfiguration _configuration;
private readonly ConcurrentDictionary<string, List<ConsumeEventHandler>> _consumers = new(); private readonly ConcurrentDictionary<string, List<ConsumeEventHandler>> _consumers = new();
@ -21,6 +20,7 @@ public sealed class RabbitMqHandler : BackgroundService
private readonly ILogger<RabbitMqHandler> _logger; private readonly ILogger<RabbitMqHandler> _logger;
private readonly ConcurrentQueue<IRabbitMqAction> _pendingActions = new(); private readonly ConcurrentQueue<IRabbitMqAction> _pendingActions = new();
private IChannel? _channel;
public RabbitMqHandler( public RabbitMqHandler(
RabbitMqConfiguration configuration, RabbitMqConfiguration configuration,
@ -38,17 +38,21 @@ public sealed class RabbitMqHandler : BackgroundService
return; return;
} }
_logger.LogInformation("Starting RabbitMQ connection");
var factory = new ConnectionFactory var factory = new ConnectionFactory
{ {
AutomaticRecoveryEnabled = true, AutomaticRecoveryEnabled = true,
HostName = _configuration.Host, HostName = _configuration.Host,
Port = _configuration.Port, Port = _configuration.Port,
UserName = _configuration.Username, UserName = _configuration.Username,
Password = _configuration.Password, Password = _configuration.Password
}; };
var connection = await factory.CreateConnectionAsync(cancellationToken); var connection = await factory.CreateConnectionAsync(cancellationToken);
_channel = await connection.CreateChannelAsync(null, cancellationToken); _channel = await connection.CreateChannelAsync(null, cancellationToken);
await base.StartAsync(cancellationToken);
} }
@ -129,14 +133,15 @@ public sealed class RabbitMqHandler : BackgroundService
AddExchangeConsumer(exchange, string.Empty, queue, consumer); AddExchangeConsumer(exchange, string.Empty, queue, consumer);
} }
private async Task AddEventConsumer(string exchange, string queueName, string routingKey, ConsumeEventHandler consumer) private async Task AddEventConsumer(string exchange, string queueName, string routingKey,
ConsumeEventHandler consumer)
{ {
if (!_configuration.Enabled) if (!_configuration.Enabled)
{ {
_logger.LogInformation("RabbitMQ is disabled. Event consumer will not be added."); _logger.LogInformation("RabbitMQ is disabled. Event consumer will not be added.");
return; return;
} }
var key = $"{exchange}-{routingKey}"; var key = $"{exchange}-{routingKey}";
if (!_consumers.TryGetValue(key, out var consumers)) if (!_consumers.TryGetValue(key, out var consumers))

View File

@ -7,7 +7,6 @@ using CleanArchitecture.Infrastructure.EventSourcing;
using CleanArchitecture.Infrastructure.Repositories; using CleanArchitecture.Infrastructure.Repositories;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Extensions; namespace CleanArchitecture.Infrastructure.Extensions;
@ -16,16 +15,15 @@ public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddInfrastructure( public static IServiceCollection AddInfrastructure(
this IServiceCollection services, this IServiceCollection services,
IConfiguration configuration,
string migrationsAssemblyName, string migrationsAssemblyName,
string connectionStringName = "DefaultConnection") string connectionString)
{ {
// Add event store db context // Add event store db context
services.AddDbContext<EventStoreDbContext>( services.AddDbContext<EventStoreDbContext>(
options => options =>
{ {
options.UseSqlServer( options.UseSqlServer(
configuration.GetConnectionString(connectionStringName), connectionString,
b => b.MigrationsAssembly(migrationsAssemblyName)); b => b.MigrationsAssembly(migrationsAssemblyName));
}); });
@ -33,7 +31,7 @@ public static class ServiceCollectionExtensions
options => options =>
{ {
options.UseSqlServer( options.UseSqlServer(
configuration.GetConnectionString(connectionStringName), connectionString,
b => b.MigrationsAssembly(migrationsAssemblyName)); b => b.MigrationsAssembly(migrationsAssemblyName));
}); });

View File

@ -14,7 +14,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.2.2" /> <PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.3.0"> <PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.IntegrationTests.Fixtures;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.ExternalServices;
public sealed class RedisTestFixture : TestFixtureBase
{
public Guid CreatedTenantId { get; } = Guid.NewGuid();
public IDistributedCache DistributedCache { get; }
public RedisTestFixture()
{
DistributedCache = Factory.Services.GetRequiredService<IDistributedCache>();
}
public async Task SeedTestData()
{
await GlobalSetupFixture.RespawnDatabaseAsync();
using var context = Factory.Services.GetRequiredService<ApplicationDbContext>();
context.Tenants.Add(new Tenant(
CreatedTenantId,
"Test Tenant"));
await context.SaveChangesAsync();
}
}

View File

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Domain;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.IntegrationTests.Extensions;
using FluentAssertions;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
namespace CleanArchitecture.IntegrationTests.ExternalServices;
public sealed class RedisTests
{
private readonly RedisTestFixture _fixture = new();
[OneTimeSetUp]
public async Task Setup() => await _fixture.SeedTestData();
[Test, Order(0)]
public async Task Should_Get_Tenant_By_Id_And_Ensure_Cache()
{
var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");
var message = await response.Content.ReadAsJsonAsync<TenantViewModel>();
message!.Data!.Id.Should().Be(_fixture.CreatedTenantId);
var json = await _fixture.DistributedCache.GetStringAsync(CacheKeyGenerator.GetEntityCacheKey<Tenant>(_fixture.CreatedTenantId));
json.Should().NotBeNullOrEmpty();
var tenant = JsonConvert.DeserializeObject<TenantViewModel>(json!)!;
tenant.Should().NotBeNull();
tenant.Id.Should().Be(_fixture.CreatedTenantId);
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.0.0-beta.12" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.9.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace CleanArchitecture.ServiceDefaults;
public static class Extensions
{
private const string AspireEnabled = "ASPIRE_ENABLED";
public static void AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
if (builder.Configuration[AspireEnabled] != "true")
{
return;
}
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
builder.Services.Configure<ServiceDiscoveryOptions>(options => { options.AllowedSchemes = ["https"]; });
}
private static void ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation()
.AddGrpcClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddHttpClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
}
private static void AddOpenTelemetryExporters<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
}
private static void AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
}
public static void MapDefaultEndpoints(this WebApplication app)
{
if (app.Configuration[AspireEnabled] != "true")
{
return;
}
if (app.Environment.IsDevelopment())
{
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
}
}

View File

@ -29,6 +29,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D3DF9DF5
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Shared", "CleanArchitecture.Shared\CleanArchitecture.Shared.csproj", "{E82B473D-0281-4713-9550-7D3FF7D9CFDE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Shared", "CleanArchitecture.Shared\CleanArchitecture.Shared.csproj", "{E82B473D-0281-4713-9550-7D3FF7D9CFDE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.AppHost", "CleanArchitecture.AppHost\CleanArchitecture.AppHost.csproj", "{AF8AC381-9A62-49A8-B42D-44BF8B0F28D0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.ServiceDefaults", "CleanArchitecture.ServiceDefaults\CleanArchitecture.ServiceDefaults.csproj", "{CED4C7AC-AD5C-4054-A338-95C32945D69E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{53D849CC-87DF-4A90-88C1-8380A8C07CB0}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -83,6 +89,14 @@ Global
{E82B473D-0281-4713-9550-7D3FF7D9CFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {E82B473D-0281-4713-9550-7D3FF7D9CFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E82B473D-0281-4713-9550-7D3FF7D9CFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {E82B473D-0281-4713-9550-7D3FF7D9CFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E82B473D-0281-4713-9550-7D3FF7D9CFDE}.Release|Any CPU.Build.0 = Release|Any CPU {E82B473D-0281-4713-9550-7D3FF7D9CFDE}.Release|Any CPU.Build.0 = Release|Any CPU
{AF8AC381-9A62-49A8-B42D-44BF8B0F28D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF8AC381-9A62-49A8-B42D-44BF8B0F28D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF8AC381-9A62-49A8-B42D-44BF8B0F28D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF8AC381-9A62-49A8-B42D-44BF8B0F28D0}.Release|Any CPU.Build.0 = Release|Any CPU
{CED4C7AC-AD5C-4054-A338-95C32945D69E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CED4C7AC-AD5C-4054-A338-95C32945D69E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CED4C7AC-AD5C-4054-A338-95C32945D69E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CED4C7AC-AD5C-4054-A338-95C32945D69E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -93,6 +107,8 @@ Global
{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} {EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
{39732BD4-909F-410C-8737-1F9FE3E269A7} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} {39732BD4-909F-410C-8737-1F9FE3E269A7} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} {E3A836DD-85DB-44FD-BC19-DDFE111D9EB0} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
{AF8AC381-9A62-49A8-B42D-44BF8B0F28D0} = {53D849CC-87DF-4A90-88C1-8380A8C07CB0}
{CED4C7AC-AD5C-4054-A338-95C32945D69E} = {53D849CC-87DF-4A90-88C1-8380A8C07CB0}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DDAAEEA0-FB1B-4EAD-902B-C12034FFC17A} SolutionGuid = {DDAAEEA0-FB1B-4EAD-902B-C12034FFC17A}

View File

@ -25,23 +25,29 @@ The project uses the following dependencies:
- **gRPC**: gRPC is an open-source remote procedure call framework that enables efficient communication between distributed systems using a variety of programming languages and protocols. - **gRPC**: gRPC is an open-source remote procedure call framework that enables efficient communication between distributed systems using a variety of programming languages and protocols.
## Running the Project ## Running the Project
To run the project, follow these steps:
To run the project, follow these steps:
1. Clone the repository to your local machine. 1. Clone the repository to your local machine.
2. Open the solution in your IDE of choice. 2. Open the solution in your IDE of choice.
3. Build the solution to restore the dependencies. 3. Build the solution to restore the dependencies.
4. Update the connection string in the appsettings.json file to point to your database. 4. Update the connection string in the appsettings.json file to point to your database.
5. Start the API project 5. Start the API project (Alterntively you can use the `dotnet run --project CleanArchitecture.Api` command)
6. The database migrations will be automatically applied on start-up. If the database does not exist, it will be created. 6. The database migrations will be automatically applied on start-up. If the database does not exist, it will be created.
7. The API should be accessible at `https://localhost:<port>/api/<controller>` where `<port>` is the port number specified in the project properties and `<controller>` is the name of the API controller. 7. The API should be accessible at `https://localhost:<port>/api/<controller>` where `<port>` is the port number specified in the project properties and `<controller>` is the name of the API controller.
### Using Aspire
1. Run `dotnet run --project CleanArchitecture.AppHost` in the root directory of the project.
### Using docker ### Using docker
Requirements Requirements
> This is only needed if running the API locally or only the docker image > This is only needed if running the API locally or only the docker image
1. Redis: `docker run --name redis -d -p 6379:6379 -e ALLOW_EMPTY_PASSWORD=yes redis:latest` 1. SqlServer: `docker run --name sqlserver -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD='Password123!#' mcr.microsoft.com/mssql/server`
2. Add this to the redis configuration in the Program.cs 1. RabbitMq: `docker run --name rabbitmq -d -p 5672:5672 -p 15672:15672 rabbitmq:4-management`
3. Redis: `docker run --name redis -d -p 6379:6379 -e ALLOW_EMPTY_PASSWORD=yes redis:latest`
4. Add this to the redis configuration in the Program.cs
```csharp ```csharp
options.ConfigurationOptions = new ConfigurationOptions options.ConfigurationOptions = new ConfigurationOptions
{ {
@ -49,11 +55,10 @@ options.ConfigurationOptions = new ConfigurationOptions
EndPoints = { "localhost", "6379" } EndPoints = { "localhost", "6379" }
}; };
``` ```
3. RabbitMq: `docker run --name rabbitmq -d -p 5672:5672 -p 15672:15672 rabbitmq:3-management`
Running the container Running the container
1. Build the Dockerfile: `docker build -t clean-architecture .` 1. Build the Dockerfile: `docker build -t clean-architecture .`
2. Run the Container: `docker run -p 80:80 clean-architecture` 2. Run the Container: `docker run --name clean-architecture -d -p 80:80 -p 8080:8080 clean-architecture`
### Using docker-compose ### Using docker-compose

View File

@ -1,4 +1,3 @@
version: "3"
services: services:
app: app:
build: build:
@ -37,7 +36,7 @@ services:
- 1433:1433 - 1433:1433
redis: redis:
image: docker.io/bitnami/redis:7.2 image: redis:latest
environment: environment:
# ALLOW_EMPTY_PASSWORD is recommended only for development. # ALLOW_EMPTY_PASSWORD is recommended only for development.
- ALLOW_EMPTY_PASSWORD=yes - ALLOW_EMPTY_PASSWORD=yes
@ -48,7 +47,7 @@ services:
- 'redis_data:/bitnami/redis/data' - 'redis_data:/bitnami/redis/data'
rabbitmq: rabbitmq:
image: "rabbitmq:3-management" image: "rabbitmq:4-management"
ports: ports:
- 5672:5672 - 5672:5672
- 15672:15672 - 15672:15672

View File

@ -6,9 +6,14 @@ spec:
selector: selector:
app: clean-architecture-app app: clean-architecture-app
ports: ports:
- protocol: TCP - name: http
protocol: TCP
port: 80 port: 80
targetPort: 80 targetPort: 80
- name: grpc
protocol: TCP
port: 8080
targetPort: 8080
type: LoadBalancer type: LoadBalancer
--- ---
@ -32,9 +37,12 @@ spec:
image: alexdev28/clean-architecture:latest image: alexdev28/clean-architecture:latest
ports: ports:
- containerPort: 80 - containerPort: 80
protocol: TCP
- containerPort: 8080
protocol: TCP
env: env:
- name: ASPNETCORE_HTTP_PORTS - name: ASPNETCORE_HTTP_PORTS
value: 80 value: "80"
- name: Kestrel__Endpoints__Http__Url - name: Kestrel__Endpoints__Http__Url
value: http://+:80 value: http://+:80
- name: Kestrel__Endpoints__Grpc__Url - name: Kestrel__Endpoints__Grpc__Url

View File

@ -32,7 +32,7 @@ spec:
spec: spec:
containers: containers:
- name: rabbitmq - name: rabbitmq
image: rabbitmq:management image: rabbitmq:4-management
ports: ports:
- containerPort: 5672 - containerPort: 5672
- containerPort: 15672 - containerPort: 15672

View File

@ -27,7 +27,7 @@ spec:
spec: spec:
containers: containers:
- name: redis - name: redis
image: docker.io/bitnami/redis:7.2 image: redis:latest
env: env:
# ALLOW_EMPTY_PASSWORD is recommended only for development. # ALLOW_EMPTY_PASSWORD is recommended only for development.
- name: ALLOW_EMPTY_PASSWORD - name: ALLOW_EMPTY_PASSWORD