Aspire in .NET: Orchestrate, Run, and Deploy a Distributed System from One App Host
Model a web app, API, PostgreSQL, and Redis in C#, then run, observe, test, and publish the complete system from one AppHost.
What You Will Build
You will build a small distributed shop system with a web frontend, an API, PostgreSQL, and Redis. One AppHost describes the topology, starts every resource, injects connection details, and sends telemetry to one dashboard.
AspireShop.AppHost βββ postgres β catalog database βββ cache β Redis βββ api β references catalog + cache βββ web β references api One command: aspire run One dashboard: resources, logs, traces, metrics
Directory.Packages.props and should be checked before publishing.The Problem: Juggling Services by Hand
A distributed application may need several terminals, manually chosen ports, copied connection strings, container commands, and separate log windows. A new team member must reproduce all of that before writing useful code.
| Manual development | With Aspire |
|---|---|
| Start each process separately | Run the AppHost once |
| Copy URLs and connection strings | Use references and service discovery |
| Search several log windows | Use one dashboard |
| Wire telemetry repeatedly | Share ServiceDefaults |
Prerequisites
- .NET 10 SDK and an IDE.
- A current Aspire CLI installation.
- Docker Desktop, Podman, or another supported container runtime.
- Basic familiarity with ASP.NET Core multi-project solutions.
- An Azure subscription only for the optional cloud deployment discussion.
Install the Aspire CLI and Scaffold the Solution
Install the CLI using the method recommended for your operating system, then confirm the active version. The exact installation command can change, so verify it against the current Aspire installation page.
aspire --version
aspire new
# Or add Aspire to an existing repository:
aspire initThe generated solution normally contains an AppHost and a ServiceDefaults project. The AppHost declares resources. ServiceDefaults gives runnable services a consistent baseline for telemetry, health checks, discovery, and HTTP resilience.
Create the Four-Resource Solution
Keep the sample small enough to understand on one screen.
AspireShop/
βββ AspireShop.AppHost/
β βββ AppHost.cs
βββ AspireShop.ServiceDefaults/
βββ AspireShop.Api/
βββ AspireShop.Web/
βββ AspireShop.Tests/
βββ Directory.Packages.props
βββ AspireShop.slndotnet new sln -n AspireShop
dotnet new webapi -n AspireShop.Api -o AspireShop.Api
dotnet new web -n AspireShop.Web -o AspireShop.Web
dotnet new classlib -n AspireShop.ServiceDefaults -o AspireShop.ServiceDefaults
# Create the AppHost with the current Aspire template or aspire CLI.The AppHost project references the API and web projects. Its source generator then creates strongly typed entries under the Projects namespace.
Model the Topology in the AppHost
The AppHost is the code-first application model. It declares what runs and how resources relate.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithDataVolume();
var catalog = postgres.AddDatabase("catalog");
var cache = builder.AddRedis("cache");
var api = builder.AddProject<Projects.AspireShop_Api>("api")
.WithReference(catalog)
.WithReference(cache)
.WaitFor(postgres)
.WaitFor(cache)
.WithHttpHealthCheck("/health");
builder.AddProject<Projects.AspireShop_Web>("web")
.WithReference(api)
.WaitFor(api)
.WithExternalHttpEndpoints();
builder.Build().Run();WithReference supplies connection details or service-discovery information. WaitFor delays dependent startup until a resource is ready. They solve different problems, and a real app commonly uses both.
Run Everything with One Command
Start the AppHost from the solution directory.
aspire runThe CLI builds the AppHost, starts its project and container resources, prints endpoints, and opens the dashboard. Use the dashboard to inspect:
- resource state and dependencies;
- console and structured logs;
- distributed traces;
- metrics and health;
- environment variables and endpoints intended for development inspection.
Add Service Defaults and Service Discovery
Each runnable service should call the shared defaults during startup.
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddProblemDetails();
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https+http://api");
});
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();The logical name api comes from the AppHost resource name. Service discovery resolves it to the active endpoint instead of forcing the web project to know a localhost port.
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}Treat this method as a shared baseline, not a dumping ground for application-specific registrations.
Consume PostgreSQL and Redis from the API
Aspire integrations usually have two sides: a hosting integration in the AppHost and a client integration in the consuming service.
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddNpgsqlDbContext<CatalogDbContext>("catalog");
builder.AddRedisClient("cache");
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapGet("/catalog/{id:int}", async (
int id,
CatalogDbContext db,
IConnectionMultiplexer redis,
CancellationToken cancellationToken) =>
{
var cache = redis.GetDatabase();
var cacheKey = $"catalog:{id}";
var cached = await cache.StringGetAsync(cacheKey);
if (cached.HasValue)
return Results.Text(cached!);
var item = await db.Items.FindAsync([id], cancellationToken);
if (item is null)
return Results.NotFound();
var json = JsonSerializer.Serialize(item);
await cache.StringSetAsync(cacheKey, json, TimeSpan.FromMinutes(5));
return Results.Text(json, "application/json");
});
app.Run();The names catalog and cache must match the AppHost resources. Local connection strings are produced by the integrations; production bindings come from the selected deployment environment.
Use Health Checks to Control Startup
WaitFor is useful only when Aspire can determine readiness. Add health checks that represent actual service readiness.
builder.Services.AddHealthChecks()
.AddDbContextCheck<CatalogDbContext>("catalog-db");
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});Readiness means the service can handle its expected work. Liveness means the process is running. Do not mark a resource healthy only because a TCP port opened.
Two Ways to Author the AppHost
The project-based AppHost is the default for a conventional solution. Aspire also supports a single-file AppHost on current .NET and Aspire releases.
| Style | Best fit |
|---|---|
AppHost.cs project | Long-lived solutions, strong project references, IDE workflows |
Single-file apphost.cs | Small repositories, demos, scripts, quick orchestration |
#:sdk Aspire.AppHost.Sdk
#:project ../AspireShop.Api/AspireShop.Api.csproj
#:project ../AspireShop.Web/AspireShop.Web.csproj
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var api = builder.AddProject("api", "../AspireShop.Api/AspireShop.Api.csproj")
.WithReference(cache)
.WaitFor(cache);
builder.AddProject("web", "../AspireShop.Web/AspireShop.Web.csproj")
.WithReference(api)
.WaitFor(api);
builder.Build().Run();Use one style consistently inside a repository. The tutorial continues with the project-based AppHost because it makes the generated Projects.* types easy to explain.
Test the Whole System
Aspire.Hosting.Testing starts the AppHost and its resources for a closed-box integration test.
public sealed class AspireShopTests
{
[Fact]
public async Task Web_home_page_is_available()
{
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireShop_AppHost>();
await using var app = await builder.BuildAsync();
await app.StartAsync();
await app.ResourceNotifications
.WaitForResourceHealthyAsync("web")
.WaitAsync(TimeSpan.FromMinutes(2));
using var client = app.CreateHttpClient("web");
using var response = await client.GetAsync("/");
response.EnsureSuccessStatusCode();
}
}WebApplicationFactory<T> for isolated ASP.NET Core tests and reserve AppHost tests for important system paths.Read Cross-Service Traces in the Dashboard
ServiceDefaults exports OpenTelemetry data to the dashboard. When the web service calls the API and the API uses PostgreSQL, a trace can connect those operations into one request path.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddNpgsql())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation());Keep sensitive payload capture disabled by default. The dashboard is a development aid, but development data can still contain secrets or personal information.
Microsoft.Extensions.AI OpenTelemetry instrumentation, current Aspire dashboards can visualize generative-AI spans alongside normal distributed traces.Publish and Deploy from the App Model
Aspire deployment is pipeline-based. A deployment target contributes steps to the app model. The CLI then enters that pipeline.
# Emit deployment artifacts without applying them
aspire publish
# Resolve and apply the configured deployment target
aspire deployCurrent deployment options include target-specific workflows such as Docker-oriented output, Azure targets, and Kubernetes/Helm. The exact publisher packages and commands change faster than the core AppHost API, so verify the current matrix before publication.
Live with Aspireβs Release Cadence
Aspire ships frequently and follows a latest-release support policy. That is different from the multi-year support expectation of a .NET LTS runtime.
- Pin the CLI and package versions used by the repository.
- Run
aspire --versionin support reports. - Use
aspire updatewhere supported, then review breaking changes. - Upgrade in a branch and run AppHost integration tests.
- Keep version-specific commands out of long-lived prose when possible.
aspire --version
aspire update
dotnet testA dated verification banner is essential for this tutorial because code that was correct a few months ago may use an older command, package name, or deployment model.
Where Aspire Fitsβand Where It Does Not
| Situation | Guidance |
|---|---|
| Local API + DB + cache + worker | Excellent fit |
| Consistent telemetry and health defaults | Excellent fit |
| Whole-system integration test | Use Aspire.Hosting.Testing |
| Fast isolated web test | Use WebApplicationFactory |
| Production runtime | Use Kubernetes, ACA, or your platform |
| Stack that cannot be upgraded regularly | Aspire may be a poor fit |
Aspire removes local orchestration friction and creates a shared application model. It does not remove the need to understand the target production platform.
Production Readiness Checklist
- The AppHost contains topology and deployment intent, not business logic.
- Every runnable service calls
AddServiceDefaults(). - Resource names are stable because they become configuration and discovery contracts.
- Dependencies use
WithReferenceand readiness usesWaitFor. - Health checks represent real readiness.
- Local data volumes are intentional and documented.
- Sensitive telemetry is disabled unless explicitly needed.
- Whole-app tests start the AppHost once per suitable fixture or collection.
- CLI and integration packages are pinned and upgraded together.
- Production secrets, scaling, ingress, and networking are owned by the target platform.
Run the Complete Demonstration
- Start the four-resource app with
aspire run. - Open the dashboard and verify all resources are healthy.
- Call the web frontend and inspect the web-to-API-to-database trace.
- Stop Redis from the dashboard and observe health and logs.
- Restart Redis and verify recovery.
- Run the AppHost integration test.
- Run
aspire publishand inspect the generated target artifacts.