Skip to content
ExponaBLOG
.NET & ASP.NET CORE · DAY 078 / 100

.NET Aspire for local development: a step-by-step guide

Orchestrate APIs, databases and queues with C# instead of YAML.

Expona Admin7 min read

.NET Aspire for local development

If you've spent the last few years context-switching between Docker Compose files, environment variable spreadsheets, and half-working local setups, .NET Aspire is going to feel like relief. .NET Aspire for local development lets you orchestrate APIs, databases and queues with C# instead of YAML—describing your entire system as code, getting observability out of the box, and running everything locally the way it'll behave in production. Over five years building multi-service platforms, I've watched teams waste months fighting flaky local environments; Aspire changes that equation.

The problem Aspire solves

Before we talk about the solution, let's be honest about what we're fixing. You've got a microservice architecture or a modern SaaS platform—maybe you've got an API, a background job service, PostgreSQL, Redis, and a message queue. Locally, you either:

  • Run everything in Docker Compose and spend 20 minutes debugging why the API can't reach Redis because the network name is wrong
  • Run some things locally and some in containers, creating environment mismatches that nobody catches until staging
  • Give up and tell new developers "just use the production-like environment"—which kills local iteration speed

Every approach trades off simplicity, accuracy, or developer experience. Aspire doesn't ask you to choose.

The AppHost — Describe your services and dependencies in code

At the heart of Aspire is the AppHost—a .NET project that describes your entire system using fluent C# APIs. Instead of maintaining separate Docker Compose, Kubernetes manifests, and local shell scripts, you define resources and their relationships once.

Here's a realistic example from a ride-hailing platform I worked on:

csharp
var builder = DistributedApplication.CreateBuilder(args);

// Add PostgreSQL and create a database
var pg = builder
    .AddPostgres("postgres")
    .AddDatabase("ridedb");

// Add Redis
var cache = builder.AddRedis("redis");

// Add a message queue
var queue = builder
    .AddRabbitMQ("rabbitmq")
    .AddExchange("ride-events", exchange => exchange.AddBinding("ride.created", "ride.completed"));

// Add the APIs
var apiService = builder
    .AddProject<Projects.RideApi>("ride-api")
    .WithReference(pg)
    .WithReference(cache)
    .WithReference(queue)
    .WithEnvironment("Logging__LogLevel__Default", "Information");

var notificationService = builder
    .AddProject<Projects.NotificationService>("notification-service")
    .WithReference(cache)
    .WithReference(queue);

builder.Build().Run();

Notice what just happened. We defined PostgreSQL, Redis, RabbitMQ, and two .NET services in 20 lines of readable code. We declared that the ride API depends on all three infrastructure components and that the notification service depends on the queue and cache. The AppHost generates connection strings, ports, and networking automatically—no manual configuration, no guessing.

When you run dotnet run in the AppHost project, it orchestrates everything. The Aspire tooling pulls images, starts containers, configures networking, and wires environment variables into your projects.

Compare that to a Docker Compose alternative:

csharp
// This is what you DON'T have to write or maintain
version: '3.8'
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: ridedb
    ports:
      - "5432:5432"
  redis:
    image: redis:7
    ports:
      - "6379:6379"
  rabbitmq:
    image: rabbitmq:3.12-management
    ports:
      - "5672:5672"
      - "15672:15672"
  ride-api:
    build: ./RideApi
    ports:
      - "8080:8080"
    environment:
      ConnectionStrings__ridedb: "Host=postgres;Username=postgres;Password=postgres;Database=ridedb"
      ConnectionStrings__redis: "redis:6379"
    depends_on:
      - postgres
      - redis
      - rabbitmq

The Docker Compose file is fragile: connection strings are strings, port conflicts happen, environment variable names drift from documentation. The C# version is type-safe, refactorable, and subject to the same code review and testing discipline as your application code.

Service defaults — Health checks, OpenTelemetry and resilience configured once

One of the biggest wins with Aspire is service defaults. You create a shared project that every service references, and you configure observability, resilience, and health checks once—then every service inherits those configurations automatically.

Here's what a service defaults configuration looks like:

csharp
// ServiceDefaults/Extensions.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace ServiceDefaults;

public static class Extensions
{
    public static WebApplicationBuilder AddServiceDefaults(this WebApplicationBuilder builder)
    {
        // Add health checks
        builder.Services.AddHealthChecks()
            .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" });

        // Add OpenTelemetry
        builder.Services.AddOpenTelemetry()
            .WithTracing(tracing =>
            {
                tracing
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddSqlClientInstrumentation();
            })
            .WithMetrics(metrics =>
            {
                metrics
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();
            });

        // Add resilience
        builder.Services.AddHttpClient()
            .ConfigureHttpClientDefaults(http =>
            {
                http.AddStandardResilienceHandler();
            });

        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;
    }
}

