.NET Aspire
Overview
.NET Aspire is an opinionated framework for building observable, production-ready, cloud-native distributed applications. It provides an app host project for orchestrating multi-service applications, service defaults for consistent configuration (OpenTelemetry, health checks, resilience), and pre-built components for common infrastructure like Redis, PostgreSQL, RabbitMQ, and Azure services. The Aspire dashboard provides real-time visibility into traces, logs, and metrics during local development.
Project Structure
MyApp/
MyApp.AppHost/ # Orchestration project (references all services)
MyApp.ServiceDefaults/ # Shared configuration (telemetry, health checks)
MyApp.Api/ # Web API project
MyApp.Worker/ # Background worker project
MyApp.Web/ # Frontend project
Getting Started
dotnet new aspire-starter -n MyApp
# Creates AppHost, ServiceDefaults, Api, and Web projects
App Host (Orchestration)
The AppHost project defines the distributed application topology: which services to run, what infrastructure they need, and how they connect.
// MyApp.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure resources
var cache = builder.AddRedis("cache")
.WithRedisCommander(); // Adds Redis Commander UI
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin() // Adds pgAdmin UI
.WithDataVolume(); // Persists data across restarts
var db = postgres.AddDatabase("appdb");
var rabbit = builder.AddRabbitMQ("messaging")
.WithManagementPlugin(); // Adds RabbitMQ management UI
// Application projects
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(db)
.WithReference(cache)
.WithReference(rabbit)
.WithExternalHttpEndpoints();
builder.AddProject<Projects.MyApp_Worker>("worker")
.WithReference(db)
.WithReference(rabbit);
builder.AddProject<Projects.MyApp_Web>("web")
.WithReference(api)
.WithExternalHttpEndpoints();
builder.Build().Run();
Service Defaults
Shared configuration that every service in the application uses.
// MyApp.ServiceDefaults/Extensions.cs
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
public static IHostApplicationBuilder ConfigureOpenTelemetry(
this IHostApplicationBuilder builder)
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
public static IHostApplicationBuilder AddDefaultHealthChecks(
this IHostApplicationBuilder builder)
{
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
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;
}
}
Service Project Configuration
// MyApp.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Aspire component for PostgreSQL with EF Core
builder.AddNpgsqlDbContext<AppDbContext>("appdb");
// Aspire component for Redis caching
builder.AddRedisDistributedCache("cache");
// Aspire component for RabbitMQ
builder.AddRabbitMQClient("messaging");
builder.Services.AddControllers();
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapControllers();
app.Run();
Built-in Components
| Component | NuGet Package | AddXxx Method |
|---|---|---|
| PostgreSQL (EF Core) | Aspire.Npgsql.EntityFrameworkCore | AddNpgsqlDbContext<T> |
| PostgreSQL (Npgsql) | Aspire.Npgsql | AddNpgsqlDataSource |
| Redis (Caching) | Aspire.StackExchange.Redis.DistributedCaching | AddRedisDistributedCache |
| Redis (Output Cache) | Aspire.StackExchange.Redis.OutputCaching | AddRedisOutputCache |
| RabbitMQ | Aspire.RabbitMQ.Client | AddRabbitMQClient |
| Azure Blob Storage | Aspire.Azure.Storage.Blobs | AddAzureBlobClient |
| Azure Service Bus | Aspire.Azure.Messaging.ServiceBus | AddAzureServiceBusClient |
| Azure Key Vault | Aspire.Azure.Security.KeyVault | AddAzureKeyVaultClient |
| MongoDB | Aspire.MongoDB.Driver | AddMongoDBClient |
| Seq (Logging) | Aspire.Seq | AddSeqEndpoint |
Using Aspire Components in Services
// PostgreSQL with Entity Framework Core
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb");
var app = builder.Build();
app.MapGet("/products", async (CatalogDbContext db) =>
await db.Products.ToListAsync());
app.MapPost("/products", async (Product product, CatalogDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/products/{product.Id}", product);
});
// Redis distributed caching
app.MapGet("/products/{id}", async (
int id, CatalogDbContext db, IDistributedCache cache) =>
{
var cacheKey = $"product:{id}";
var cached = await cache.GetStringAsync(cacheKey);
if (cached is not null)
return Results.Ok(JsonSerializer.Deserialize<Product>(cached));
var product = await db.Products.FindAsync(id);
if (product is null) return Results.NotFound();
await cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
return Results.Ok(product);
});
Service Discovery and HTTP Clients
// In the API project, call another service by name
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHttpClient<CatalogClient>(client =>
{
// "catalog-api" is resolved via service discovery from the AppHost
client.BaseAddress = new Uri("https+http://catalog-api");
});
var app = builder.Build();
app.MapGet("/orders/{id}/details", async (int id, CatalogClient catalog) =>
{
var products = await catalog.GetProductsForOrderAsync(id);
return Results.Ok(products);
});
public class CatalogClient(HttpClient httpClient)
{
public async Task<List<Product>> GetProductsForOrderAsync(int orderId) =>
await httpClient.GetFromJsonAsync<List<Product>>(
$"/api/orders/{orderId}/products") ?? [];
}
Custom Resources
// Add a container resource for services not built into Aspire
var elasticsearch = builder.AddContainer("elasticsearch", "elasticsearch", "8.12.0")
.WithHttpEndpoint(port: 9200, targetPort: 9200)
.WithEnvironment("discovery.type", "single-node")
.WithEnvironment("xpack.security.enabled", "false")
.WithVolume("es-data", "/usr/share/elasticsearch/data");
builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(elasticsearch);
Environment and Parameters
var builder = DistributedApplication.CreateBuilder(args);
// Parameters (prompted at deploy time or from config)
var adminPassword = builder.AddParameter("admin-password", secret: true);
var postgres = builder.AddPostgres("postgres", password: adminPassword);
// Connection strings from configuration
var apiKey = builder.AddParameter("api-key", secret: true);
builder.AddProject<Projects.MyApp_Api>("api")
.WithEnvironment("ApiKey", apiKey);
Deployment to Azure Container Apps
# Install the azd CLI
winget install microsoft.azd
# Initialize and deploy
azd init
azd up
// AppHost with Azure-specific resources
var builder = DistributedApplication.CreateBuilder(args);
var insights = builder.AddAzureApplicationInsights("insights");
var storage = builder.AddAzureStorage("storage");
var blobs = storage.AddBlobs("blobs");
builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(insights)
.WithReference(blobs);
Best Practices
- Use the AppHost project solely for orchestration (defining resources, references, and endpoints); never put business logic, controllers, or domain code in the AppHost.
- Call
builder.AddServiceDefaults()as the first line in every service project'sProgram.csto ensure consistent OpenTelemetry, health checks, resilience, and service discovery configuration. - Use
WithDataVolume()on database resources (PostgreSQL, Redis) during development to persist data across container restarts rather than re-seeding on every startup. - Prefer Aspire's
AddNpgsqlDbContext<T>("name")over manualDbContextconfiguration because it automatically wires connection strings from service discovery, configures health checks, and adds OpenTelemetry instrumentation. - Use
WithExternalHttpEndpoints()only on services that need external access (API gateways, frontend); internal services should only be reachable through service discovery. - Reference services by name (
https+http://catalog-api) inHttpClient.BaseAddressand let Aspire's service discovery resolve the actual endpoint, rather than hardcodinglocalhost:PORTURLs. - Use
builder.AddParameter("key", secret: true)for sensitive values (API keys, passwords) instead of hardcoding them; Aspire prompts for these duringazd upand stores them securely. - Add
WithManagementPlugin(),WithPgAdmin(), orWithRedisCommander()during development to get admin UIs for infrastructure resources; these are automatically excluded in production deployments. - Run
azd initandazd upfor deploying to Azure Container Apps rather than manually building Docker images and configuring infrastructure. - Open the Aspire Dashboard (automatically launched during
dotnet runon the AppHost) to inspect distributed traces, structured logs, and metrics across all services during local development.