You are viewing a preview of this lesson. Sign in to start learning
Back to ASP.NET with .NET 10

.NET Aspire

Aspire streamlines building, running, debugging, and deploying distributed apps. Picture your app as a set of services, databases, and frontends—when they’re deployed, they all work together seamlessly, but every time you develop them they need to be individually started and connected. With Aspire, you get a unified toolchain that eliminates complex configs and makes local debugging effortless. Instantly launch and debug your entire app with a single command. Ready to deploy? Aspire lets you publish anywhere—Kubernetes, the cloud, or your own servers. It’s also fully extensible, so you can integrate your favorite tools and services with ease.

.NET Aspire for Cloud-Native Applications

.NET Aspire revolutionizes cloud-native development with orchestration tools and integrated observability features. Master these concepts with free flashcards and spaced repetition practice as you explore service discovery, distributed tracing, and application composition—essential skills for building scalable microservices in .NET 10.

Welcome to .NET Aspire 🚀

NET Aspire is an opinionated, cloud-ready stack for building observable, production-ready, distributed applications. It's not a framework you deploy—it's a development-time orchestrator that simplifies local development of complex cloud-native systems.

💡 Key Insight: .NET Aspire runs during development to coordinate multiple services, inject configurations, and provide telemetry—but your actual deployed application doesn't require Aspire components in production (though you can use them).

What Problem Does .NET Aspire Solve?

Imagine building a microservices application with:

  • 🗄️ A PostgreSQL database
  • 🔄 A Redis cache
  • 📨 A RabbitMQ message queue
  • 🌐 Three web APIs
  • 📱 A frontend app

Without Aspire: You manually start each service, configure connection strings, set up observability tools, and maintain multiple configuration files.

With Aspire: You define your application composition in C# code, and Aspire orchestrates everything with a single command.

Core Concepts 💻

1. The AppHost Project

The AppHost is the orchestration center of your Aspire application. It's a special console application that defines your distributed application's architecture.

var builder = DistributedApplication.CreateBuilder(args);

// Define resources
var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres")
    .AddDatabase("catalogdb");

// Define projects with dependencies
var apiService = builder.AddProject<Projects.ApiService>("apiservice")
    .WithReference(cache)
    .WithReference(db);

builder.AddProject<Projects.Web>("webfrontend")
    .WithReference(apiService);

builder.Build().Run();

What's happening here?

  • 📦 Resources (Redis, Postgres) are declared as dependencies
  • 🔗 Service references automatically inject connection strings
  • 🎯 Project references enable service discovery

2. Service Discovery

.NET Aspire provides automatic service discovery without manual configuration. When you reference a service, Aspire injects the endpoint configuration.

// In your Web project
public class CatalogService
{
    private readonly HttpClient _httpClient;
    
    // Aspire resolves "apiservice" to the actual endpoint
    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

// Startup configuration
builder.Services.AddHttpClient<CatalogService>(
    client => client.BaseAddress = new("http://apiservice"));

🧠 Memory Device: Think "Aspire Auto-Addresses" - Aspire Automatically handles service Addresses.

3. Observability Dashboard 📊

Aspire includes a built-in dashboard that provides:

  • 📈 Distributed tracing (OpenTelemetry)
  • 📝 Structured logging from all services
  • 📊 Metrics (CPU, memory, request rates)
  • 🔍 Resource health monitoring

The dashboard launches automatically at http://localhost:15888 (by default) when you run the AppHost.

┌─────────────────────────────────────────────────┐
│         ASPIRE DASHBOARD ARCHITECTURE           │
├─────────────────────────────────────────────────┤
│                                                 │
│   🌐 Browser ← → 📊 Dashboard (Port 15888)    │
│                      │                          │
│                      ↓                          │
│              ┌───────────────┐                  │
│              │ OTLP Collector│                  │
│              └───────┬───────┘                  │
│                      │                          │
│        ┌─────────────┼─────────────┐            │
│        ↓             ↓             ↓            │
│    📝 Logs      📈 Traces      📊 Metrics      │
│        │             │             │            │
│        └─────────────┴─────────────┘            │
│                      ↑                          │
│              ┌───────┴────────┐                 │
│              │                │                 │
│         🔧 API Service   🌐 Web App            │
│                                                 │
└─────────────────────────────────────────────────┘

4. Component Packages

.NET Aspire provides component packages that integrate with your services:

ComponentPackagePurpose
🔴 RedisAspire.StackExchange.RedisDistributed caching
🗄️ PostgreSQLAspire.NpgsqlDatabase connection
📨 RabbitMQAspire.RabbitMQ.ClientMessage queuing
🔵 Azure BlobAspire.Azure.Storage.BlobsCloud storage
📧 SMTPAspire.MailKitEmail services

These packages provide:

