Skip to content

Evaluate integrating .NET Aspire for Dev-time orchestration and observability #256

@nanotaboada

Description

@nanotaboada

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/health and /alive endpoints
  • 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 /health and /alive endpoints
  • 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 aspire

Phase 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.csproj

Phase 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.csproj

Phase 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();
EOF

Phase 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:15888

Phase 7: Update Documentation

# Update README.md with new development workflow
# Add section explaining Aspire vs Docker Compose usage

Acceptance Criteria

  • Aspire templates installed (dotnet new list aspire shows templates)
  • AppHost project created and added to solution
  • ServiceDefaults project created and referenced by Web API
  • Web API updated with AddServiceDefaults() and MapDefaultEndpoints()
  • Solution builds successfully (dotnet build passes)
  • AppHost launches and shows dashboard URL
  • Dashboard accessible and shows Web API service status
  • Traces visible in dashboard for API requests
  • Health endpoints work (/health and /alive respond)
  • Docker Compose still works for production scenarios
  • Existing tests pass (dotnet test passes)
  • 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

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codecontainersPull requests that update containers codeenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions