Skip to main content
.NET Aspire is the recommended way to run the FullStackHero starter kit locally. It orchestrates PostgreSQL, Redis, and your application services with automatic service discovery, configuration injection, and observability.

What is Aspire?

.NET Aspire is an opinionated, cloud-ready stack for building distributed applications. It provides:

Service Orchestration

Manages containers, dependencies, and startup order

Service Discovery

Automatic connection string injection between services

Configuration

Environment variables wired automatically

Observability

Built-in OpenTelemetry tracing, metrics, and logs

AppHost Configuration

The FSH.Playground.AppHost project defines the entire application topology. Here’s the actual configuration:
src/Playground/FSH.Playground.AppHost/AppHost.cs
var builder = DistributedApplication.CreateBuilder(args);

// Postgres container + database
var postgres = builder.AddPostgres("postgres").WithDataVolume("fsh-postgres-data").AddDatabase("fsh");

var redis = builder.AddRedis("redis").WithDataVolume("fsh-redis-data");

builder.AddProject<Projects.Playground_Api>("playground-api")
    .WithReference(postgres)
    .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
    .WithEnvironment("OpenTelemetryOptions__Exporter__Otlp__Endpoint", "https://localhost:4317")
    .WithEnvironment("OpenTelemetryOptions__Exporter__Otlp__Protocol", "grpc")
    .WithEnvironment("OpenTelemetryOptions__Exporter__Otlp__Enabled", "true")
    .WithEnvironment("DatabaseOptions__Provider", "POSTGRESQL")
    .WithEnvironment("DatabaseOptions__ConnectionString", postgres.Resource.ConnectionStringExpression)
    .WithEnvironment("DatabaseOptions__MigrationsAssembly", "FSH.Playground.Migrations.PostgreSQL")
    .WaitFor(postgres)
    .WithReference(redis)
    .WithEnvironment("CachingOptions__Redis", redis.Resource.ConnectionStringExpression)
    .WithEnvironment("CachingOptions__EnableSsl", "true")
    .WaitFor(redis);

builder.AddProject<Projects.Playground_Blazor>("playground-blazor");

await builder.Build().RunAsync();

How It Works

1

Builder Creation

Aspire creates a distributed application builder:
var builder = DistributedApplication.CreateBuilder(args);
This reads configuration from appsettings.json and environment variables.
2

PostgreSQL Container

Adds a PostgreSQL container with persistent storage:
var postgres = builder.AddPostgres("postgres")
    .WithDataVolume("fsh-postgres-data")
    .AddDatabase("fsh");
What this does:
  • Pulls postgres:17 image from Docker Hub
  • Creates a named Docker volume fsh-postgres-data for persistence
  • Creates a database named fsh
  • Generates a connection string automatically
  • Exposes on default port 5432
3

Redis Container

Adds a Redis container for distributed caching:
var redis = builder.AddRedis("redis")
    .WithDataVolume("fsh-redis-data");
What this does:
  • Pulls redis:7 image
  • Creates volume fsh-redis-data
  • Exposes on default port 6379
4

Playground API Project

