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