πŸš€ Stop Juggling Terminals

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
Verified stack. This tutorial targets Aspire 13.x with a C# AppHost on .NET 10. Aspire changes quickly, so package versions belong in 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 developmentWith Aspire
Start each process separatelyRun the AppHost once
Copy URLs and connection stringsUse references and service discovery
Search several log windowsUse one dashboard
Wire telemetry repeatedlyShare ServiceDefaults
Aspire is not a production runtime. It coordinates local development and drives deployment pipelines. Kubernetes, Azure Container Apps, or another target platform still runs the production system.

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.

bash
aspire --version
aspire new
# Or add Aspire to an existing repository:
aspire init

The 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.

Pin before you teach. Aspire supports its current release line. Record the tested CLI and package versions in source control before publishing the tutorial.

Create the Four-Resource Solution

Keep the sample small enough to understand on one screen.

text
AspireShop/
β”œβ”€β”€ AspireShop.AppHost/
β”‚   └── AppHost.cs
β”œβ”€β”€ AspireShop.ServiceDefaults/
β”œβ”€β”€ AspireShop.Api/
β”œβ”€β”€ AspireShop.Web/
β”œβ”€β”€ AspireShop.Tests/
β”œβ”€β”€ Directory.Packages.props
└── AspireShop.sln
bash
dotnet 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.

csharp
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.

Do not use fixed local ports as service contracts. Let Aspire allocate endpoints and reference the logical resource name.

Run Everything with One Command

Start the AppHost from the solution directory.

bash
aspire run

The 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.
The first payoff: PostgreSQL, Redis, the API, and the web app are now running from one command without manually copying connection strings.

Add Service Defaults and Service Discovery

Each runnable service should call the shared defaults during startup.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

StyleBest fit
AppHost.cs projectLong-lived solutions, strong project references, IDE workflows
Single-file apphost.csSmall repositories, demos, scripts, quick orchestration
csharp
#: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.

csharp
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();
    }
}
This is not a unit-speed test. It may start real containers and several processes. Use 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.

csharp
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.

GenAI connection. If one service uses 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.

bash
# Emit deployment artifacts without applying them
aspire publish

# Resolve and apply the configured deployment target
aspire deploy

Current 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.

Do not deploy the AppHost as the orchestrator. Aspire evaluates the model and produces or applies platform-specific output. The selected platform runs the application.

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 --version in support reports.
  • Use aspire update where 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.
bash
aspire --version
aspire update
dotnet test

A 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

SituationGuidance
Local API + DB + cache + workerExcellent fit
Consistent telemetry and health defaultsExcellent fit
Whole-system integration testUse Aspire.Hosting.Testing
Fast isolated web testUse WebApplicationFactory
Production runtimeUse Kubernetes, ACA, or your platform
Stack that cannot be upgraded regularlyAspire 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 WithReference and readiness uses WaitFor.
  • 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

  1. Start the four-resource app with aspire run.
  2. Open the dashboard and verify all resources are healthy.
  3. Call the web frontend and inspect the web-to-API-to-database trace.
  4. Stop Redis from the dashboard and observe health and logs.
  5. Restart Redis and verify recovery.
  6. Run the AppHost integration test.
  7. Run aspire publish and inspect the generated target artifacts.
The central payoff: one app model drives local startup, wiring, observability, testing, and deployment generation without making the AppHost the production runtime.

What to Build Next

Frequently Asked Questions

Is Aspire the same as Kubernetes?

No. Aspire models and orchestrates the application for development and deployment workflows. Kubernetes is a production orchestration platform.

Must every service target .NET 10?

The current C# AppHost requires the current .NET SDK specified by Aspire documentation, but Aspire can orchestrate services written in other languages and services targeting supported earlier .NET versions.

Does WithReference wait for a dependency?

No. WithReference supplies connection or discovery information. Use WaitFor when startup should be delayed until the dependency is ready.

Should every test start the whole AppHost?

No. Keep most tests isolated and fast. Use AppHost tests for the system paths where real dependencies and service wiring matter.

Primary References

  1. Aspire documentation
  2. What is the Aspire AppHost?
  3. Aspire CLI overview
  4. Aspire testing overview
  5. Aspire deployment overview
  6. Aspire support policy
  7. Aspire repository
Back to Tutorials