build: add solution, central package management and Aspire AppHost

AppHost orchestrates SQL Server (persistent volume), migrations worker, Api and Web and registers the Docker Compose environment. ServiceDefaults expose /health and /alive in all environments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JannisandClaude Opus 5.5 committed 2026-10-09 18:25:10 +02:00
1 parent 9c35a3c5b7
commit 6e92523afd
16 files changed
+457

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{json,yml,yaml,props,targets,csproj,slnx,xml,webmanifest}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.cs]
# Sprache
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
csharp_prefer_primary_constructors = true:suggestion
csharp_using_directive_placement = outside_namespace:suggestion
dotnet_sort_system_directives_first = true
csharp_new_line_before_open_brace = all
# Benennung
dotnet_naming_rule.private_fields_underscore.symbols = private_fields
dotnet_naming_rule.private_fields_underscore.style = underscore_camel
dotnet_naming_rule.private_fields_underscore.severity = suggestion
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.underscore_camel.capitalization = camel_case
dotnet_naming_style.underscore_camel.required_prefix = _
# Generierter Code
[**/Migrations/*.cs]
generated_code = true
+9
View File
@@ -0,0 +1,9 @@
* text=auto eol=lf
*.sh text eol=lf
*.ps1 text eol=crlf
*.cmd text eol=crlf
*.png binary
*.ico binary
*.jpg binary
+29
View File
@@ -0,0 +1,29 @@
## .NET
bin/
obj/
artifacts/
*.user
*.suo
*.userprefs
.vs/
.vscode/*
!.vscode/extensions.json
.idea/
*.swp
TestResults/
coverage*/
*.trx
## Secrets / lokale Konfiguration
.env
deploy/.env
*.pfx
appsettings.*.local.json
## Aspire
.aspire/
aspire-output/
## OS
.DS_Store
Thumbs.db
+12
View File
@@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<SatelliteResourceLanguages>de;en</SatelliteResourceLanguages>
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>
</Project>
+59
View File
@@ -0,0 +1,59 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<AspireVersion>13.6.1</AspireVersion>
<AspNetCoreVersion>10.0.12</AspNetCoreVersion>
<EfCoreVersion>10.0.12</EfCoreVersion>
<OpenTelemetryVersion>1.19.1</OpenTelemetryVersion>
</PropertyGroup>
<ItemGroup Label="Aspire">
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Docker" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="$(AspireVersion)" />
</ItemGroup>
<ItemGroup Label="ASP.NET Core / EF Core">
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EfCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(EfCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EfCoreVersion)" />
</ItemGroup>
<ItemGroup Label="Service defaults">
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.10.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.10.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="10.10.0" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.19.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.19.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.19.0" />
</ItemGroup>
<ItemGroup Label="Libraries">
<PackageVersion Include="MudBlazor" Version="9.11.0" />
<PackageVersion Include="FluentValidation" Version="12.1.1" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageVersion Include="Ical.Net" Version="5.2.3" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
</ItemGroup>
<ItemGroup Label="Tests">
<PackageVersion Include="xunit.v3" Version="4.0.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
<PackageVersion Include="bunit" Version="2.11.3" />
<PackageVersion Include="Testcontainers.MsSql" Version="4.16.0" />
</ItemGroup>
</Project>
+18
View File
@@ -0,0 +1,18 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/FamilyPlanner.Api/FamilyPlanner.Api.csproj" />
<Project Path="src/FamilyPlanner.AppHost/FamilyPlanner.AppHost.csproj" />
<Project Path="src/FamilyPlanner.Domain/FamilyPlanner.Domain.csproj" />
<Project Path="src/FamilyPlanner.Infrastructure/FamilyPlanner.Infrastructure.csproj" />
<Project Path="src/FamilyPlanner.MigrationService/FamilyPlanner.MigrationService.csproj" />
<Project Path="src/FamilyPlanner.ServiceDefaults/FamilyPlanner.ServiceDefaults.csproj" />
<Project Path="src/FamilyPlanner.Shared/FamilyPlanner.Shared.csproj" />
<Project Path="src/FamilyPlanner.Web.Client/FamilyPlanner.Web.Client.csproj" />
<Project Path="src/FamilyPlanner.Web/FamilyPlanner.Web.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/FamilyPlanner.IntegrationTests/FamilyPlanner.IntegrationTests.csproj" />
<Project Path="tests/FamilyPlanner.UnitTests/FamilyPlanner.UnitTests.csproj" />
<Project Path="tests/FamilyPlanner.Web.Tests/FamilyPlanner.Web.Tests.csproj" />
</Folder>
</Solution>
+13
View File
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.12",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
+48
View File
@@ -0,0 +1,48 @@
var builder = DistributedApplication.CreateBuilder(args);
// `aspire publish` erzeugt daraus eine docker-compose.yaml (siehe docs/ARCHITECTURE.md §9).
builder.AddDockerComposeEnvironment("compose");
// Integrationstests setzen FamilyPlanner:EphemeralDatabase=true, damit die Entwicklungsdaten unberührt bleiben.
var ephemeralDatabase = bool.TryParse(builder.Configuration["FamilyPlanner:EphemeralDatabase"], out var ephemeral) && ephemeral;
var sql = builder.AddSqlServer("sql");
if (!ephemeralDatabase)
{
sql.WithDataVolume("familyplanner-sql-data")
.WithLifetime(ContainerLifetime.Persistent);
}
var database = sql.AddDatabase("familyplannerdb");
// Initial-Admin (nur beim allerersten Start relevant, Passwortwechsel wird erzwungen).
var adminUserName = builder.AddParameter("seed-admin-username", value: "admin", publishValueAsDefault: true);
var adminPassword = builder.AddParameter("seed-admin-password", secret: true);
var adminEmail = builder.AddParameter("seed-admin-email", value: "", publishValueAsDefault: true);
var migrations = builder.AddProject<Projects.FamilyPlanner_MigrationService>("migrations")
.WithReference(database)
.WaitFor(database)
.WithEnvironment("Seed__AdminUserName", adminUserName)
.WithEnvironment("Seed__AdminPassword", adminPassword)
.WithEnvironment("Seed__AdminEmail", adminEmail);
var api = builder.AddProject<Projects.FamilyPlanner_Api>("api")
.WithReference(database)
.WaitFor(database)
.WaitForCompletion(migrations)
.WithHttpHealthCheck("/health");
if (ephemeralDatabase)
{
// Tests melden sich mehrfach an – Rate Limit großzügiger.
api.WithEnvironment("RateLimiting__LoginPermitLimit", "100");
}
builder.AddProject<Projects.FamilyPlanner_Web>("web")
.WithExternalHttpEndpoints()
.WithReference(api)
.WaitFor(api)
.WithHttpHealthCheck("/health");
builder.Build().Run();
@@ -0,0 +1,22 @@
<Project Sdk="Aspire.AppHost.Sdk/13.6.1">
<PropertyGroup>
<OutputType>Exe</OutputType>
<UserSecretsId>28bccc18-4f99-42dc-a9f2-5cb77c780430</UserSecretsId>
<!-- DCP und Dashboard kommen aus den NuGet-Paketen; so laufen `dotnet run` und Aspire.Hosting.Testing
auch ohne installierte Aspire-CLI (z. B. in der CI). -->
<NoWarn>$(NoWarn);ASPIRE010</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.SqlServer" />
<PackageReference Include="Aspire.Hosting.Docker" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FamilyPlanner.MigrationService\FamilyPlanner.MigrationService.csproj" />
<ProjectReference Include="..\FamilyPlanner.Api\FamilyPlanner.Api.csproj" />
<ProjectReference Include="..\FamilyPlanner.Web\FamilyPlanner.Web.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17001;http://localhost:15234",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21215",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22044"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15234",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19252",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20180"
}
}
}
}
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Parameters": {
"seed-admin-password": "Admin123!ChangeMe"
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
@@ -0,0 +1,5 @@
{
"appHost": {
"path": "FamilyPlanner.AppHost.csproj"
}
}
@@ -0,0 +1,124 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
// Adds common 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/aspire/service-defaults
public static class Extensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
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;
}
public 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.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation(tracing =>
// Exclude health check requests from tracing
tracing.Filter = context =>
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
)
// 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;
}
public 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)
{
// Die Endpunkte werden auch in Produktion gemappt (Docker-Health-Checks). Sie liefern nur den
// aggregierten Status (Healthy/Unhealthy) ohne Details und sind daher unkritisch.
// Readiness: alle Health Checks (inkl. Datenbank) müssen erfolgreich sein.
app.MapHealthChecks(HealthEndpointPath).AllowAnonymous().DisableRateLimiting();
// Liveness: nur Checks mit dem Tag "live".
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
}).AllowAnonymous().DisableRateLimiting();
return app;
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
</Project>