.NET Aspire: Building Cloud-Native Apps Made Simple
If you've spent any time building distributed applications in .NET, you know the pain. You wire up Docker Compose files, hand-roll health checks, configure service URLs through environment variables, and cross your fingers that everything talks to everything else when you hit F5. we've been there more times than I care to admit, and it's exactly the kind of friction that makes developers dread microservices.
.NET Aspire changes that equation entirely. After spending the last several months migrating two production systems to Aspire, we can honestly say it's the most impactful addition to the .NET ecosystem since minimal APIs. It doesn't try to replace Kubernetes or your CI/CD pipeline — instead, it focuses on the developer inner loop and makes the local development experience for distributed apps genuinely pleasant.
In this post, let's walk you through what Aspire actually is, how it orchestrates your services, and why the built-in dashboard alone is worth the migration effort. let's explore real patterns from production systems and the gotchas I wish someone had told me about upfront.
What Is .NET Aspire, Really?
At its core, Aspire is an opinionated stack for building observable, production-ready distributed applications. But that description undersells it. Think of Aspire as three things bundled together:
- An orchestrator — the AppHost project that wires your services, databases, and message brokers together
- A component library — pre-built integrations for Redis, PostgreSQL, RabbitMQ, Azure services, and more
- A dashboard — a real-time observability UI that shows logs, traces, and metrics across all your services
Here's what a typical AppHost looks like:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var postgres = builder.AddPostgres("db")
.AddDatabase("orders");
var messaging = builder.AddRabbitMQ("messaging");
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
.WithReference(postgres)
.WithReference(cache);
var orderApi = builder.AddProject<Projects.OrderApi>("order-api")
.WithReference(postgres)
.WithReference(messaging)
.WithReference(catalogApi);
builder.AddProject<Projects.WebFrontend>("web-frontend")
.WithExternalHttpEndpoints()
.WithReference(catalogApi)
.WithReference(orderApi);
builder.Build().Run();
That's it. No Docker Compose. No environment variable juggling. Aspire pulls the container images for Redis, PostgreSQL, and RabbitMQ automatically, assigns ports, injects connection strings, and sets up service discovery. When you run the AppHost, everything comes up together and you get a dashboard at https://localhost:15888 showing the health of every component.
Service Discovery That Just Works
In practice, service-to-service communication is where most distributed app headaches begin. You hardcode localhost:5001 during development, use environment variables in staging, and then fight with DNS in production. Aspire's service discovery eliminates this entirely.
When you add .WithReference(catalogApi) to the order API, Aspire automatically configures the HTTP client factory so you can call the catalog service by name:
// In OrderApi's Program.cs
builder.AddServiceDefaults();
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog-api");
});
// The CatalogClient just works — no URLs to configure
public class CatalogClient(HttpClient httpClient)
{
public async Task<Product?> GetProductAsync(int id)
{
return await httpClient.GetFromJsonAsync<Product>($"/api/products/{id}");
}
}
The https+http:// scheme is Aspire's convention. It tries HTTPS first, falls back to HTTP. In production, you swap this out for your actual service URLs through configuration — but during development, it just works.
One thing it's common to see teams get wrong: they try to use Aspire's service discovery in production without a proper backing implementation. For AKS deployments, you'll want to use the DNS-based service discovery or plug in your own IServiceDiscoveryProvider. Aspire's built-in resolution is designed for the development inner loop.
The Dashboard Changes Everything
It's no exaggeration to say the Aspire dashboard is the single feature that convinced a team to adopt it. Before Aspire, we had a Grafana stack for production observability but nothing comparable during development. We'd Console.WriteLine our way through debugging distributed issues.
The dashboard gives you:
- Structured logs from every service, filterable and searchable in real time
- Distributed traces that follow a request across service boundaries
- Metrics for HTTP requests, database calls, and custom counters
- Resource health showing which containers are running, restarting, or failed
Here's how you add custom metrics that show up in the dashboard:
// Register a custom meter
builder.Services.AddSingleton(sp =>
{
var meter = new Meter("OrderApi.Processing");
return meter;
});
// Use it in your service
public class OrderProcessor(Meter meter)
{
private readonly Counter<long> _ordersProcessed =
meter.CreateCounter<long>("orders.processed", "orders", "Total orders processed");
private readonly Histogram<double> _processingDuration =
meter.CreateHistogram<double>("orders.processing_duration", "ms");
public async Task ProcessOrderAsync(Order order)
{
var sw = Stopwatch.StartNew();
// ... processing logic
sw.Stop();
_ordersProcessed.Add(1, new KeyValuePair<string, object?>("status", "completed"));
_processingDuration.Record(sw.ElapsedMilliseconds);
}
}
Those metrics appear in the dashboard immediately. No Prometheus scraping, no Grafana config. During development, this tight feedback loop is invaluable.
Integrations: Redis, PostgreSQL, and RabbitMQ
Aspire's component library handles the boilerplate of connecting to infrastructure services. Each integration configures health checks, connection pooling, retry policies, and telemetry automatically.
Here's how you configure the service defaults and consume these integrations in your API projects:
// In your API project's Program.cs
var builder = WebApplication.CreateBuilder(args);
// Adds health checks, OpenTelemetry, service discovery
builder.AddServiceDefaults();
// Redis — configures IDistributedCache with resilience
builder.AddRedisDistributedCache("cache");
// PostgreSQL with Entity Framework
builder.AddNpgsqlDbContext<OrderDbContext>("orders", settings =>
{
settings.DisableRetry = false; // Enable Npgsql retry logic
});
// RabbitMQ — configures IConnection with health checks
builder.AddRabbitMQClient("messaging", configureConnectionFactory: factory =>
{
factory.ConsumerDispatchConcurrency = 4;
});
What's great about this approach is that each Add* method does the right thing by default. The Redis integration sets up connection multiplexing and retry policies. The PostgreSQL integration configures connection pooling and health checks. RabbitMQ gets automatic reconnection. You can override any of these defaults, but you rarely need to.
One pattern that's worked well for us is combining the Redis cache integration with output caching:
builder.AddRedisOutputCache("cache");
// Then in your endpoints
app.MapGet("/api/products", async (CatalogDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput(policy => policy
.Expire(TimeSpan.FromMinutes(5))
.Tag("products"));
Health Checks and Resilience
Aspire's AddServiceDefaults() extension method sets up health check endpoints that follow the ASP.NET Core health check conventions. Every integration automatically registers its own health check, so by the time your app starts, you have a comprehensive health picture:
// This is what AddServiceDefaults() sets up behind the scenes
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
// These are added automatically by each Aspire integration:
// .AddRedis("cache")
// .AddNpgSql("orders")
// .AddRabbitMQ("messaging")
;
// Two endpoints: liveness and readiness
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
For resilience, consider combining Aspire with the Microsoft.Extensions.Http.Resilience package. Here's a pattern that's served us well:
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog-api");
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromMilliseconds(500);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});
Deployment Considerations
Aspire orchestrates your local development, but it doesn't deploy your app. That's a deliberate design choice. For deployment, you have a few options:
- Azure Container Apps — Aspire has first-class support via
azd. Runazd initin your Aspire project and it generates the Bicep templates automatically. - Kubernetes/AKS — Use the Aspirate tool (
aspirate generate) to produce Helm charts from your AppHost configuration. - Docker Compose — For simpler deployments,
aspirate generate --output-format composegives you a docker-compose.yml.
Here's what the Azure deployment flow looks like:
# Initialize Azure Developer CLI with your Aspire project
azd init
# Provision infrastructure and deploy
azd up
# Subsequent deployments
azd deploy
The azd integration inspects your AppHost and provisions the right Azure resources — Container Apps for your services, Azure Cache for Redis, Azure Database for PostgreSQL, and Azure Service Bus if you're using messaging.
Key Takeaways
After running Aspire in production across two systems, here's my honest assessment:
- Start with the AppHost and dashboard. Even if you don't use any integrations, the orchestration and observability alone justify Aspire.
- Don't fight the conventions. Aspire is opinionated about connection strings, health checks, and telemetry. Embrace the patterns rather than working around them.
- Use service discovery for development, DNS for production. Aspire's service discovery is a development convenience, not a production service mesh.
- The integrations save real time. teams can save about two weeks of boilerplate configuration across our two migration projects.
- Plan your deployment strategy early. Aspire doesn't dictate deployment, so decide between ACA, AKS, or Compose before you get too deep.
If you're building anything with more than two services in .NET, Aspire should be your starting point. The inner-loop experience is transformative, and the path to production is well-lit. Give it a serious try — it's worth noting you'll find it hard to go back to Docker Compose files and manual service wiring.
Comments
Ajit Gangurde
Software Engineer II at Microsoft | 15+ years in .NET & Azure
Related Posts
Mar 14, 2026
Feb 28, 2026