Learnixo
Back to blog
AI Systemsintermediate

.NET Aspire — Orchestrating Services, Databases, and Observability Locally

How .NET Aspire simplifies local development in a Clean Architecture project: AppHost orchestration, automatic connection string injection, the Aspire Dashboard, ServiceDefaults, and what changes between local and production.

Asma Hafeez KhanMay 16, 20264 min read
Clean Architecture.NETAspireOrchestrationLocal DevelopmentObservability
Share:š•

What .NET Aspire Solves

Without Aspire, local development for a multi-service application looks like:

Step 1: Start SQL Server (docker run ...)
Step 2: Create the database (dotnet ef database update ...)
Step 3: Start Redis (docker run ...)
Step 4: Set connection strings in environment variables
Step 5: Start the API project
Step 6: Remember all ports, check all health checks manually

With Aspire, it is one command:

Bash
dotnet run --project src/SystemForge.AppHost

Aspire starts all resources, injects all connection strings, and opens the dashboard.


The 8-Project Solution: Aspire Projects

src/SystemForge.AppHost/        ← Orchestrator — declares all resources
src/SystemForge.ServiceDefaults/ ← Shared defaults: health checks, OTel, logging

AppHost/Program.cs

C#
// src/SystemForge.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

// Database: SQL Server with a named database
var sqlServer = builder.AddSqlServer("sql")
    .WithDataVolume()                           // persists data between runs
    .AddDatabase("SystemForge");

// Cache: Redis with the optional Redis Commander UI
var redis = builder.AddRedis("redis")
    .WithRedisCommander();                      // visual Redis browser at localhost:8001

// Message broker (optional)
// var rabbitmq = builder.AddRabbitMQ("messaging");

// The API project, referencing both resources
var api = builder.AddProject<Projects.SystemForge_Api>("api")
    .WithReference(sqlServer)   // injects ConnectionStrings:SystemForge
    .WithReference(redis)       // injects ConnectionStrings:redis
    // .WithReference(rabbitmq)
    .WithExternalHttpEndpoints();

builder.Build().Run();

How Connection Strings Are Injected

When .WithReference(sqlServer) is called, Aspire injects a connection string into the API project's environment variables automatically. The API project reads it with configuration.GetConnectionString("SystemForge") — no manual setup required.

C#
// Infrastructure/DependencyInjection.cs — reads the Aspire-injected connection string
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(
        configuration.GetConnectionString("SystemForge")!,
        sql => sql.EnableRetryOnFailure(3)));

services.AddStackExchangeRedisCache(options =>
    options.Configuration = configuration.GetConnectionString("redis"));

ServiceDefaults — Shared Configuration

C#
// ServiceDefaults/Extensions.cs
public static IHostApplicationBuilder AddServiceDefaults(
    this IHostApplicationBuilder builder)
{
    // OpenTelemetry: traces, metrics, logs → Aspire Dashboard
    builder.ConfigureOpenTelemetry();

    // Health checks for all registered resources
    builder.AddDefaultHealthChecks();

    // Service discovery (for multi-project solutions)
    builder.Services.AddServiceDiscovery();

    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        http.AddStandardResilienceHandler();   // Polly retry/circuit breaker
        http.AddServiceDiscovery();
    });

    return builder;
}

// Api/Program.cs — one line to enable all defaults
builder.AddServiceDefaults();

The Aspire Dashboard

After dotnet run --project src/SystemForge.AppHost, navigate to the Aspire Dashboard URL (printed to console):

āœ“ Resources panel:   all services (API, SQL Server, Redis) with start/stop controls
āœ“ Traces panel:      waterfall view of every HTTP request with child spans
āœ“ Metrics panel:     request rate, error rate, latency percentiles, custom metrics
āœ“ Logs panel:        structured logs with full correlation — click a trace to see its logs
āœ“ Structured errors: exceptions shown in context with the full request chain

Production issue I've seen (resolved by Aspire): In a microservices project without observability tooling, a slow response on one endpoint was traced — over 4 hours — to an N+1 query in a joined service. With Aspire's trace view, the same diagnosis takes under 2 minutes: open the trace, expand the child spans, see the 47 SQL queries.


Health Check Endpoints

ServiceDefaults adds standardized health check endpoints automatically:

C#
// ServiceDefaults/Extensions.cs (built-in)
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
    app.MapHealthChecks("/health");
    app.MapHealthChecks("/alive");
    return app;
}

// Api/Program.cs
app.MapDefaultEndpoints();
// GET /health  → checks all registered health checks (DB, Redis, etc.)
// GET /alive   → liveness probe — returns 200 if the app is running

Adding a Custom Resource to AppHost

C#
// If you add a background worker service:
var worker = builder.AddProject<Projects.SystemForge_Worker>("worker")
    .WithReference(sqlServer)
    .WithReference(redis);

// If you add a second API:
var adminApi = builder.AddProject<Projects.SystemForge_AdminApi>("admin-api")
    .WithReference(sqlServer)
    .WithReference(redis)
    .WithReference(api);   // service discovery: admin-api can call api

AppHost.csproj

XML
<!-- src/SystemForge.AppHost/SystemForge.AppHost.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <IsAspireHost>true</IsAspireHost>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.AppHost" Version="9.*" />
    <PackageReference Include="Aspire.Hosting.SqlServer" Version="9.*" />
    <PackageReference Include="Aspire.Hosting.Redis" Version="9.*" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\SystemForge.Api\SystemForge.Api.csproj" />
  </ItemGroup>
</Project>

What Changes Between Local and Production

Local (Aspire):                     Production:
  SQL Server:  Docker container       Azure SQL / RDS / managed DB
  Redis:       Docker container       Azure Cache for Redis / ElastiCache
  Connection:  Auto-injected by Aspire  Azure Key Vault / environment variables
  Observability: Aspire Dashboard     Azure Monitor / Datadog / Grafana
  Orchestration: AppHost process      Kubernetes / Azure Container Apps / ECS

The API code does not change. Connection strings come from configuration either way. Aspire just makes the local setup effortless.


PRO TIP — Aspire in CI

You do not run Aspire in CI. In CI, each test that needs a database spins up its own Testcontainers SQL Server instance. Aspire is a local development tool. The production and CI infrastructure use their own resource provisioning.


Key Takeaway

Aspire removes the "it works on my machine" friction from multi-service .NET projects. One command starts all resources, injects all connection strings, and opens a dashboard that shows you traces, logs, and metrics in real time. The observable local development experience Aspire provides is close enough to production observability that production bugs surface during development — where they are cheapest to fix.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.