  • Health checks out of the box
  • 📊 Telemetry integration (logs, traces, metrics)
  • ⚙️ Configuration binding for connection strings
  • 🔒 Resilience patterns (retries, circuit breakers)

5. Configuration Management

Aspire uses named references that map to configuration sections:

// AppHost
var db = builder.AddPostgres("postgres")
    .AddDatabase("catalogdb");

var api = builder.AddProject<Projects.ApiService>("apiservice")
    .WithReference(db);
// In ApiService - automatic configuration injection
builder.AddNpgsqlDbContext<CatalogContext>("catalogdb");

The connection string is automatically injected as:

{
  "ConnectionStrings": {
    "catalogdb": "Host=localhost;Port=5432;Database=catalogdb;..."
  }
}

6. Environment Variables and Secrets

Aspire manages environment-specific configuration elegantly:

// AppHost
var apiKey = builder.AddParameter("openai-key", secret: true);

var api = builder.AddProject<Projects.ApiService>("apiservice")
    .WithEnvironment("OpenAI__ApiKey", apiKey);

🔒 Security Note: Parameters marked as secret: true are never displayed in logs or the dashboard.

7. Container Resources

Aspire can orchestrate container-based resources without requiring Docker Compose files:

var postgres = builder.AddPostgres("postgres")
    .WithPgAdmin()  // Adds pgAdmin container
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache")
    .WithRedisCommander();  // Adds Redis Commander UI

var rabbitmq = builder.AddRabbitMQ("messaging")
    .WithManagementPlugin();  // Adds RabbitMQ management UI

💡 Tip: Aspire uses container images from Docker Hub by default but can be configured for private registries.

8. Deployment Manifests

Aspire generates deployment manifests for various targets:

dotnet run --project AppHost --publisher manifest --output-path ../deploy

This creates a manifest.json that describes your application topology for:

  • ☁️ Azure Container Apps
  • 🐳 Kubernetes
  • 🚀 Docker Compose
DEPLOYMENT WORKFLOW

┌─────────────────┐
│  AppHost.csproj │ (Your composition)
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ manifest.json   │ (Generated)
└────────┬────────┘
         │
    ┌────┴────┐
    ▼         ▼
 ☁️ Azure   🐳 K8s

Practical Examples 🔧

Example 1: Simple Web API with Redis Cache

AppHost Project:

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
    .WithRedisCommander();  // Optional management UI

var api = builder.AddProject<Projects.WeatherApi>("weatherapi")
    .WithReference(cache);

builder.Build().Run();

WeatherApi Project:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Aspire automatically configures the Redis connection
builder.AddRedisClient("cache");

var app = builder.Build();

app.MapGet("/weather/{city}", async (
    string city,
    IConnectionMultiplexer redis) =>
{
    var db = redis.GetDatabase();
    var cacheKey = $"weather:{city}";
    
    var cached = await db.StringGetAsync(cacheKey);
    if (cached.HasValue)
    {
        return Results.Ok(cached.ToString());
    }
    
    var weather = await FetchWeatherData(city);
    await db.StringSetAsync(cacheKey, weather, TimeSpan.FromMinutes(10));
    
    return Results.Ok(weather);
});

app.Run();

What Aspire Does:

  1. 🚀 Starts Redis container automatically
  2. 🔗 Injects connection string into WeatherApi
  3. 📊 Collects telemetry from both services
  4. 🌐 Opens dashboard showing logs and metrics

Example 2: Microservices with Database and Messaging

var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
    .WithPgAdmin()
    .AddDatabase("ordersdb");

var rabbitmq = builder.AddRabbitMQ("messaging")
    .WithManagementPlugin();

// Service projects
var orderService = builder.AddProject<Projects.OrderService>("orderservice")
    .WithReference(postgres)
    .WithReference(rabbitmq);

var notificationService = builder.AddProject<Projects.NotificationService>("notificationservice")
    .WithReference(rabbitmq);

var webApp = builder.AddProject<Projects.WebApp>("webapp")
    .WithReference(orderService);

builder.Build().Run();

Order Service publishes events:

public class OrderService
{
    private readonly IConnection _connection;
    
    public async Task CreateOrder(Order order)
    {
        // Save to database
        await _dbContext.Orders.AddAsync(order);
        await _dbContext.SaveChangesAsync();
        
        // Publish event
        var channel = _connection.CreateModel();
        var message = JsonSerializer.Serialize(new OrderCreatedEvent(order.Id));
        var body = Encoding.UTF8.GetBytes(message);
        
        channel.BasicPublish(
            exchange: "orders",
            routingKey: "order.created",
            body: body);
    }
}

Notification Service consumes events:

var channel = connection.CreateModel();
channel.QueueDeclare("order-notifications", durable: true);
channel.QueueBind("order-notifications", "orders", "order.created");

var consumer = new EventingBasicConsumer(channel);
consumer.Received += async (model, ea) =>
{
    var body = ea.Body.ToArray();
    var message = Encoding.UTF8.GetString(body);
    var orderEvent = JsonSerializer.Deserialize<OrderCreatedEvent>(message);
    
    await SendNotificationEmail(orderEvent.OrderId);
};

channel.BasicConsume("order-notifications", autoAck: true, consumer);

Example 3: Using Azure Services in Development

var builder = DistributedApplication.CreateBuilder(args);

// Use Azure Blob Storage emulator (Azurite) locally
var storage = builder.AddAzureStorage("storage")
    .RunAsEmulator()  // Uses Azurite container
    .AddBlobs("images");

// Use Azure Service Bus emulator locally
var serviceBus = builder.AddAzureServiceBus("messaging")
    .RunAsEmulator();  // Requires Azure Service Bus emulator

var api = builder.AddProject<Projects.ImageApi>("imageapi")
    .WithReference(storage)
    .WithReference(serviceBus);

builder.Build().Run();

ImageApi using blob storage:

var builder = WebApplication.CreateBuilder(args);

// Aspire component provides BlobServiceClient
builder.AddAzureBlobClient("images");

var app = builder.Build();

app.MapPost("/upload", async (
    IFormFile file,
    BlobServiceClient blobClient) =>
{
    var containerClient = blobClient.GetBlobContainerClient("uploads");
    await containerClient.CreateIfNotExistsAsync();
    
    var blobClient = containerClient.GetBlobClient(file.FileName);
    await blobClient.UploadAsync(file.OpenReadStream(), overwrite: true);
    
    return Results.Ok(new { url = blobClient.Uri });
});

app.Run();

🤔 Did you know? Aspire can automatically switch from emulators (development) to real Azure services (production) using the same code—just change the AppHost configuration!

Example 4: Custom Resource Configuration

var builder = DistributedApplication.CreateBuilder(args);

// Add custom container resource
var elasticsearch = builder.AddContainer("elasticsearch", "elasticsearch", "8.11.0")
    .WithEndpoint(port: 9200, targetPort: 9200, name: "http")
    .WithEnvironment("discovery.type", "single-node")
    .WithEnvironment("xpack.security.enabled", "false");

var api = builder.AddProject<Projects.SearchApi>("searchapi")
    .WithReference(elasticsearch);

builder.Build().Run();

SearchApi connecting to custom resource:

// The endpoint URL is automatically injected
builder.Services.AddSingleton(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var connectionString = config.GetConnectionString("elasticsearch");
    
    return new ElasticClient(new Uri(connectionString));
});

Common Mistakes ⚠️

Mistake 1: Deploying the AppHost to Production

