.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.
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 manuallyWith Aspire, it is one command:
dotnet run --project src/SystemForge.AppHostAspire 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, loggingAppHost/Program.cs
// 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.
// 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
// 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 chainProduction 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:
// 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 runningAdding a Custom Resource to AppHost
// 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 apiAppHost.csproj
<!-- 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 / ECSThe 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.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.