.NET Aspire for local development
If you've spent the last few years context-switching between Docker Compose files, environment variable spreadsheets, and half-working local setups, .NET Aspire is going to feel like relief. .NET Aspire for local development lets you orchestrate APIs, databases and queues with C# instead of YAML—describing your entire system as code, getting observability out of the box, and running everything locally the way it'll behave in production. Over five years building multi-service platforms, I've watched teams waste months fighting flaky local environments; Aspire changes that equation.
The problem Aspire solves
Before we talk about the solution, let's be honest about what we're fixing. You've got a microservice architecture or a modern SaaS platform—maybe you've got an API, a background job service, PostgreSQL, Redis, and a message queue. Locally, you either:
- Run everything in Docker Compose and spend 20 minutes debugging why the API can't reach Redis because the network name is wrong
- Run some things locally and some in containers, creating environment mismatches that nobody catches until staging
- Give up and tell new developers "just use the production-like environment"—which kills local iteration speed
Every approach trades off simplicity, accuracy, or developer experience. Aspire doesn't ask you to choose.
The AppHost — Describe your services and dependencies in code
At the heart of Aspire is the AppHost—a .NET project that describes your entire system using fluent C# APIs. Instead of maintaining separate Docker Compose, Kubernetes manifests, and local shell scripts, you define resources and their relationships once.
Here's a realistic example from a ride-hailing platform I worked on:
var builder = DistributedApplication.CreateBuilder(args);
// Add PostgreSQL and create a database
var pg = builder
.AddPostgres("postgres")
.AddDatabase("ridedb");
// Add Redis
var cache = builder.AddRedis("redis");
// Add a message queue
var queue = builder
.AddRabbitMQ("rabbitmq")
.AddExchange("ride-events", exchange => exchange.AddBinding("ride.created", "ride.completed"));
// Add the APIs
var apiService = builder
.AddProject<Projects.RideApi>("ride-api")
.WithReference(pg)
.WithReference(cache)
.WithReference(queue)
.WithEnvironment("Logging__LogLevel__Default", "Information");
var notificationService = builder
.AddProject<Projects.NotificationService>("notification-service")
.WithReference(cache)
.WithReference(queue);
builder.Build().Run();Notice what just happened. We defined PostgreSQL, Redis, RabbitMQ, and two .NET services in 20 lines of readable code. We declared that the ride API depends on all three infrastructure components and that the notification service depends on the queue and cache. The AppHost generates connection strings, ports, and networking automatically—no manual configuration, no guessing.
When you run dotnet run in the AppHost project, it orchestrates everything. The Aspire tooling pulls images, starts containers, configures networking, and wires environment variables into your projects.
Compare that to a Docker Compose alternative:
// This is what you DON'T have to write or maintain
version: '3.8'
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ridedb
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
rabbitmq:
image: rabbitmq:3.12-management
ports:
- "5672:5672"
- "15672:15672"
ride-api:
build: ./RideApi
ports:
- "8080:8080"
environment:
ConnectionStrings__ridedb: "Host=postgres;Username=postgres;Password=postgres;Database=ridedb"
ConnectionStrings__redis: "redis:6379"
depends_on:
- postgres
- redis
- rabbitmqThe Docker Compose file is fragile: connection strings are strings, port conflicts happen, environment variable names drift from documentation. The C# version is type-safe, refactorable, and subject to the same code review and testing discipline as your application code.
Service defaults — Health checks, OpenTelemetry and resilience configured once
One of the biggest wins with Aspire is service defaults. You create a shared project that every service references, and you configure observability, resilience, and health checks once—then every service inherits those configurations automatically.
Here's what a service defaults configuration looks like:
// ServiceDefaults/Extensions.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace ServiceDefaults;
public static class Extensions
{
public static WebApplicationBuilder AddServiceDefaults(this WebApplicationBuilder builder)
{
// Add health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" });
// Add OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation();
})
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
});
// Add resilience
builder.Services.AddHttpClient()
.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
});
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;
}
}
Now every service that calls builder.AddServiceDefaults() gets health checks, tracing, metrics, and HTTP resilience. You're not repeating configuration across ten projects. You're not gambling that someone forgot to add OpenTelemetry instrumentation. It's declared once, inherited everywhere.
In the AppHost, you can even layer additional configuration:
var rideApi = builder
.AddProject<Projects.RideApi>("ride-api")
.WithReference(pg)
.WithEnvironment("OpenTelemetry__SamplingRatio", "1.0"); // Full tracing in local devThe dashboard — Logs, traces and metrics for every service locally
Run your AppHost and Aspire launches a dashboard. It's a web interface showing every service, their health status, and live logs. Click into a service and you see structured logs, traces, and metrics—the same observability you'd get from an enterprise APM tool, running on your laptop.
This changes local debugging. Instead of:
- Running your API in a terminal
- Opening another terminal to tail database logs
- SSH-ing into a container to check Redis
- Hunting through exception logs to find correlation IDs
You open the Aspire dashboard and see everything at once. Logs are structured and queryable. If a request times out, you can see the full distributed trace—where it spent time, which service was slow, where it failed.
I ran this locally while debugging a payment service that was timing out intermittently. In 90 seconds, the Aspire dashboard showed me that the API was hitting Redis, Redis was responding fine, but the database query was taking 8 seconds. Without the dashboard, I would have added logging and redeployed—costing 10 minutes per iteration.
Not a production host — Aspire shines locally and generates deployment manifests
Here's an important boundary: Aspire is a local development orchestrator, not a production host. The AppHost runs on your machine. It's not Kubernetes replacement.
But Aspire does generate deployment artifacts. You can export your AppHost configuration to:
- Azure Container Apps — Aspire generates Azure resource templates
- Docker Compose — For on-premise or legacy deployments
- Kubernetes manifests — If your team runs Kubernetes
This is powerful because your local setup and production setup are derived from the same source. You're not maintaining Docker Compose for local, Kubernetes YAML for staging, and CloudFormation for production—three separate and drifting definitions of the same system.
The export command looks like:
dotnet run --project AppHost -- --publisher azureOr for Docker Compose:
dotnet run --project AppHost -- --publisher composeAspire generates the manifests. You review them, adjust resource limits or environment-specific secrets, and deploy.
Practical workflow — Getting started
Start with the .NET Aspire templates. Create a new AppHost project:
dotnet new aspireThis scaffolds an AppHost solution with a sample API and dashboard. From there:
- Add your infrastructure resources using
builder.AddPostgres(),builder.AddRedis(), etc. - Add your .NET projects using
builder.AddProject<T>() - Declare dependencies with
.WithReference() - Run
dotnet runin the AppHost and let it orchestrate everything
Your team members do the same—they clone the repo, run the AppHost, and have a fully working local environment in under a minute. No Docker setup guide, no "it works on my machine" excuses.
Common mistakes
- Forgetting to reference the ServiceDefaults project — If a new service doesn't call
AddServiceDefaults(), it won't get observability or health checks. Use a template or code generator to prevent this. - Hardcoding connection strings — Don't. Aspire generates them. Use
IConfigurationor dependency injection to read them from the runtime environment. - Treating Aspire as a production orchestrator — It's not. Use it for local development and generating manifests, but your production deployment tooling (Azure, Kubernetes, etc.) is still your responsibility.
- Not running the dashboard — The whole point is observability. Open the dashboard. Use it. The insights pay for the setup cost.
- Expecting Aspire to replace your deployment pipeline — Aspire generates manifests, but secrets management, traffic routing, auto-scaling policies, and compliance checks are still your domain.
Key takeaways
- .NET Aspire replaces Docker Compose and manual configuration with type-safe C# code that describes your entire system.
- The AppHost orchestrates services and infrastructure locally, generating connection strings and networking automatically.
- Service defaults configure health checks, OpenTelemetry, and resilience once—every service inherits those configurations.
- The Aspire dashboard provides local observability: structured logs, traces, and metrics for every service, out of the box.
- Aspire is a development tool that generates deployment manifests for Azure, Docker Compose, or Kubernetes—your local setup and production deployment come from the same source.
- New team members get a working, production-like local environment in minutes with zero configuration.
- Avoid hardcoding connection strings, ensure every service references ServiceDefaults, and use the dashboard actively for debugging.






Comments
Loading comments…