Wrong:

// Don't deploy AppHost.csproj to production!
// It's for orchestration during development only

Correct:

# Generate deployment manifest instead
dotnet run --project AppHost --publisher manifest
# Deploy individual services using the manifest
az containerapp up --manifest manifest.json

Mistake 2: Hardcoding Connection Strings

Wrong:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql("Host=localhost;Port=5432;Database=mydb"));

Correct:

// Let Aspire inject the connection string
builder.AddNpgsqlDbContext<AppDbContext>("mydb");

Mistake 3: Forgetting to Add Component Packages

Wrong:

// AppHost references Redis, but ApiService doesn't have the component package
var cache = builder.AddRedis("cache");
var api = builder.AddProject<Projects.ApiService>("api")
    .WithReference(cache);  // Runtime error!

Correct:

# In ApiService project
dotnet add package Aspire.StackExchange.Redis
// Then in Program.cs
builder.AddRedisClient("cache");

Mistake 4: Using Wrong Reference Names

Wrong:

// AppHost
var db = builder.AddPostgres("postgres").AddDatabase("catalogdb");

// ApiService - name mismatch!
builder.AddNpgsqlDbContext<CatalogContext>("catalog");  // Won't find connection

Correct:

// Names must match exactly
builder.AddNpgsqlDbContext<CatalogContext>("catalogdb");

Mistake 5: Not Using Health Checks

Wrong:

// Service doesn't expose health endpoint
var app = builder.Build();
app.Run();

Correct:

// Aspire components add health checks automatically
builder.AddRedisClient("cache");  // Includes health check

var app = builder.Build();

app.MapHealthChecks("/health");  // Expose health endpoint
app.Run();

Key Takeaways 🎯

  1. NET Aspire is a development-time orchestrator, not a runtime framework—it simplifies local microservices development

  2. AppHost project defines your distributed application's topology using C# code instead of YAML or JSON

  3. Service discovery is automatic—reference services by name, and Aspire handles endpoint resolution

  4. Built-in dashboard provides observability (logs, traces, metrics) without additional configuration

  5. Component packages (Aspire.* NuGet packages) provide preconfigured integrations with databases, caches, and cloud services

  6. Configuration injection happens automatically—connection strings and settings flow from AppHost to services

  7. Generate deployment manifests for production—don't deploy the AppHost itself

  8. Emulator support allows testing Azure services locally without cloud costs

💡 Pro Tip: Use .WithReplicas(3) on service projects to test load balancing and resilience patterns locally!

📚 Further Study

📋 Quick Reference Card

AppHostOrchestration project that defines app composition
AddProjectReferences a .NET project as a service
WithReferenceInjects dependency (DB, cache, service) into a project
AddRedis/AddPostgresAdds infrastructure resource (starts container)
Component PackageAspire.* NuGet providing integration + telemetry
Dashboardhttp://localhost:15888 - observability UI
Service DiscoveryReference services by name ("http://apiservice")
ManifestJSON describing app topology for deployment
RunAsEmulatorUses local emulator instead of cloud service
Health ChecksAutomatic with component packages
🚀 ASPIRE DEVELOPMENT WORKFLOW

┌─────────────────────────────────────────────┐
│  1. Create AppHost Project                  │
│     dotnet new aspire-apphost               │
└──────────────┬──────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────┐
│  2. Define Resources & Projects             │
│     var db = builder.AddPostgres(...)       │
│     var api = builder.AddProject(...)       │
└──────────────┬──────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────┐
│  3. Add Component Packages to Services      │
│     dotnet add package Aspire.Npgsql        │
└──────────────┬──────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────┐
│  4. Configure Services                      │
│     builder.AddNpgsqlDbContext("db")        │
└──────────────┬──────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────┐
│  5. Run AppHost                             │
│     dotnet run --project AppHost            │
└──────────────┬──────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────┐
│  6. View Dashboard                          │
│     http://localhost:15888                  │
└──────────────┬──────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────┐
│  7. Generate Manifest for Deployment        │
│     --publisher manifest --output-path ./   │
└─────────────────────────────────────────────┘