Skip to content

Aspire — Cloud-Native

Key Points

  • .NET Aspire is Microsoft's cloud-native dev experience for distributed apps. Define resources (services, DBs, queues, caches) in AppHost project; run locally with aspire run.
  • AppHost is the orchestrator — declares the topology of your app. Uses standard .NET DI; not YAML.
  • Built-in dev dashboard — service graph, logs, traces, metrics. No setup. Live, hot-reload-friendly.
  • Service discovery built-in — services reference each other by logical name; AppHost wires endpoints.
  • Deployment: azd up for Azure (Container Apps); Bicep/manifest export for K8s. Aspire 9+ has rich deployment story.

Concepts (deep dive)

What Aspire is

.NET Aspire is a distributed-app development experience for .NET — not a runtime, not a service mesh, not a deployment target. It's the layer that sits between your services during development: it starts every part of your system (your .NET projects, a Postgres container, a Redis container, a RabbitMQ broker), wires them together so services find each other by name, threads OpenTelemetry through everything, and lights up a dashboard that shows logs, traces, and metrics across the entire topology — all from aspire run or F5.

                    ┌───────────────────────────────────────┐
                    │      AppHost project (C# code)        │
                    │  builder.AddRedis("cache")            │
                    │  builder.AddPostgres("db")…           │
                    │  builder.AddProject<Api>("api")       │
                    │       .WithReference(redis, db)       │
                    └───────────────┬───────────────────────┘
                                    │ aspire run / F5
                  ┌─────────────────┼─────────────────────┐
                  ▼                 ▼                     ▼
         ┌───────────────┐  ┌───────────────┐    ┌───────────────┐
         │ redis (docker)│  │ pg (docker)   │    │ api (.NET)    │
         │ + auto config │  │ + auto config │    │ wired up via  │
         │   for clients │  │   for clients │    │ service disco │
         └───────────────┘  └───────────────┘    └───────────────┘
                            ▲                            │
                            │     OpenTelemetry          │
                            │   ◄────────────────────────┘
                  ┌───────────────────────┐
                  │   Aspire dashboard    │
                  │ logs / traces /       │
                  │   metrics / resources │
                  └───────────────────────┘

Why Aspire exists: the "F5 to run the whole stack" experience for a distributed .NET app used to be a folder full of docker-compose.yml, hand-wired connection strings in five appsettings.Development.json files, and no unified view of what was happening across services. Aspire makes the topology code (typed C#, refactorable, debuggable), and the runtime view (the dashboard) is built in. It's a productivity layer; the deployed app still runs on Container Apps, AKS, or wherever you target.

AppHost as the topology

The AppHost project is a regular .NET console app whose Program.cs declares what the distributed system is made of. Resources (AddRedis, AddPostgres, AddProject<…>) become nodes in a graph; WithReference becomes edges. At runtime, Aspire turns the graph into actual processes and containers, and automatically injects connection strings and discovery URLs into the consuming projects' configuration. No appsettings rituals, no docker-compose.yml.

The same AppHost — without changes — also feeds the deployment story: azd up reads the graph and provisions equivalent Azure resources (Container Apps, Service Bus, Cosmos DB) wired together the same way.

AppHost project

// MyApp.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var redis = builder.AddRedis("cache");

var postgres = builder.AddPostgres("db")
    .WithDataVolume()
    .AddDatabase("orders");

var queue = builder.AddRabbitMQ("rabbit");

var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReference(queue);

var web = builder.AddProject<Projects.MyApp_Web>("web")
    .WithReference(api);

builder.Build().Run();

aspire run (or F5 in VS) starts all of it: containers for redis/postgres/rabbit, .NET projects for api/web, opens dashboard.

ServiceDefaults

Every Aspire-aware project references MyApp.ServiceDefaults — a shared library setting up: - OpenTelemetry (traces, metrics, logs). - Health checks. - Service discovery (AddServiceDiscovery). - HttpClient resilience.

public static class Extensions
{
    public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder)
        where TBuilder : IHostApplicationBuilder
    {
        builder.ConfigureOpenTelemetry();
        builder.AddDefaultHealthChecks();
        builder.Services.AddServiceDiscovery();
        builder.Services.ConfigureHttpClientDefaults(http => http
            .AddStandardResilienceHandler()
            .AddServiceDiscovery());
        return builder;
    }
}

Service discovery

Hardcoded base addresses (https://localhost:5001) are the root of "works in dev, breaks in prod" — they leak the topology into application code. Aspire's service-discovery scheme replaces the host with a logical name (https+http://api) that the runtime resolves to whatever endpoint AppHost actually wired up: a local port in dev, a K8s service in cluster, a Container Apps URL in Azure. The application code never changes between environments.

builder.Services.AddHttpClient<ApiClient>(c => c.BaseAddress = new Uri("https+http://api"));

https+http://api resolves to whatever AppHost wired up (local Docker port, K8s service, etc.). Same code in dev and prod.

Connection strings

// In API project
builder.AddNpgsqlDbContext<AppDb>("orders");        // Aspire integration
builder.AddRedisClient("cache");
builder.AddRabbitMQClient("rabbit");

Configuration injected from AppHost. No hardcoded connection strings.

Dev dashboard

The dashboard is what makes Aspire feel different from docker compose up. Every service registered in AppHost streams logs, OTel traces, and OTel metrics to the dashboard out of the box — no agent setup, no Jaeger, no Grafana. A failing request in the web tier is one click from the corresponding trace through the API and into the database. Same OTel pipeline lights up Application Insights / Jaeger / Honeycomb in production.

http://localhost:18888 (default)

Shows: - Resources: all running services with status, logs, env, endpoints. - Traces: distributed traces across services. - Logs: aggregated structured logs. - Metrics: counters, gauges per service.

Backed by OpenTelemetry — same data shipping to prod observability.

Aspire integrations (NuGet packages)

An integration is a paired set of NuGet packages: a hosting package the AppHost uses to declare the resource, and a client package the consuming project uses to consume it. The hosting package knows how to start the container (image, env vars, ports); the client package knows how to register the typed client in DI and pick up the auto-injected connection string. Adding a Postgres database to your topology is one line in AppHost and one line in the consumer.

Package Adds
Aspire.Hosting.Postgres Postgres container
Aspire.Hosting.SqlServer SQL Server container
Aspire.Hosting.Redis Redis container
Aspire.Hosting.Azure.ServiceBus Service Bus emulator + cloud
Aspire.Hosting.Azure.Storage Azurite
Aspire.Hosting.Azure.CosmosDB Cosmos emulator + cloud
Aspire.Hosting.RabbitMQ RabbitMQ
Aspire.Hosting.Kafka Kafka
Aspire.Hosting.Ollama Ollama for local LLMs

Each integration: container resource + auto-configured client in consuming projects.

AI integrations

var ollama = builder.AddOllama("ollama")
    .AddModel("llama3:latest");

api.WithReference(ollama);

Aspire bundles Ollama for local LLM dev. See Ollama & Aspire.

Resource customization

var redis = builder.AddRedis("cache")
    .WithImage("redis", "7-alpine")
    .WithDataVolume()
    .WithEnvironment("REDIS_PASSWORD", "secret")
    .WithLifetime(ContainerLifetime.Persistent);   // survives across runs

Persistent state in dev

.WithDataVolume()

Container data persists across aspire run cycles. Faster onboarding (DB schema not recreated each time).

Deployment

The AppHost graph is the bridge between dev and prod. azd reads it and produces an Azure deployment (Container Apps by default, with managed identities and the right RBAC for each resource); aspire publish emits Bicep or K8s manifests for review. The promise: the same code that runs your topology locally describes how it deploys.

azd up (Azure Container Apps)

azd init
azd up

azd reads AppHost; provisions Azure resources (Container Apps, Service Bus, Cosmos, etc.); deploys.

Bicep generation

aspire publish bicep

Outputs Bicep templates for the topology — review and apply manually.

Kubernetes

Aspire 9+ has aspire publish with Kubernetes target — emits Helm charts / manifests.

What Aspire isn't

  • Not a service mesh — uses .NET service discovery; orchestration is dev-time.
  • Not a runtime — production runs on Container Apps / K8s / VM.
  • Not Docker Compose replacement — but for .NET apps, often easier.

Aspire vs Docker Compose

Aspect Aspire Compose
Definition C# / typed YAML
Service discovery Built-in Manual
Dashboard Yes None
.NET integrations First-class Generic
Cross-language Limited Universal

For .NET-heavy apps, Aspire wins. For polyglot, Compose still useful.

Multiple environments

if (builder.Environment.IsDevelopment())
    builder.AddAzureCosmosDB("cosmos").RunAsEmulator();   // emulator
else
    builder.AddAzureCosmosDB("cosmos");                   // real Azure

Same code; environment-specific behavior.

Testing

public class IntegrationTests : IAsyncLifetime
{
    private DistributedApplication _app = null!;

    public async Task InitializeAsync()
    {
        var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.MyApp_AppHost>();
        _app = await builder.BuildAsync();
        await _app.StartAsync();
    }

    [Fact]
    public async Task GetUsers_returns_ok()
    {
        var http = _app.CreateHttpClient("api");
        var r = await http.GetAsync("/users");
        Assert.True(r.IsSuccessStatusCode);
    }

    public Task DisposeAsync() => _app.DisposeAsync().AsTask();
}

Real distributed integration tests with all dependencies.

Performance / cold start

Aspire dev experience is fast on warm caches. First run downloads images. Subsequent: ~5-10s startup for typical app.

When NOT Aspire

  • Single-project app — overkill.
  • Polyglot stack — limited non-.NET support.
  • Existing Helm/Compose-driven dev — disruption may not be worth.

Code: correct vs wrong

❌ Wrong: hardcoded connection strings

// API project
"ConnectionStrings": { "Default": "Host=localhost;Port=5432;..." }

✅ Correct: AppHost-injected

builder.AddNpgsqlDbContext<AppDb>("orders");
// connection string flows from AppHost

❌ Wrong: ad-hoc HttpClient base addresses

builder.Services.AddHttpClient<ApiClient>(c => c.BaseAddress = new Uri("https://localhost:5001"));

✅ Correct: service discovery

builder.Services.AddHttpClient<ApiClient>(c => c.BaseAddress = new Uri("https+http://api"));

Design patterns for this topic

Pattern 1 — "AppHost as topology"

  • Intent: declarative app composition.

Pattern 2 — "ServiceDefaults shared lib"

  • Intent: OTel + resilience + discovery in one line.

Pattern 3 — "Aspire integrations for resources"

  • Intent: typed; auto-wired clients.

Pattern 4 — "azd for Azure deploy"

  • Intent: AppHost → Container Apps in one command.

Pattern 5 — "Persistent volumes for dev DB"

  • Intent: fast iteration.

Pros & cons / trade-offs

Aspect Pros Cons
Aspire dev Fast loop; observability New tool to learn
azd deploy Quick to Azure Azure-only convenience
ServiceDefaults Consistent Opinionated
Compose Polyglot More YAML

When to use / when to avoid

  • Use for new multi-project .NET apps.
  • Use for distributed app dev experience.
  • Avoid for single-project apps.
  • Avoid if non-.NET dominates.

Interview Q&A

Q1. What's Aspire? .NET cloud-native dev experience. Declarative app topology in C# AppHost; runs locally with dashboard.

Q2. AppHost role? Defines services, dependencies, dev-time resources (containers). Wires service discovery and config.

Q3. ServiceDefaults? Shared lib applying OTel, resilience, discovery. Each project references it.

Q4. Service discovery in Aspire? https+http://api URL pattern. AppHost resolves to runtime endpoint.

Q5. Dev dashboard? http://localhost:18888 — resources, traces, metrics, logs across all services.

Q6. Aspire vs Compose? Aspire: typed C#; .NET-first; dashboard. Compose: YAML; polyglot; broader.

Q7. Deployment target? azd up to Container Apps. Aspire 9+ supports K8s manifest export.

Q8. AI integrations? Ollama, Azure OpenAI, etc. as Aspire resources.

Q9. Persistent dev volumes? .WithDataVolume() keeps DB state across runs.

Q10. Test integrations? DistributedApplicationTestingBuilder boots full topology in tests.

Q11. When NOT Aspire? Single-project apps; non-.NET dominant.

Q12. Aspire and OpenTelemetry? Dashboard backed by OTel. Same telemetry pipeline locally and in prod.


Gotchas / common mistakes

  • ⚠️ Hardcoded URLs — defeats discovery.
  • ⚠️ Missing ServiceDefaults reference — no OTel.
  • ⚠️ Production assumptions dev-only resources (in-memory DB).
  • ⚠️ Aspire as runtime — it's not.

Further reading