-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Description
Our current development workflow is containerized via Docker and Compose, but lacks a cohesive orchestration layer tailored for .NET services. As the architecture grows, managing service dependencies, observability (traces, metrics, logs), and local debugging becomes increasingly complex.
.NET Aspire offers a .NET-native orchestration and diagnostics platform for development-time, which could streamline the setup of service dependencies while providing built-in support for OpenTelemetry and diagnostics dashboards.
Current Development Workflow
Developer Machine
├── Docker Compose (compose.yaml)
│ └── Web API container
│ ├── SQLite database (file-based)
│ └── Serilog logging (file + console)
└── Manual observability
├── Swagger UI for API testing
├── Log files for debugging
└── No distributed tracing
Current Limitations
- No unified dashboard — must switch between Swagger, log files, and terminal
- No distributed tracing — difficult to trace requests end-to-end
- No metrics collection — no visibility into runtime performance
- Manual health checks — health endpoint exists but no centralized monitoring
- Limited local observability — Serilog logs are great but lack trace correlation
Aspire vs Docker Compose
| Aspect | Docker Compose (Current) | .NET Aspire |
|---|---|---|
| Purpose | Container orchestration | .NET dev-time orchestration |
| Observability | Manual setup required | Built-in dashboard, traces, metrics |
| Service Discovery | Manual configuration | Automatic endpoint resolution |
| Health Monitoring | External tools needed | Built-in dashboard |
| Production Ready | ✅ Yes | ❌ Dev-time only |
| Learning Curve | Low (already using) | Low-Medium |
Recommendation
Use both: Aspire for development, Docker Compose for production. They complement each other.
Proposed Solution
Add .NET Aspire orchestration for development-time benefits while keeping Docker Compose for production deployments.
Project Structure After Integration
Dotnet.Samples.AspNetCore.WebApi/
├── src/
│ └── Dotnet.Samples.AspNetCore.WebApi/ # Existing Web API (updated)
├── test/
│ └── Dotnet.Samples.AspNetCore.WebApi.Tests/
├── Dotnet.Samples.AspNetCore.WebApi.AppHost/ # NEW - Orchestration
│ ├── Dotnet.Samples.AspNetCore.WebApi.AppHost.csproj
│ └── Program.cs
├── Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults/ # NEW - Shared defaults
│ ├── Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults.csproj
│ └── Extensions.cs
├── compose.yaml # Keep for production
└── Dockerfile # Keep for production
What Each Project Does
| Project | Purpose |
|---|---|
| AppHost | Orchestrates services, launches dashboard, defines resources |
| ServiceDefaults | Shared OpenTelemetry, health checks, resilience, service discovery |
| Web API | Existing project, updated to use ServiceDefaults |
AppHost Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// Add existing Web API project
var api = builder.AddProject<Projects.Dotnet_Samples_AspNetCore_WebApi>("webapi")
.WithExternalHttpEndpoints();
// SQLite is file-based, no resource needed
// Dashboard launches automatically
builder.Build().Run();ServiceDefaults Extensions.cs
The ServiceDefaults project provides standardized configuration for:
- OpenTelemetry — traces, metrics, and structured logging
- Health Checks —
/healthand/aliveendpoints - Service Discovery — automatic endpoint resolution
- Resilience — retry, circuit breaker, timeout policies
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 =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
return app;
}
}Changes to Web API Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add this line immediately after CreateBuilder
builder.AddServiceDefaults();
// ... existing service registrations (unchanged) ...
var app = builder.Build();
// Add this line to map health check endpoints
app.MapDefaultEndpoints();
// ... existing middleware and endpoint mappings (unchanged) ...
app.Run();Benefits of This Approach
1. Unified Developer Dashboard
- Real-time view of all services and their status
- Structured logs with filtering and search
- Distributed traces visualization
- Runtime metrics and health checks
2. Built-in OpenTelemetry
- Automatic trace correlation across services
- Pre-configured instrumentation for ASP.NET Core and HTTP clients
- OTLP export to any OpenTelemetry-compatible backend
3. Standardized Health Checks
- Consistent
/healthand/aliveendpoints - Centralized monitoring in the dashboard
- Aligns with existing health check pattern in this project
4. Future-Proof Architecture
- Easy to add new services (databases, caches, message queues)
- Service discovery works automatically
- Ready for microservices evolution
5. Complements Existing Stack
- Serilog continues to work (Aspire adds OTEL on top)
- Docker Compose unchanged for production
- No impact to FluentValidation, AutoMapper, EF Core
Migration Strategy
All commands assume you're in the repository root directory.
Phase 1: Install Aspire Templates
# Install .NET Aspire project templates
dotnet new install Aspire.ProjectTemplates
# Verify templates are installed
dotnet new list aspirePhase 2: Create AppHost Project
# Create the AppHost project
dotnet new aspire-apphost -o Dotnet.Samples.AspNetCore.WebApi.AppHost
# Add to solution
dotnet sln Dotnet.Samples.AspNetCore.WebApi.sln add \
Dotnet.Samples.AspNetCore.WebApi.AppHost/Dotnet.Samples.AspNetCore.WebApi.AppHost.csproj
# Add reference to existing Web API project
dotnet add Dotnet.Samples.AspNetCore.WebApi.AppHost/Dotnet.Samples.AspNetCore.WebApi.AppHost.csproj \
reference src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csprojPhase 3: Create ServiceDefaults Project
# Create the ServiceDefaults project
dotnet new aspire-servicedefaults -o Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults
# Add to solution
dotnet sln Dotnet.Samples.AspNetCore.WebApi.sln add \
Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults/Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults.csproj
# Add reference from Web API to ServiceDefaults
dotnet add src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj \
reference Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults/Dotnet.Samples.AspNetCore.WebApi.ServiceDefaults.csprojPhase 4: Update AppHost Program.cs
# Replace the generated AppHost Program.cs content
cat > Dotnet.Samples.AspNetCore.WebApi.AppHost/Program.cs << 'EOF'
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Dotnet_Samples_AspNetCore_WebApi>("webapi")
.WithExternalHttpEndpoints();
builder.Build().Run();
EOFPhase 5: Update Web API Program.cs
Add builder.AddServiceDefaults() after CreateBuilder and app.MapDefaultEndpoints() before app.Run():
# The changes need to be made manually in Program.cs:
# 1. Add: builder.AddServiceDefaults(); (after WebApplication.CreateBuilder)
# 2. Add: app.MapDefaultEndpoints(); (before app.Run())Phase 6: Validate Integration
# Build the solution
dotnet build
# Run via AppHost (launches dashboard automatically)
dotnet run --project Dotnet.Samples.AspNetCore.WebApi.AppHost
# Dashboard URL will be displayed in terminal output
# Typically: http://localhost:15888Phase 7: Update Documentation
# Update README.md with new development workflow
# Add section explaining Aspire vs Docker Compose usageAcceptance Criteria
- Aspire templates installed (
dotnet new list aspireshows templates) - AppHost project created and added to solution
- ServiceDefaults project created and referenced by Web API
- Web API updated with
AddServiceDefaults()andMapDefaultEndpoints() - Solution builds successfully (
dotnet buildpasses) - AppHost launches and shows dashboard URL
- Dashboard accessible and shows Web API service status
- Traces visible in dashboard for API requests
- Health endpoints work (
/healthand/aliverespond) - Docker Compose still works for production scenarios
- Existing tests pass (
dotnet testpasses) - README updated with Aspire development workflow
Trade-offs
Pros ✅
- Unified observability — single dashboard for logs, traces, metrics
- Built-in OpenTelemetry — no manual OTEL configuration needed
- Standardized patterns — health checks, resilience, service discovery
- Low learning curve — templates generate most code
- Complements Serilog — adds tracing on top of existing logging
- Future-proof — easy to add services as project grows
- Great for learning — demonstrates modern .NET observability patterns
Cons ⚠️
- Dev-time only — Aspire orchestration doesn't run in production
- Requires Docker — Docker Desktop or Podman must be running
- Additional projects — two new projects in solution
- Rapid evolution — Aspire is actively developed, expect template changes
- Overhead for single service — benefits increase with more services
- Serilog overlap — some logging features duplicate
Mitigation
- Keep Docker Compose for production deployments
- Document when to use Aspire vs Docker Compose
- Pin Aspire package versions for stability
- Evaluate periodically if both Serilog and OTEL logging are needed
Compatibility Matrix
| Current Technology | Aspire Compatibility | Notes |
|---|---|---|
| .NET 8 (LTS) | ✅ Fully supported | Aspire 9.x works with .NET 8 |
| ASP.NET Core Web API | ✅ Fully supported | Primary use case |
| EF Core + SQLite | ✅ Works as-is | File-based DB needs no changes |
| Docker Compose | ✅ Coexists | Aspire for dev, Compose for prod |
| Serilog | ✅ Complements | Both can run together |
| FluentValidation | ✅ No impact | Unchanged |
| AutoMapper | ✅ No impact | Unchanged |
| xUnit + Moq | ✅ No impact | Tests unchanged |
NuGet Packages Added
AppHost Project
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\Dotnet.Samples.AspNetCore.WebApi\Dotnet.Samples.AspNetCore.WebApi.csproj" />
</ItemGroup>
</Project>ServiceDefaults Project
<ItemGroup>
<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.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.10.0" />
</ItemGroup>Future Enhancements
Once Aspire is integrated, consider these additions:
Add PostgreSQL (relates to issue #249)
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.AddDatabase("players");
var api = builder.AddProject<Projects.Dotnet_Samples_AspNetCore_WebApi>("webapi")
.WithReference(postgres)
.WithExternalHttpEndpoints();
builder.Build().Run();Add Redis Cache
var cache = builder.AddRedis("cache");
var api = builder.AddProject<Projects.Dotnet_Samples_AspNetCore_WebApi>("webapi")
.WithReference(cache)
.WithExternalHttpEndpoints();Export to External Backends
// Aspire can export to Jaeger, Zipkin, Azure Monitor, etc.
// Configure via environment variables or AppHost