Registers the API project with service references and configuration:
builder.AddProject<Projects.Playground_Api>("playground-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WaitFor(postgres)
    .WaitFor(redis);
Key features:
  • .WithReference(postgres): Injects the PostgreSQL connection string as environment variable
  • .WaitFor(postgres): Ensures Postgres is healthy before starting the API
  • Environment variables: Automatically wired from Aspire to the API
5

Blazor UI Project

Registers the Blazor WebAssembly frontend:
builder.AddProject<Projects.Playground_Blazor>("playground-blazor");
The Blazor app discovers the API via Aspire’s service discovery.

Environment Variables Injected

Aspire automatically injects these environment variables into the Playground API:
VariableValuePurpose
ASPNETCORE_ENVIRONMENTDevelopmentEnables development mode
DatabaseOptions__ProviderPOSTGRESQLSets EF Core provider
DatabaseOptions__ConnectionString{postgres.connectionString}Auto-generated connection string
DatabaseOptions__MigrationsAssemblyFSH.Playground.Migrations.PostgreSQLMigrations project
CachingOptions__Redis{redis.connectionString}Redis connection string
OpenTelemetryOptions__Exporter__Otlp__Endpointhttps://localhost:4317OTLP exporter endpoint
OpenTelemetryOptions__Exporter__Otlp__EnabledtrueEnables telemetry export
Connection strings use Aspire’s ConnectionStringExpression, which resolves at runtime to the actual container IP and port.

Service Dependencies

Aspire ensures services start in the correct order: The .WaitFor() method ensures health checks pass before dependent services start.

AppHost Project Structure

src/Playground/FSH.Playground.AppHost/FSH.Playground.AppHost.csproj
<Project Sdk="Aspire.AppHost.Sdk/13.1.0">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UserSecretsId>9fe5df9a-b9b2-4202-bdb4-d30b01b71d1a</UserSecretsId>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.PostgreSQL" />
    <PackageReference Include="Aspire.Hosting.Redis" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Playground.Api\Playground.Api.csproj" />
    <ProjectReference Include="..\Playground.Blazor\Playground.Blazor.csproj" />
  </ItemGroup>
</Project>
Key NuGet packages:
  • Aspire.Hosting.PostgreSQL (v13.1.0): PostgreSQL container hosting
  • Aspire.Hosting.Redis (v13.1.0): Redis container hosting

Running the AppHost

Start the entire stack:
dotnet run --project src/Playground/FSH.Playground.AppHost
What happens:
1

Container Orchestration

Aspire pulls Docker images and starts containers:
docker ps
You’ll see postgres and redis containers running.
2

Health Checks

Aspire waits for container health checks before proceeding:
  • PostgreSQL: Waits for TCP connection on port 5432
  • Redis: Waits for PING response
3

Configuration Injection

Connection strings and environment variables are injected into the API.
4

Migration Execution

The Playground API calls UseHeroMultiTenantDatabases() which:
  • Applies pending EF Core migrations
  • Seeds initial data (admin user, permissions)
  • Creates tenant schemas
5

Service Startup

All services start in dependency order:
  • API: https://localhost:5285
  • Blazor: https://localhost:7140

Observability

Aspire enables comprehensive observability out of the box.

OpenTelemetry Export

The API exports telemetry to http://localhost:4317 (OTLP gRPC endpoint):
From appsettings.json
{
  "OpenTelemetryOptions": {
    "Enabled": true,
    "Tracing": { "Enabled": true },
    "Metrics": {
      "Enabled": true,
      "MeterNames": ["FSH.Modules.Identity", "FSH.Modules.Multitenancy", "FSH.Modules.Auditing"]
    },
    "Exporter": {
      "Otlp": {
        "Enabled": true,
        "Endpoint": "http://localhost:4317",
        "Protocol": "grpc"
      }
    }
  }
}

Traced Operations

  • HTTP requests: ASP.NET Core instrumentation
  • EF Core queries: Database command tracing (filtered via FilterEfStatements)
  • Redis commands: StackExchange.Redis instrumentation (filtered)
  • Hangfire jobs: Background job execution
  • Mediator handlers: Command/query handling

Metrics Collected

  • Request duration histograms
  • Module-specific meters (Identity, Multitenancy, Auditing)
  • Job execution metrics
  • Cache hit/miss rates

Customizing AppHost

Adding a New Service

To add another project or container:
var myService = builder.AddProject<Projects.MyService>("my-service")
    .WithReference(postgres)
    .WithEnvironment("MyConfig__Value", "example")
    .WaitFor(postgres);

Using SQL Server Instead

Replace PostgreSQL with SQL Server:
var sqlserver = builder.AddSqlServer("sqlserver")
    .WithDataVolume("fsh-sqlserver-data")
    .AddDatabase("fsh");

builder.AddProject<Projects.Playground_Api>("playground-api")
    .WithReference(sqlserver)
    .WithEnvironment("DatabaseOptions__Provider", "SQLSERVER")
    .WithEnvironment("DatabaseOptions__MigrationsAssembly", "FSH.Playground.Migrations.SqlServer");

Adding Jaeger for Tracing

var jaeger = builder.AddContainer("jaeger", "jaegertracing/all-in-one")
    .WithEndpoint(16686, 16686, "ui")
    .WithEndpoint(4317, 4317, "otlp");

builder.AddProject<Projects.Playground_Api>("playground-api")
    .WithReference(jaeger)
    .WaitFor(jaeger);

Data Persistence

Aspire creates named Docker volumes for data persistence:
View volumes
docker volume ls | grep fsh
Output:
fsh-postgres-data
fsh-redis-data
Data survives container restarts. To reset:
docker volume rm fsh-postgres-data fsh-redis-data

Aspire Dashboard

Aspire provides a web dashboard at http://localhost:15000 (may vary) showing:
  • Running services and their health
  • Logs from all containers
  • Distributed traces
  • Metrics and histograms
  • Environment variables
The dashboard URL is printed to the console when you run dotnet run --project FSH.Playground.AppHost.

Production Deployment

Aspire is designed for local development only. For production:
  1. Use Kubernetes, Docker Compose, or cloud-native services (Azure Container Apps, AWS ECS)
  2. Externalize configuration to Azure Key Vault, AWS Secrets Manager, or Kubernetes Secrets
  3. Use managed databases (Azure PostgreSQL, AWS RDS) instead of containers
  4. Configure proper OTLP endpoints (Azure Monitor, Datadog, New Relic)
See Deployment Guide for production patterns.

Next Steps

Configuration

Explore all configuration options

Observability

Deep dive into OpenTelemetry setup

Database

Learn about EF Core and migrations

Modules

Understand the modular architecture

Build docs developers (and LLMs) love