Now every service that calls builder.AddServiceDefaults() gets health checks, tracing, metrics, and HTTP resilience. You're not repeating configuration across ten projects. You're not gambling that someone forgot to add OpenTelemetry instrumentation. It's declared once, inherited everywhere.

In the AppHost, you can even layer additional configuration:

csharp
var rideApi = builder
    .AddProject<Projects.RideApi>("ride-api")
    .WithReference(pg)
    .WithEnvironment("OpenTelemetry__SamplingRatio", "1.0"); // Full tracing in local dev

The dashboard — Logs, traces and metrics for every service locally

Run your AppHost and Aspire launches a dashboard. It's a web interface showing every service, their health status, and live logs. Click into a service and you see structured logs, traces, and metrics—the same observability you'd get from an enterprise APM tool, running on your laptop.

This changes local debugging. Instead of:

  1. Running your API in a terminal
  2. Opening another terminal to tail database logs
  3. SSH-ing into a container to check Redis
  4. Hunting through exception logs to find correlation IDs

You open the Aspire dashboard and see everything at once. Logs are structured and queryable. If a request times out, you can see the full distributed trace—where it spent time, which service was slow, where it failed.

I ran this locally while debugging a payment service that was timing out intermittently. In 90 seconds, the Aspire dashboard showed me that the API was hitting Redis, Redis was responding fine, but the database query was taking 8 seconds. Without the dashboard, I would have added logging and redeployed—costing 10 minutes per iteration.

Not a production host — Aspire shines locally and generates deployment manifests

Here's an important boundary: Aspire is a local development orchestrator, not a production host. The AppHost runs on your machine. It's not Kubernetes replacement.

But Aspire does generate deployment artifacts. You can export your AppHost configuration to:

  • Azure Container Apps — Aspire generates Azure resource templates
  • Docker Compose — For on-premise or legacy deployments
  • Kubernetes manifests — If your team runs Kubernetes

This is powerful because your local setup and production setup are derived from the same source. You're not maintaining Docker Compose for local, Kubernetes YAML for staging, and CloudFormation for production—three separate and drifting definitions of the same system.

The export command looks like:

csharp
dotnet run --project AppHost -- --publisher azure

Or for Docker Compose:

csharp
dotnet run --project AppHost -- --publisher compose

Aspire generates the manifests. You review them, adjust resource limits or environment-specific secrets, and deploy.

Practical workflow — Getting started

Start with the .NET Aspire templates. Create a new AppHost project:

csharp
dotnet new aspire

This scaffolds an AppHost solution with a sample API and dashboard. From there:

  1. Add your infrastructure resources using builder.AddPostgres(), builder.AddRedis(), etc.
  2. Add your .NET projects using builder.AddProject<T>()
  3. Declare dependencies with .WithReference()
  4. Run dotnet run in the AppHost and let it orchestrate everything

Your team members do the same—they clone the repo, run the AppHost, and have a fully working local environment in under a minute. No Docker setup guide, no "it works on my machine" excuses.

Common mistakes

  • Forgetting to reference the ServiceDefaults project — If a new service doesn't call AddServiceDefaults(), it won't get observability or health checks. Use a template or code generator to prevent this.
  • Hardcoding connection strings — Don't. Aspire generates them. Use IConfiguration or dependency injection to read them from the runtime environment.
  • Treating Aspire as a production orchestrator — It's not. Use it for local development and generating manifests, but your production deployment tooling (Azure, Kubernetes, etc.) is still your responsibility.
  • Not running the dashboard — The whole point is observability. Open the dashboard. Use it. The insights pay for the setup cost.
  • Expecting Aspire to replace your deployment pipeline — Aspire generates manifests, but secrets management, traffic routing, auto-scaling policies, and compliance checks are still your domain.

Key takeaways

  • .NET Aspire replaces Docker Compose and manual configuration with type-safe C# code that describes your entire system.
  • The AppHost orchestrates services and infrastructure locally, generating connection strings and networking automatically.
  • Service defaults configure health checks, OpenTelemetry, and resilience once—every service inherits those configurations.
  • The Aspire dashboard provides local observability: structured logs, traces, and metrics for every service, out of the box.
  • Aspire is a development tool that generates deployment manifests for Azure, Docker Compose, or Kubernetes—your local setup and production deployment come from the same source.
  • New team members get a working, production-like local environment in minutes with zero configuration.
  • Avoid hardcoding connection strings, ensure every service references ServiceDefaults, and use the dashboard actively for debugging.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles