diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj
index b6e744f..862a120 100644
--- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj
+++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj
@@ -32,6 +32,7 @@
+
diff --git a/CleanArchitecture.Api/Extensions/ConfigurationExtensions.cs b/CleanArchitecture.Api/Extensions/ConfigurationExtensions.cs
new file mode 100644
index 0000000..a539f68
--- /dev/null
+++ b/CleanArchitecture.Api/Extensions/ConfigurationExtensions.cs
@@ -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 ?? ""
+ };
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs
index 5fb7fad..37a2c17 100644
--- a/CleanArchitecture.Api/Program.cs
+++ b/CleanArchitecture.Api/Program.cs
@@ -6,18 +6,20 @@
using CleanArchitecture.Domain.Rabbitmq.Extensions;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions;
+using CleanArchitecture.ServiceDefaults;
using HealthChecks.ApplicationStatus.DependencyInjection;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults();
+
builder.Services.AddControllers();
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();
@@ -28,32 +30,36 @@
.AddDbContextCheck()
.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())
{
- 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
.AddHealthChecks()
- .AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")!)
- .AddRedis(builder.Configuration["RedisHostName"]!, "Redis")
+ .AddSqlServer(dbConnectionString!)
+ .AddRedis(redisConnectionString!, "Redis")
.AddRabbitMQ(
- $"amqp://{rabbitUser}:{rabbitPass}@{rabbitHost}:{rabbitPort}",
+ rabbitConfiguration.ConnectionString,
name: "RabbitMQ");
}
builder.Services.AddDbContext(options =>
{
options.UseLazyLoadingProxies();
- options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
+ options.UseSqlServer(dbConnectionString,
b => b.MigrationsAssembly("CleanArchitecture.Infrastructure"));
});
builder.Services.AddSwagger();
builder.Services.AddAuth(builder.Configuration);
-builder.Services.AddInfrastructure(builder.Configuration, "CleanArchitecture.Infrastructure");
+builder.Services.AddInfrastructure("CleanArchitecture.Infrastructure", dbConnectionString!);
builder.Services.AddQueryHandlers();
builder.Services.AddServices();
builder.Services.AddSortProviders();
@@ -61,7 +67,7 @@
builder.Services.AddNotificationHandlers();
builder.Services.AddApiUser();
-builder.Services.AddRabbitMqHandler(builder.Configuration, "RabbitMQ");
+builder.Services.AddRabbitMqHandler(rabbitConfiguration);
builder.Services.AddHostedService();
@@ -73,11 +79,11 @@
console.IncludeScopes = true;
}));
-if (builder.Environment.IsProduction() || !string.IsNullOrWhiteSpace(builder.Configuration["RedisHostName"]))
+if (builder.Environment.IsProduction() || !string.IsNullOrWhiteSpace(redisConnectionString))
{
builder.Services.AddStackExchangeRedisCache(options =>
{
- options.Configuration = builder.Configuration["RedisHostName"];
+ options.Configuration = redisConnectionString;
options.InstanceName = "clean-architecture";
});
}
@@ -88,6 +94,8 @@
var app = builder.Build();
+app.MapDefaultEndpoints();
+
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
diff --git a/CleanArchitecture.Api/Properties/launchSettings.json b/CleanArchitecture.Api/Properties/launchSettings.json
index 7a96097..5b148db 100644
--- a/CleanArchitecture.Api/Properties/launchSettings.json
+++ b/CleanArchitecture.Api/Properties/launchSettings.json
@@ -1,13 +1,5 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:38452",
- "sslPort": 44309
- }
- },
"profiles": {
"CleanArchitecture.Api": {
"commandName": "Project",
@@ -18,14 +10,6 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
- },
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "launchUrl": "swagger",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
}
}
}
diff --git a/CleanArchitecture.AppHost/CleanArchitecture.AppHost.csproj b/CleanArchitecture.AppHost/CleanArchitecture.AppHost.csproj
new file mode 100644
index 0000000..74a66dc
--- /dev/null
+++ b/CleanArchitecture.AppHost/CleanArchitecture.AppHost.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ true
+ e7ec3788-69e9-4631-b350-d59657ddd747
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CleanArchitecture.AppHost/Program.cs b/CleanArchitecture.AppHost/Program.cs
new file mode 100644
index 0000000..81aba7e
--- /dev/null
+++ b/CleanArchitecture.AppHost/Program.cs
@@ -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("CleanArchitecture-Api")
+ .WithOtlpExporter()
+ .WithHttpHealthCheck("/health")
+ .WithReference(redis)
+ .WaitFor(redis)
+ .WithReference(rabbitMq)
+ .WaitFor(rabbitMq)
+ .WithReference(db)
+ .WaitFor(sqlServer);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/CleanArchitecture.AppHost/Properties/launchSettings.json b/CleanArchitecture.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000..030b6d4
--- /dev/null
+++ b/CleanArchitecture.AppHost/Properties/launchSettings.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/CleanArchitecture.AppHost/appsettings.Development.json b/CleanArchitecture.AppHost/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/CleanArchitecture.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CleanArchitecture.AppHost/appsettings.json b/CleanArchitecture.AppHost/appsettings.json
new file mode 100644
index 0000000..31c092a
--- /dev/null
+++ b/CleanArchitecture.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/CleanArchitecture.Domain/Rabbitmq/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Domain/Rabbitmq/Extensions/ServiceCollectionExtensions.cs
index 42ba1ca..a785525 100644
--- a/CleanArchitecture.Domain/Rabbitmq/Extensions/ServiceCollectionExtensions.cs
+++ b/CleanArchitecture.Domain/Rabbitmq/Extensions/ServiceCollectionExtensions.cs
@@ -1,4 +1,3 @@
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Domain.Rabbitmq.Extensions;
@@ -7,12 +6,9 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRabbitMqHandler(
this IServiceCollection services,
- IConfiguration configuration,
- string rabbitMqConfigSection)
+ RabbitMqConfiguration configuration)
{
- var rabbitMq = new RabbitMqConfiguration();
- configuration.Bind(rabbitMqConfigSection, rabbitMq);
- services.AddSingleton(rabbitMq);
+ services.AddSingleton(configuration);
services.AddSingleton();
services.AddHostedService(serviceProvider => serviceProvider.GetService()!);
diff --git a/CleanArchitecture.Domain/Rabbitmq/RabbitMqConfiguration.cs b/CleanArchitecture.Domain/Rabbitmq/RabbitMqConfiguration.cs
index e293b6f..b5771e4 100644
--- a/CleanArchitecture.Domain/Rabbitmq/RabbitMqConfiguration.cs
+++ b/CleanArchitecture.Domain/Rabbitmq/RabbitMqConfiguration.cs
@@ -7,4 +7,6 @@ public sealed class RabbitMqConfiguration
public bool Enabled { get; set; }
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
+
+ public string ConnectionString => $"amqp://{Username}:{Password}@{Host}:{Port}";
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs b/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs
index 4fcb983..1d70e99 100644
--- a/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs
+++ b/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs
@@ -13,7 +13,6 @@ namespace CleanArchitecture.Domain.Rabbitmq;
public sealed class RabbitMqHandler : BackgroundService
{
- private IChannel? _channel;
private readonly RabbitMqConfiguration _configuration;
private readonly ConcurrentDictionary> _consumers = new();
@@ -21,6 +20,7 @@ public sealed class RabbitMqHandler : BackgroundService
private readonly ILogger _logger;
private readonly ConcurrentQueue _pendingActions = new();
+ private IChannel? _channel;
public RabbitMqHandler(
RabbitMqConfiguration configuration,
@@ -38,17 +38,21 @@ public override async Task StartAsync(CancellationToken cancellationToken)
return;
}
+ _logger.LogInformation("Starting RabbitMQ connection");
+
var factory = new ConnectionFactory
{
AutomaticRecoveryEnabled = true,
HostName = _configuration.Host,
Port = _configuration.Port,
UserName = _configuration.Username,
- Password = _configuration.Password,
+ Password = _configuration.Password
};
var connection = await factory.CreateConnectionAsync(cancellationToken);
_channel = await connection.CreateChannelAsync(null, cancellationToken);
+
+ await base.StartAsync(cancellationToken);
}
@@ -129,14 +133,15 @@ public void AddExchangeConsumer(string exchange, string queue, ConsumeEventHandl
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)
{
_logger.LogInformation("RabbitMQ is disabled. Event consumer will not be added.");
return;
}
-
+
var key = $"{exchange}-{routingKey}";
if (!_consumers.TryGetValue(key, out var consumers))
diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs
index ad8ebac..994058f 100644
--- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs
+++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs
@@ -7,7 +7,6 @@
using CleanArchitecture.Infrastructure.Repositories;
using MediatR;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Extensions;
@@ -16,16 +15,15 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
- IConfiguration configuration,
string migrationsAssemblyName,
- string connectionStringName = "DefaultConnection")
+ string connectionString)
{
// Add event store db context
services.AddDbContext(
options =>
{
options.UseSqlServer(
- configuration.GetConnectionString(connectionStringName),
+ connectionString,
b => b.MigrationsAssembly(migrationsAssemblyName));
});
@@ -33,7 +31,7 @@ public static IServiceCollection AddInfrastructure(
options =>
{
options.UseSqlServer(
- configuration.GetConnectionString(connectionStringName),
+ connectionString,
b => b.MigrationsAssembly(migrationsAssemblyName));
});
diff --git a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
index 4cca339..7d82a63 100644
--- a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
+++ b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
@@ -14,7 +14,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/CleanArchitecture.IntegrationTests/ExternalServices/RedisTestFixture.cs b/CleanArchitecture.IntegrationTests/ExternalServices/RedisTestFixture.cs
new file mode 100644
index 0000000..17b429c
--- /dev/null
+++ b/CleanArchitecture.IntegrationTests/ExternalServices/RedisTestFixture.cs
@@ -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();
+ }
+
+ public async Task SeedTestData()
+ {
+ await GlobalSetupFixture.RespawnDatabaseAsync();
+
+ using var context = Factory.Services.GetRequiredService();
+
+ context.Tenants.Add(new Tenant(
+ CreatedTenantId,
+ "Test Tenant"));
+
+ await context.SaveChangesAsync();
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/ExternalServices/RedisTests.cs b/CleanArchitecture.IntegrationTests/ExternalServices/RedisTests.cs
new file mode 100644
index 0000000..8ead306
--- /dev/null
+++ b/CleanArchitecture.IntegrationTests/ExternalServices/RedisTests.cs
@@ -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();
+ message!.Data!.Id.Should().Be(_fixture.CreatedTenantId);
+
+ var json = await _fixture.DistributedCache.GetStringAsync(CacheKeyGenerator.GetEntityCacheKey(_fixture.CreatedTenantId));
+ json.Should().NotBeNullOrEmpty();
+
+ var tenant = JsonConvert.DeserializeObject(json!)!;
+
+ tenant.Should().NotBeNull();
+ tenant.Id.Should().Be(_fixture.CreatedTenantId);
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.ServiceDefaults/CleanArchitecture.ServiceDefaults.csproj b/CleanArchitecture.ServiceDefaults/CleanArchitecture.ServiceDefaults.csproj
new file mode 100644
index 0000000..f0eaa55
--- /dev/null
+++ b/CleanArchitecture.ServiceDefaults/CleanArchitecture.ServiceDefaults.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CleanArchitecture.ServiceDefaults/Extensions.cs b/CleanArchitecture.ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..9d1b110
--- /dev/null
+++ b/CleanArchitecture.ServiceDefaults/Extensions.cs
@@ -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(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(options => { options.AllowedSchemes = ["https"]; });
+ }
+
+ private static void ConfigureOpenTelemetry(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(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(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")
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.sln b/CleanArchitecture.sln
index 3e8b93c..2d516fe 100644
--- a/CleanArchitecture.sln
+++ b/CleanArchitecture.sln
@@ -29,6 +29,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D3DF9DF5
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Shared", "CleanArchitecture.Shared\CleanArchitecture.Shared.csproj", "{E82B473D-0281-4713-9550-7D3FF7D9CFDE}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -93,6 +107,8 @@ Global
{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2} = {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}
+ {AF8AC381-9A62-49A8-B42D-44BF8B0F28D0} = {53D849CC-87DF-4A90-88C1-8380A8C07CB0}
+ {CED4C7AC-AD5C-4054-A338-95C32945D69E} = {53D849CC-87DF-4A90-88C1-8380A8C07CB0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DDAAEEA0-FB1B-4EAD-902B-C12034FFC17A}
diff --git a/Readme.md b/Readme.md
index 357aa6a..0ae2fc3 100644
--- a/Readme.md
+++ b/Readme.md
@@ -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.
## 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.
2. Open the solution in your IDE of choice.
3. Build the solution to restore the dependencies.
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.
7. The API should be accessible at `https://localhost:/api/` where `` is the port number specified in the project properties and `` 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
Requirements
> 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`
-2. Add this to the redis configuration in the Program.cs
+1. SqlServer: `docker run --name sqlserver -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD='Password123!#' mcr.microsoft.com/mssql/server`
+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
options.ConfigurationOptions = new ConfigurationOptions
{
@@ -49,11 +55,10 @@ options.ConfigurationOptions = new ConfigurationOptions
EndPoints = { "localhost", "6379" }
};
```
-3. RabbitMq: `docker run --name rabbitmq -d -p 5672:5672 -p 15672:15672 rabbitmq:3-management`
Running the container
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
diff --git a/docker-compose.yml b/docker-compose.yml
index e0c871b..b4c1a8a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,3 @@
-version: "3"
services:
app:
build:
@@ -37,7 +36,7 @@ services:
- 1433:1433
redis:
- image: docker.io/bitnami/redis:7.2
+ image: redis:latest
environment:
# ALLOW_EMPTY_PASSWORD is recommended only for development.
- ALLOW_EMPTY_PASSWORD=yes
@@ -48,7 +47,7 @@ services:
- 'redis_data:/bitnami/redis/data'
rabbitmq:
- image: "rabbitmq:3-management"
+ image: "rabbitmq:4-management"
ports:
- 5672:5672
- 15672:15672
diff --git a/k8s-deployments/clean-architecture.yml b/k8s-deployments/clean-architecture.yml
index 92a8910..c65773e 100644
--- a/k8s-deployments/clean-architecture.yml
+++ b/k8s-deployments/clean-architecture.yml
@@ -6,9 +6,14 @@ spec:
selector:
app: clean-architecture-app
ports:
- - protocol: TCP
+ - name: http
+ protocol: TCP
port: 80
targetPort: 80
+ - name: grpc
+ protocol: TCP
+ port: 8080
+ targetPort: 8080
type: LoadBalancer
---
@@ -32,9 +37,12 @@ spec:
image: alexdev28/clean-architecture:latest
ports:
- containerPort: 80
+ protocol: TCP
+ - containerPort: 8080
+ protocol: TCP
env:
- name: ASPNETCORE_HTTP_PORTS
- value: 80
+ value: "80"
- name: Kestrel__Endpoints__Http__Url
value: http://+:80
- name: Kestrel__Endpoints__Grpc__Url
diff --git a/k8s-deployments/rabbitmq.yml b/k8s-deployments/rabbitmq.yml
index bfef599..bb52301 100644
--- a/k8s-deployments/rabbitmq.yml
+++ b/k8s-deployments/rabbitmq.yml
@@ -32,7 +32,7 @@ spec:
spec:
containers:
- name: rabbitmq
- image: rabbitmq:management
+ image: rabbitmq:4-management
ports:
- containerPort: 5672
- containerPort: 15672
diff --git a/k8s-deployments/redis.yml b/k8s-deployments/redis.yml
index 4055e86..4824019 100644
--- a/k8s-deployments/redis.yml
+++ b/k8s-deployments/redis.yml
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: redis
- image: docker.io/bitnami/redis:7.2
+ image: redis:latest
env:
# ALLOW_EMPTY_PASSWORD is recommended only for development.
- name: ALLOW_EMPTY_PASSWORD