Skip to content

Commit

Permalink
✨ Add .NET Aspire (#362) (#382)
Browse files Browse the repository at this point in the history
* Add Aspire

* Got aspire running

* Added Aspire service defaults

* Remove docker compose and up.ps1.

* Got integration tests working

* Remove unneeded code

* Remove incorrect connection string

* Fixed up connection string for tests

* Fix merge conflicts
  • Loading branch information
danielmackay authored Oct 22, 2024
1 parent 5a3243c commit 63e90fa
Show file tree
Hide file tree
Showing 17 changed files with 306 additions and 136 deletions.
9 changes: 7 additions & 2 deletions SSW.CleanArchitecture.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
.gitignore = .gitignore
Directory.Build.Props = Directory.Build.Props
docker-compose.yml = docker-compose.yml
global.json = global.json
README.md = README.md
up.ps1 = up.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A388D69B-2789-4D2E-A6D9-331D8571DA7E}"
Expand All @@ -39,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7AFFF91C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database", "tools\Database\Database.csproj", "{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "tools\AppHost\AppHost.csproj", "{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -77,6 +77,10 @@ Global
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD}.Release|Any CPU.Build.0 = Release|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -90,6 +94,7 @@ Global
{E631C496-2285-4B0E-8C04-EB9D0766D01A} = {A388D69B-2789-4D2E-A6D9-331D8571DA7E}
{6FAECF07-C024-45F6-B78A-A2771833F867} = {A388D69B-2789-4D2E-A6D9-331D8571DA7E}
{08A4FCE5-D49A-4FC1-BB0B-3206326157BD} = {7AFFF91C-498F-4CE9-9DCB-E2FDBA18E81A}
{874CCCC1-3F73-4DA9-BA2B-89EE6C7C4B8E} = {7AFFF91C-498F-4CE9-9DCB-E2FDBA18E81A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8975F724-46D1-4802-8C4B-6B386B69846F}
Expand Down
21 changes: 0 additions & 21 deletions docker-compose.yml

This file was deleted.

38 changes: 18 additions & 20 deletions src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SSW.CleanArchitecture.Application.Common.Interfaces;
using SSW.CleanArchitecture.Infrastructure.Persistence;
using SSW.CleanArchitecture.Infrastructure.Persistence.Interceptors;
Expand All @@ -9,27 +8,26 @@ namespace SSW.CleanArchitecture.Infrastructure;

public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
public static void AddInfrastructure(this IHostApplicationBuilder builder)
{
services.AddScoped<EntitySaveChangesInterceptor>();
services.AddScoped<DispatchDomainEventsInterceptor>();

services.AddDbContext<IApplicationDbContext, ApplicationDbContext>(options =>
{
options.AddInterceptors(
services.BuildServiceProvider().GetRequiredService<EntitySaveChangesInterceptor>(),
services.BuildServiceProvider().GetRequiredService<DispatchDomainEventsInterceptor>()
);

options.UseSqlServer(config.GetConnectionString("DefaultConnection"), builder =>
builder.AddSqlServerDbContext<ApplicationDbContext>("clean-architecture",
null,
options =>
{
builder.MigrationsAssembly(typeof(DependencyInjection).Assembly.FullName);
builder.EnableRetryOnFailure();
var serviceProvider = builder.Services.BuildServiceProvider();
options.AddInterceptors(
serviceProvider.GetRequiredService<EntitySaveChangesInterceptor>(),
serviceProvider.GetRequiredService<DispatchDomainEventsInterceptor>());

// TODO: Add this
// options.UseExceptionProcessor();
});
});

services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<IApplicationDbContext>(sp => sp.GetRequiredService<ApplicationDbContext>());

builder.Services.AddScoped<EntitySaveChangesInterceptor>();
builder.Services.AddScoped<DispatchDomainEventsInterceptor>();

return services;
builder.Services.AddSingleton(TimeProvider.System);
}
}
15 changes: 14 additions & 1 deletion src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@
<PropertyGroup>
<RootNamespace>SSW.CleanArchitecture.Infrastructure</RootNamespace>
<AssemblyName>SSW.CleanArchitecture.Infrastructure</AssemblyName>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-rc.2.24474.1" />
<FrameworkReference Include="Microsoft.AspNetCore.App"/>

<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-rc.1.24511.1" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.10.0"/>
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0-rc.1.24511.1"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0"/>
</ItemGroup>

</Project>
118 changes: 118 additions & 0 deletions src/Infrastructure/ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Hosting;

// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
public static class Extensions
{
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();

builder.AddDefaultHealthChecks();

builder.Services.AddServiceDiscovery();

builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();

// Turn on service discovery by default
http.AddServiceDiscovery();
});

// Uncomment the following to restrict the allowed schemes for service discovery.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });

return builder;
}

private static TBuilder 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.AddAspNetCoreInstrumentation()
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
});

builder.AddOpenTelemetryExporters();

return builder;
}

private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);

if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}

// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}

return builder;
}

private static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);

return builder;
}

public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");

// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
}

return app;
}
}
7 changes: 5 additions & 2 deletions src/WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
using SSW.CleanArchitecture.Application;
using SSW.CleanArchitecture.Infrastructure;
using SSW.CleanArchitecture.Infrastructure.Persistence;
using SSW.CleanArchitecture.WebApi;
using SSW.CleanArchitecture.WebApi.Features;
using SSW.CleanArchitecture.WebApi.Filters;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddWebApi(builder.Configuration);
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
builder.AddInfrastructure();

var app = builder.Build();

Expand Down Expand Up @@ -38,4 +39,6 @@
app.MapHeroEndpoints();
app.MapTeamEndpoints();

app.MapDefaultEndpoints();

app.Run();
45 changes: 23 additions & 22 deletions tests/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Azure.Identity" Version="1.13.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="JunitXml.TestLogger" Version="3.1.12" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="xunit" Version="2.7.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2" />
<PackageVersion Include="Bogus" Version="34.0.2" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="Respawn" Version="6.2.1" />
<PackageVersion Include="Testcontainers" Version="3.8.0" />
<PackageVersion Include="Testcontainers.SqlEdge" Version="3.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-rc.2.24474.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0-rc.2.24473.5" />
<PackageVersion Include="Meziantou.Extensions.Logging.Xunit" Version="1.0.7" />
</ItemGroup>
</Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Azure.Identity" Version="1.13.0"/>
<PackageVersion Include="FluentAssertions" Version="6.12.0"/>
<PackageVersion Include="JunitXml.TestLogger" Version="3.1.12"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageVersion Include="xunit" Version="2.7.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8"/>
<PackageVersion Include="coverlet.collector" Version="6.0.0"/>
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2"/>
<PackageVersion Include="Bogus" Version="34.0.2"/>
<PackageVersion Include="NSubstitute" Version="5.1.0"/>
<PackageVersion Include="Respawn" Version="6.2.1"/>
<PackageVersion Include="Testcontainers" Version="3.10.0"/>
<PackageVersion Include="Testcontainers.MsSql" Version="3.10.0"/>
<PackageVersion Include="Polly" Version="8.4.2"/>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-rc.2.24474.3"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0-rc.2.24473.5"/>
<PackageVersion Include="Meziantou.Extensions.Logging.Xunit" Version="1.0.7"/>
</ItemGroup>
</Project>
38 changes: 27 additions & 11 deletions tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
using Testcontainers.SqlEdge;
using Polly;
using Testcontainers.MsSql;

namespace WebApi.IntegrationTests.Common.Fixtures;

/// <summary>
/// Wraper for SQL edge container
/// </summary>
public class DatabaseContainer
public class DatabaseContainer : IAsyncDisposable
{
private readonly SqlEdgeContainer? _container = new SqlEdgeBuilder()
.WithName("CleanArchitecture-IntegrationTests-DbContainer")
.WithPassword("sqledge!Strong")
private readonly MsSqlContainer _container = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04")
.WithName($"CleanArchitecture-IntegrationTests-{Guid.NewGuid()}")
.WithPassword("Password123")
.WithPortBinding(1433, true)
.WithAutoRemove(true)
.Build();

private const int MaxRetries = 5;

public string? ConnectionString { get; private set; }

public async Task InitializeAsync()
{
if (_container != null)
{
await _container.StartAsync();
ConnectionString = _container.GetConnectionString();
}
await StartWithRetry();
ConnectionString = _container.GetConnectionString();
}

public Task DisposeAsync() => _container?.StopAsync() ?? Task.CompletedTask;
private async Task StartWithRetry()
{
// NOTE: For some reason the container sometimes fails to start up. Add in a retry to protect against this
var policy = Policy.Handle<InvalidOperationException>()
.WaitAndRetryAsync(MaxRetries, _ => TimeSpan.FromSeconds(5));

await policy.ExecuteAsync(async () => { await _container.StartAsync(); });
}

public async ValueTask DisposeAsync()
{
await _container.StopAsync();
await _container.DisposeAsync();
GC.SuppressFinalize(this);
}
}
Loading

0 comments on commit 63e90fa

Please sign in to comment.