
Dotnet Aspire
- 95 installs
- 22 repo stars
- Updated February 19, 2026
- markpitt/claude-skills
Helps with ai & agent building tasks.
About
dotnet-aspire is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- dotnet-aspire
- AI & Agent Building
- AI-coding skill
Dotnet Aspire by the numbers
- 95 all-time installs (skills.sh)
- Ranked #4,602 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/markpitt/claude-skills --skill dotnet-aspireAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 95 |
|---|---|
| repo stars | ★ 22 |
| Last updated | February 19, 2026 |
| Repository | markpitt/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
.NET Aspire Integration Skill
This skill helps add .NET Aspire to existing .NET solutions or create new Aspire-enabled distributed applications. It provides modular guidance for orchestration, service discovery, component integration, configuration management, and deployment.
What Is .NET Aspire?
.NET Aspire is an opinionated, cloud-ready stack for building observable, production-ready, distributed applications. It provides:
- Service orchestration - Coordinate multiple projects and services with dependency management
- Service discovery - Automatic discovery and connection between services
- Telemetry and observability - Built-in logging, metrics, and distributed tracing
- Configuration management - Centralized configuration with strong typing and secrets
- Resource provisioning - Integration with databases, caching, messaging, and cloud services
- Developer dashboard - Local monitoring and debugging interface
When to Use This Skill
Use this skill when:
- Adding Aspire to an existing .NET solution with multiple services
- Creating a new distributed application with Aspire
- Modernizing microservices or distributed systems for cloud deployment
- Setting up service orchestration for local development and deployment
- Integrating cloud-native observability and configuration patterns
What Component/Feature Do I Need?
| Need | Resource | Description |
|---|---|---|
| Overall structure | Overview & Setup | Step-by-step implementation from analysis to running |
| Database, cache, messaging | resources/components.md | All available Aspire component packages with examples |
| Inter-service communication | resources/service-communication.md | Service discovery, HttpClient patterns, resilience |
| Configuration & secrets | resources/configuration-management.md | Environment settings, secrets, feature flags |
| Local development | resources/local-development.md | Dashboard, debugging, testing, health checks |
| Production deployment | resources/deployment.md | Azure Container Apps, Kubernetes, Docker Compose |
Overview & Setup
Core Concept
.NET Aspire uses two key projects:
- AppHost - Orchestrates services and resources; provides the developer dashboard
- ServiceDefaults - Shared configuration for all services (OpenTelemetry, health checks, service discovery)
Prerequisites
# Install .NET Aspire workload
dotnet workload install aspire
# Verify installation
dotnet workload list # Should show "aspire"
# Docker Desktop (for container resources)
# Ensure it's running before launching AppHostBasic Implementation Flow
1. Analyze the solution
- Identify services (APIs, web apps, workers)
- List external dependencies (databases, Redis, message queues)
- Determine service communication patterns
2. Create Aspire projects
dotnet new aspire-apphost -n MyApp.AppHost
dotnet new aspire-servicedefaults -n MyApp.ServiceDefaults
dotnet sln add MyApp.AppHost/MyApp.AppHost.csproj
dotnet sln add MyApp.ServiceDefaults/MyApp.ServiceDefaults.csproj3. Configure services
- Add ServiceDefaults reference to each service
- Call
builder.AddServiceDefaults()in Program.cs - Call
app.MapDefaultEndpoints()for ASP.NET Core services
4. Orchestrate in AppHost
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var database = builder.AddPostgres("postgres").AddDatabase("appdb");
var api = builder.AddProject<Projects.MyApi>("api")
.WithReference(database)
.WithReference(cache);
var web = builder.AddProject<Projects.MyWeb>("web")
.WithReference(api)
.WithExternalHttpEndpoints();
builder.Build().Run();5. Update service communication
- Replace hardcoded URLs with service names
- Use
builder.AddServiceDefaults()pattern matching
6. Run and verify
dotnet run --project MyApp.AppHost
# Opens dashboard at https://localhost:15001Key Decisions
AppHost Project Naming:
- Convention:
[SolutionName].AppHost - Example: For solution "ECommerceSystem", create "ECommerceSystem.AppHost"
Service Resource Names:
- Use short, descriptive names in AppHost
- Examples: "api", "web", "worker", "cache", "database"
- These names are used for service discovery URLs
Resource Management:
- Local development: Use Aspire-managed containers (PostgreSQL, Redis, RabbitMQ)
- Production: Azure resources auto-provisioned by
azdor manually configured - Connection strings: Automatically injected; rarely need hardcoding
Service Discovery Setup:
- HttpClient URLs use service names:
http://apiinstead ofhttps://localhost:7001 - Aspire handles routing and authentication between services
- External services use explicit endpoint configuration
Common Architectures
Web API + Frontend
var api = builder.AddProject<Projects.Api>("api")
.WithReference(database)
.WithExternalHttpEndpoints();
var web = builder.AddProject<Projects.Web>("web")
.WithReference(api)
.WithExternalHttpEndpoints();Microservices with Message Queue
var messaging = builder.AddRabbitMQ("messaging");
var orderService = builder.AddProject<Projects.OrderService>("orders")
.WithReference(messaging);
var inventoryService = builder.AddProject<Projects.InventoryService>("inventory")
.WithReference(messaging);Multi-Database System
var postgres = builder.AddPostgres("postgres")
.AddDatabase("users")
.AddDatabase("products");
var mongo = builder.AddMongoDB("mongo")
.AddDatabase("orders");
var userApi = builder.AddProject<Projects.UserApi>("userapi")
.WithReference(postgres);
var orderApi = builder.AddProject<Projects.OrderApi>("orderapi")
.WithReference(mongo);Resource Index
For detailed implementation guidance, see:
- Components - Component packages and integration patterns:
resources/components.md - Service Communication - Service discovery and inter-service calls:
resources/service-communication.md - Configuration Management - Secrets, environment variables, settings:
resources/configuration-management.md - Local Development - Dashboard features, debugging, health checks:
resources/local-development.md - Deployment - Azure Container Apps, Kubernetes, Docker Compose:
resources/deployment.md
Best Practices
Service Organization
- Keep AppHost focused on composition, not business logic
- Use ServiceDefaults for cross-cutting concerns (observability, health checks)
- Ensure each service runs independently with fallback configuration
Resource Management
- Use Aspire-managed resources for local development consistency
- Define explicit dependencies in AppHost via
.WithReference() - Add data persistence for databases:
.WithDataVolume()
Configuration Patterns
- Development: Use
appsettings.Development.jsonand.WithEnvironment() - Production: Use Azure Key Vault or managed secrets
- Avoid secrets in source control; use user secrets locally
Observable Services
- Enable OpenTelemetry via ServiceDefaults (automatic)
- Use the developer dashboard for local debugging
- Export telemetry to Application Insights or similar in production
Common Issues & Solutions
Services can't communicate
- Verify service name in AppHost matches HttpClient URL
- Ensure
AddServiceDefaults()is called in all services - Check that ASP.NET services call
MapDefaultEndpoints()
Connection strings not injected
- Use
builder.AddNpgsqlDbContext<>()instead of manual configuration - Verify database/resource name matches between AppHost and service
- Confirm ServiceDefaults reference exists in project file
Dashboard won't start
- Ensure Docker Desktop is running
- Check for port conflicts (default: 15001)
- Verify AppHost project runs, not individual services
Health checks failing
- Review resource startup logs in dashboard
- Check port availability on local machine
- Verify Docker has sufficient resources
Getting Started Checklist
- [ ] Install .NET Aspire workload and verify
- [ ] Analyze solution structure and identify services
- [ ] Create AppHost and ServiceDefaults projects
- [ ] Add ServiceDefaults reference to each service
- [ ] Update service Program.cs with
AddServiceDefaults()andMapDefaultEndpoints() - [ ] Configure AppHost orchestration with services and resources
- [ ] Update service HttpClient URLs to use service discovery names
- [ ] Test locally with
dotnet run --project AppHost - [ ] Verify dashboard shows all services running
- [ ] Configure deployment target (Azure, Kubernetes, etc.)
Next Steps
1. For component details → See resources/components.md 2. For service communication → See resources/service-communication.md 3. For configuration → See resources/configuration-management.md 4. For local development → See resources/local-development.md 5. For deployment → See resources/deployment.md
---
Remember: Start with a single service, verify communication works, then add complexity. Use the dashboard to debug issues locally before deploying to production. 1. Which projects should be orchestrated as services? 2. What external resources are needed (databases, Redis, storage, etc.)? 3. Should this use the minimal Aspire setup or include additional components? 4. Are there any existing Docker or orchestration configurations?
2. Clarification Questions
Before implementing, confirm with the user:
Service Identification:
- "I've identified [X] services in your solution: [list]. Should all of these be included in Aspire orchestration?"
- "Are there any additional services or projects that should be added?"
Infrastructure Requirements:
- "I see references to [database/Redis/etc.]. Should Aspire manage these resources?"
- "Do you want to use container resources or connection string configuration?"
Aspire Structure:
- "Should I create a new AppHost project named '[SolutionName].AppHost' and ServiceDefaults project named '[SolutionName].ServiceDefaults'?"
- "Do you prefer a specific naming convention?"
Dependencies:
- "Which services depend on each other? (This helps set up service discovery)"
- "Are there any startup ordering requirements?"
3. Implementation Steps
Step 1: Create Aspire Projects
Create the AppHost project:
dotnet new aspire-apphost -n [SolutionName].AppHostThe AppHost project:
- Orchestrates all services and resources
- Defines service dependencies and configurations
- Provides the developer dashboard for local development
- Contains
Program.cswith application composition
Create the ServiceDefaults project:
dotnet new aspire-servicedefaults -n [SolutionName].ServiceDefaultsThe ServiceDefaults project:
- Provides shared service configuration
- Configures OpenTelemetry, health checks, and service discovery
- Applied to all services for consistent behavior
Add projects to solution:
dotnet sln add [SolutionName].AppHost/[SolutionName].AppHost.csproj
dotnet sln add [SolutionName].ServiceDefaults/[SolutionName].ServiceDefaults.csprojStep 2: Configure Service Projects
For each service project (API, Web App, Worker):
1. Add ServiceDefaults reference:
dotnet add [ServiceProject] reference [SolutionName].ServiceDefaults/[SolutionName].ServiceDefaults.csproj2. Update Program.cs to register service defaults:
// At the top of Program.cs, after builder creation
var builder = WebApplication.CreateBuilder(args);
// Add this line
builder.AddServiceDefaults();
// ... rest of service configuration ...
var app = builder.Build();
// Add this line before app.Run()
app.MapDefaultEndpoints();
app.Run();3. For Worker Services, the pattern is similar:
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
// ... service configuration ...
var host = builder.Build();
host.Run();Step 3: Configure the AppHost
Add project references in AppHost:
dotnet add [SolutionName].AppHost reference [ServiceProject1]/[ServiceProject1].csproj
dotnet add [SolutionName].AppHost reference [ServiceProject2]/[ServiceProject2].csprojUpdate AppHost Program.cs to orchestrate services:
var builder = DistributedApplication.CreateBuilder(args);
// Add infrastructure resources
var cache = builder.AddRedis("cache");
var postgres = builder.AddPostgres("postgres")
.AddDatabase("appdb");
// Add services with dependencies
var apiService = builder.AddProject<Projects.MyApi>("apiservice")
.WithReference(postgres)
.WithReference(cache);
var webApp = builder.AddProject<Projects.MyWebApp>("webapp")
.WithReference(apiService)
.WithExternalHttpEndpoints();
builder.Build().Run();Common resource methods:
.AddRedis("name")- Redis cache.AddPostgres("name").AddDatabase("dbname")- PostgreSQL.AddSqlServer("name").AddDatabase("dbname")- SQL Server.AddRabbitMQ("name")- RabbitMQ messaging.AddMongoDB("name").AddDatabase("dbname")- MongoDB.AddAzureStorage("name")- Azure Storage
Service configuration methods:
.WithReference(resource)- Add dependency and inject connection info.WithExternalHttpEndpoints()- Make service accessible externally.WithReplicas(count)- Run multiple instances.WithEnvironment("KEY", "value")- Add environment variables.WithHttpsEndpoint(port: 7001)- Specify HTTPS port
Step 4: Add Required NuGet Packages
Aspire packages are automatically added by templates, but verify:
AppHost project:
Aspire.Hosting.AppHost(typically included via workload)- Additional hosting packages for resources (e.g.,
Aspire.Hosting.PostgreSQL)
ServiceDefaults project:
Microsoft.Extensions.Http.ResilienceMicrosoft.Extensions.ServiceDiscoveryOpenTelemetry.Exporter.OpenTelemetryProtocolOpenTelemetry.Extensions.HostingOpenTelemetry.Instrumentation.AspNetCoreOpenTelemetry.Instrumentation.HttpOpenTelemetry.Instrumentation.Runtime
Service projects:
Aspire.Npgsql.EntityFrameworkCore.PostgreSQL(if using PostgreSQL)Aspire.StackExchange.Redis(if using Redis)- Component packages as needed for databases, messaging, etc.
Install packages:
dotnet add [Project] package [PackageName]Step 5: Update Service Communication
For services that call other services, use service discovery:
Before (hardcoded URLs):
builder.Services.AddHttpClient("apiservice", client =>
{
client.BaseAddress = new Uri("https://localhost:7001");
});After (service discovery):
builder.Services.AddHttpClient("apiservice", client =>
{
client.BaseAddress = new Uri("http://apiservice");
});The service name matches the name in AppHost's AddProject<>() call.
For typed HttpClients:
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("http://apiservice");
});Step 6: Configuration and Connection Strings
Resource connection strings are automatically injected. Update service configuration:
Before:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));After:
builder.AddNpgsqlDbContext<AppDbContext>("appdb");The connection name ("appdb") matches the database name in AppHost.
For Redis:
builder.AddRedisClient("cache");Step 7: Verify and Test
Run the AppHost project:
dotnet run --project [SolutionName].AppHostThis launches:
- The Aspire dashboard (typically at https://localhost:15001)
- All configured services
- Any resource containers (Redis, PostgreSQL, etc.)
Verify: 1. Dashboard shows all services running 2. Services can communicate via service discovery 3. Telemetry data appears in the dashboard 4. Resource connections work correctly
4. Advanced Configurations
External Services
For services not in the solution:
var externalApi = builder.AddProject<Projects.ExternalApi>("external-api")
.WithHttpsEndpoint(port: 8001);Container Resources
Run services in containers:
var myApi = builder.AddContainer("myapi", "myapiimage")
.WithHttpEndpoint(port: 8000, targetPort: 80);Azure Resources
For Azure-hosted resources:
var storage = builder.AddAzureStorage("storage")
.AddBlobs("blobs");
var keyVault = builder.AddAzureKeyVault("keyvault");Custom Resources
Extend Aspire with custom resources:
var customResource = builder.AddResource(new CustomResource("name"))
.WithEndpoint("http", endpoint => endpoint.Port = 9000);Best Practices
1. Service Organization
- Keep AppHost focused on orchestration, not business logic
- Use ServiceDefaults for cross-cutting concerns
- Ensure each service is independently runnable (with fallback config)
2. Resource Management
- Use Aspire-managed resources for local development
- Use connection strings for production deployments
- Configure resource persistence for databases (avoid data loss)
3. Configuration
- Use
appsettings.Development.jsonfor local overrides - Keep sensitive data in user secrets or key vaults
- Use environment-specific configurations
4. Dependencies
- Define explicit service dependencies in AppHost
- Use
.WithReference()to inject connection information - Consider startup order for database migrations
5. Observability
- Leverage built-in OpenTelemetry for distributed tracing
- Use the dashboard for local debugging
- Configure appropriate log levels per service
Common Patterns
API Gateway Pattern
var apiGateway = builder.AddProject<Projects.ApiGateway>("gateway")
.WithExternalHttpEndpoints();
var serviceA = builder.AddProject<Projects.ServiceA>("servicea");
var serviceB = builder.AddProject<Projects.ServiceB>("serviceb");
apiGateway.WithReference(serviceA).WithReference(serviceB);Worker with Message Queue
var messaging = builder.AddRabbitMQ("messaging");
var worker = builder.AddProject<Projects.Worker>("worker")
.WithReference(messaging);
var api = builder.AddProject<Projects.Api>("api")
.WithReference(messaging);Web App with Backend API
var cache = builder.AddRedis("cache");
var database = builder.AddPostgres("postgres").AddDatabase("appdb");
var api = builder.AddProject<Projects.Api>("api")
.WithReference(database)
.WithReference(cache);
var web = builder.AddProject<Projects.Web>("web")
.WithReference(api)
.WithExternalHttpEndpoints();Troubleshooting
Service Discovery Not Working
- Ensure
builder.AddServiceDefaults()is called in service Program.cs - Verify service name in HttpClient matches AppHost AddProject name
- Check that
app.MapDefaultEndpoints()is called for ASP.NET services
Connection Strings Not Injected
- Confirm resource name matches in both AppHost and service configuration
- Use
builder.AddNpgsqlDbContext<>()instead of manual AddDbContext - Verify ServiceDefaults reference exists
Dashboard Not Accessible
- Check AppHost is running (not individual services)
- Verify port isn't blocked (default: 15001)
- Look for dashboard URL in AppHost console output
Resources Not Starting
- Ensure Docker Desktop is running (for container resources)
- Check for port conflicts with existing services
- Review AppHost console for startup errors
Files to Modify
When adding Aspire to an existing solution, expect to modify:
1. Solution file (.sln) - Add AppHost and ServiceDefaults projects 2. Each service's Program.cs - Add service defaults registration 3. Each service's .csproj - Add ServiceDefaults reference 4. AppHost/Program.cs - Define orchestration and resources 5. Service configuration - Replace hardcoded URLs with service discovery 6. Database configuration - Use Aspire component methods
Prerequisites
Ensure the following are installed:
- .NET 8.0 SDK or later
- .NET Aspire workload:
dotnet workload install aspire - Docker Desktop (for container resources)
Verify installation:
dotnet workload listShould show "aspire" in the installed workloads.
Summary Checklist
After implementing Aspire, verify:
- [ ] AppHost project created and added to solution
- [ ] ServiceDefaults project created and added to solution
- [ ] All service projects reference ServiceDefaults
- [ ] Service Program.cs files call
AddServiceDefaults()andMapDefaultEndpoints() - [ ] AppHost Program.cs orchestrates all services with proper dependencies
- [ ] Service-to-service communication uses service discovery (not hardcoded URLs)
- [ ] Database and cache connections use Aspire component methods
- [ ] AppHost runs successfully and launches dashboard
- [ ] All services appear in dashboard and show healthy status
- [ ] Telemetry data appears for requests across services
Additional Resources
For detailed information about specific components and patterns, see:
resources/components.md- Aspire component packages and configurationsresources/deployment.md- Deploying Aspire applications to production
Example Output
When complete, the solution structure should look like:
MySolution/
├── MySolution.sln
├── MySolution.AppHost/
│ ├── Program.cs # Orchestration configuration
│ ├── MySolution.AppHost.csproj
│ └── appsettings.json
├── MySolution.ServiceDefaults/
│ ├── Extensions.cs # Service defaults implementation
│ └── MySolution.ServiceDefaults.csproj
├── MySolution.Api/
│ ├── Program.cs # Calls AddServiceDefaults()
│ └── MySolution.Api.csproj # References ServiceDefaults
├── MySolution.Web/
│ ├── Program.cs # Calls AddServiceDefaults()
│ └── MySolution.Web.csproj # References ServiceDefaults
└── MySolution.Worker/
├── Program.cs # Calls AddServiceDefaults()
└── MySolution.Worker.csproj # References ServiceDefaultsRunning dotnet run --project MySolution.AppHost starts all services and opens the dashboard for monitoring and debugging the distributed application.
.NET Aspire Component Packages
This reference guide covers the available Aspire component packages for integrating databases, caching, messaging, storage, and other cloud services into your distributed applications.
Component Categories
Database Components
PostgreSQL
Hosting Package: Aspire.Hosting.PostgreSQL Client Package: Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
AppHost Configuration:
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin() // Optional: Add PgAdmin container
.AddDatabase("mydb");Service Configuration:
builder.AddNpgsqlDbContext<MyDbContext>("mydb");Connection String Pattern: Host=localhost;Database=mydb;Username=postgres;Password=...
---
SQL Server
Hosting Package: Aspire.Hosting.SqlServer Client Package: Aspire.Microsoft.EntityFrameworkCore.SqlServer
AppHost Configuration:
var sql = builder.AddSqlServer("sqlserver")
.AddDatabase("catalogdb");Service Configuration:
builder.AddSqlServerDbContext<CatalogDbContext>("catalogdb");Connection String Pattern: Server=localhost,1433;Database=catalogdb;User Id=sa;Password=...
---
MySQL
Hosting Package: Aspire.Hosting.MySql Client Package: Aspire.Pomelo.EntityFrameworkCore.MySql
AppHost Configuration:
var mysql = builder.AddMySql("mysql")
.AddDatabase("appdb");Service Configuration:
builder.AddMySqlDbContext<AppDbContext>("appdb");---
MongoDB
Hosting Package: Aspire.Hosting.MongoDB Client Package: Aspire.MongoDB.Driver
AppHost Configuration:
var mongo = builder.AddMongoDB("mongodb")
.AddDatabase("ordersdb");Service Configuration:
builder.AddMongoDBClient("mongodb");Access in Service:
var mongoClient = serviceProvider.GetRequiredService<IMongoClient>();
var database = mongoClient.GetDatabase("ordersdb");---
Oracle Database
Hosting Package: Aspire.Hosting.Oracle Client Package: Aspire.Oracle.EntityFrameworkCore
AppHost Configuration:
var oracle = builder.AddOracle("oracle")
.AddDatabase("freepdb1");Service Configuration:
builder.AddOracleDbContext<MyOracleContext>("freepdb1");---
Caching Components
Redis
Hosting Package: Aspire.Hosting.Redis Client Packages:
Aspire.StackExchange.Redis(general caching)Aspire.StackExchange.Redis.OutputCaching(ASP.NET output caching)Aspire.StackExchange.Redis.DistributedCaching(distributed cache)
AppHost Configuration:
var redis = builder.AddRedis("cache")
.WithRedisCommander(); // Optional: Add Redis Commander UIService Configuration (General):
builder.AddRedisClient("cache");Service Configuration (Distributed Cache):
builder.AddRedisDistributedCache("cache");Service Configuration (Output Cache):
builder.AddRedisOutputCache("cache");Usage:
// IDistributedCache
var cache = serviceProvider.GetRequiredService<IDistributedCache>();
await cache.SetStringAsync("key", "value");
// IConnectionMultiplexer
var redis = serviceProvider.GetRequiredService<IConnectionMultiplexer>();
var db = redis.GetDatabase();
await db.StringSetAsync("key", "value");---
Valkey (Redis alternative)
Hosting Package: Aspire.Hosting.Valkey Client Package: Aspire.StackExchange.Redis
AppHost Configuration:
var valkey = builder.AddValkey("cache");Service Configuration:
builder.AddRedisClient("cache");---
Messaging Components
RabbitMQ
Hosting Package: Aspire.Hosting.RabbitMQ Client Package: Aspire.RabbitMQ.Client
AppHost Configuration:
var messaging = builder.AddRabbitMQ("messaging")
.WithManagementPlugin(); // Optional: Enable management UIService Configuration:
builder.AddRabbitMQClient("messaging");Usage:
var connectionFactory = serviceProvider.GetRequiredService<IConnectionFactory>();
using var connection = connectionFactory.CreateConnection();
using var channel = connection.CreateModel();
channel.QueueDeclare("orders", durable: true, exclusive: false);
channel.BasicPublish("", "orders", null, body);---
Azure Service Bus
Client Package: Aspire.Azure.Messaging.ServiceBus
AppHost Configuration:
var serviceBus = builder.AddAzureServiceBus("messaging");Service Configuration:
builder.AddAzureServiceBusClient("messaging");---
Kafka
Hosting Package: Aspire.Hosting.Kafka Client Package: Aspire.Confluent.Kafka
AppHost Configuration:
var kafka = builder.AddKafka("messaging")
.WithKafkaUI(); // Optional: Add Kafka UIService Configuration:
builder.AddKafkaProducer<string, string>("messaging");
builder.AddKafkaConsumer<string, string>("messaging", config => {
config.GroupId = "my-consumer-group";
});---
Storage Components
Azure Blob Storage
Client Package: Aspire.Azure.Storage.Blobs
AppHost Configuration:
var storage = builder.AddAzureStorage("storage")
.RunAsEmulator() // Use Azurite for local dev
.AddBlobs("blobs");Service Configuration:
builder.AddAzureBlobClient("blobs");Usage:
var blobServiceClient = serviceProvider.GetRequiredService<BlobServiceClient>();
var containerClient = blobServiceClient.GetBlobContainerClient("mycontainer");---
Azure Queue Storage
Client Package: Aspire.Azure.Storage.Queues
AppHost Configuration:
var storage = builder.AddAzureStorage("storage")
.RunAsEmulator()
.AddQueues("queues");Service Configuration:
builder.AddAzureQueueClient("queues");---
Azure Table Storage
Client Package: Aspire.Azure.Storage.Tables
AppHost Configuration:
var storage = builder.AddAzureStorage("storage")
.RunAsEmulator()
.AddTables("tables");Service Configuration:
builder.AddAzureTableClient("tables");---
Search Components
Elasticsearch
Hosting Package: Aspire.Hosting.Elasticsearch Client Package: Aspire.Elastic.Clients.Elasticsearch
AppHost Configuration:
var elasticsearch = builder.AddElasticsearch("search")
.WithDataVolume();Service Configuration:
builder.AddElasticsearchClient("search");---
Azure AI Search
Client Package: Aspire.Azure.Search.Documents
AppHost Configuration:
var search = builder.AddAzureSearch("search");Service Configuration:
builder.AddAzureSearchClient("search");---
Configuration and Secrets
Azure Key Vault
Client Package: Aspire.Azure.Security.KeyVault
AppHost Configuration:
var keyVault = builder.AddAzureKeyVault("keyvault");Service Configuration:
builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());---
Observability Components
Application Insights
Client Package: Aspire.Azure.ApplicationInsights
Service Configuration:
builder.Services.AddApplicationInsightsTelemetry();---
Email Components
MailKit (SMTP)
Hosting Package: Aspire.Hosting.MailDev Client Package: Aspire.MailKit
AppHost Configuration:
var mail = builder.AddMailDev("mail"); // Local SMTP server for devService Configuration:
builder.AddMailKitClient("mail");Usage:
var smtpClient = serviceProvider.GetRequiredService<ISmtpClient>();
await smtpClient.SendAsync(message);---
Identity and Authentication
Azure Active Directory
Client Package: Aspire.Azure.Identity
Service Configuration:
builder.Services.AddDefaultAzureCredential();---
Developer Tools
Seq (Logging)
Hosting Package: Aspire.Hosting.Seq
AppHost Configuration:
var seq = builder.AddSeq("seq");Services automatically send logs to Seq when configured.
---
Grafana + Prometheus
Hosting Package: Aspire.Hosting.Prometheus / Aspire.Hosting.Grafana
AppHost Configuration:
var prometheus = builder.AddPrometheus("prometheus");
var grafana = builder.AddGrafana("grafana")
.WithReference(prometheus);---
Component Configuration Patterns
Health Checks
Most components automatically register health checks:
builder.AddRedisClient("cache"); // Registers Redis health checkAccess at /health endpoint (when using ServiceDefaults).
Connection String Override
Override auto-generated connection strings:
var postgres = builder.AddPostgres("postgres")
.WithEnvironment("POSTGRES_PASSWORD", "custom-password");Persistence
Add volumes for data persistence:
var mongo = builder.AddMongoDB("mongodb")
.WithDataVolume(); // Persists data between restartsResource Limits
Set container resource limits:
var postgres = builder.AddPostgres("postgres")
.WithMemoryLimit(1024 * 1024 * 1024); // 1GB limitCustom Images
Use custom container images:
var redis = builder.AddRedis("cache")
.WithImage("redis/redis-stack", "latest");Component Selection Guide
When to Use Each Database
| Database | Use Case |
|---|---|
| PostgreSQL | General-purpose RDBMS, JSON support, full-text search |
| SQL Server | Microsoft stack, advanced analytics, reporting |
| MySQL | Web applications, read-heavy workloads |
| MongoDB | Document storage, flexible schemas, hierarchical data |
| Oracle | Enterprise applications, complex transactions |
When to Use Each Cache
| Cache | Use Case |
|---|---|
| Redis | Session storage, real-time analytics, pub/sub |
| Valkey | Redis alternative (open-source fork) |
| Output Cache | ASP.NET page/fragment caching |
When to Use Each Message Broker
| Broker | Use Case |
|---|---|
| RabbitMQ | Traditional messaging, task queues, routing |
| Kafka | Event streaming, log aggregation, high throughput |
| Azure Service Bus | Enterprise messaging, Azure integration |
Quick Reference: Install Commands
# Database
dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Aspire.Pomelo.EntityFrameworkCore.MySql
dotnet add package Aspire.MongoDB.Driver
# Caching
dotnet add package Aspire.StackExchange.Redis
dotnet add package Aspire.StackExchange.Redis.DistributedCaching
dotnet add package Aspire.StackExchange.Redis.OutputCaching
# Messaging
dotnet add package Aspire.RabbitMQ.Client
dotnet add package Aspire.Azure.Messaging.ServiceBus
dotnet add package Aspire.Confluent.Kafka
# Storage
dotnet add package Aspire.Azure.Storage.Blobs
dotnet add package Aspire.Azure.Storage.Queues
dotnet add package Aspire.Azure.Storage.Tables
# Search
dotnet add package Aspire.Elastic.Clients.Elasticsearch
dotnet add package Aspire.Azure.Search.Documents
# Other
dotnet add package Aspire.MailKit
dotnet add package Aspire.Azure.Security.KeyVaultAdditional Component Resources
For the latest component packages and versions:
- NuGet: Search for "Aspire" packages
- GitHub: https://github.com/dotnet/aspire
- Documentation: https://learn.microsoft.com/dotnet/aspire/fundamentals/components-overview
Configuration Management in .NET Aspire
This guide covers environment configuration, secrets management, and feature flags in .NET Aspire applications.
Configuration Fundamentals
How Configuration Works
Aspire uses standard .NET configuration hierarchy: 1. appsettings.json (default) 2. appsettings.{Environment}.json (environment-specific) 3. Environment variables 4. User secrets (development only) 5. Command-line arguments
AppHost automatically injects configuration through environment variables.
Development vs. Production
Development:
- Uses local
appsettings.Development.json - User secrets for sensitive data
- Aspire-managed resources (containers)
- Local dashboard for debugging
Production:
- Uses Azure Key Vault or managed secrets
- Connection strings injected by deployment system
- Managed Azure resources
- Remote telemetry (Application Insights, etc.)
Configuration in AppHost
Environment Variables
Pass environment variables from AppHost to services:
In AppHost Program.cs:
var builder = DistributedApplication.CreateBuilder(args);
var apiService = builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("LOG_LEVEL", "Information")
.WithEnvironment("CACHE_DURATION", "300")
.WithEnvironment("FEATURE_FLAG_NEW_PRICING", "true");
builder.Build().Run();Access in service Program.cs:
var builder = WebApplication.CreateBuilder(args);
var logLevel = builder.Configuration["LOG_LEVEL"]; // "Information"
var cacheDuration = int.Parse(builder.Configuration["CACHE_DURATION"]); // 300
var featureEnabled = bool.Parse(builder.Configuration["FEATURE_FLAG_NEW_PRICING"]); // trueResource Connection Strings
Aspire automatically injects connection strings for resources:
AppHost configuration:
var postgres = builder.AddPostgres("postgres")
.AddDatabase("appdb");
var redis = builder.AddRedis("cache");
var api = builder.AddProject<Projects.MyApi>("api")
.WithReference(postgres)
.WithReference(redis);Injected environment variables:
ConnectionStrings__postgres=Host=localhost;Database=postgres;Username=postgres;Password=...ConnectionStrings__appdb=Host=localhost;Database=appdb;Username=postgres;Password=...ConnectionStrings__cache=localhost:6379
Access in service:
// Using connection string directly
var dbConnection = builder.Configuration.GetConnectionString("postgres");
// Or using Aspire component method (recommended)
builder.AddNpgsqlDbContext<MyDbContext>("appdb");
builder.AddRedisClient("cache");Parameters for Secrets
Define secrets in AppHost that are injected at runtime:
var builder = DistributedApplication.CreateBuilder(args);
// Define secret parameter
var apiKey = builder.CreateResourceBuilder(new Parameter("api-key", secret: true))
.WithDefault("dev-key-12345"); // Default for local development
// Pass to services
var api = builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("API_KEY", apiKey);
var web = builder.AddProject<Projects.MyWeb>("web")
.WithEnvironment("API_KEY", apiKey)
.WithReference(api);
builder.Build().Run();Access in service:
var apiKey = builder.Configuration["API_KEY"];Configuration in Services
Reading Configuration
In Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Read specific values
var databaseHost = builder.Configuration["Database:Host"];
var cacheTimeout = builder.Configuration.GetValue<int>("Cache:Timeout", defaultValue: 300);
// Read sections
var databaseSection = builder.Configuration.GetSection("Database");
string host = databaseSection["Host"];
int port = databaseSection.GetValue<int>("Port");In controllers/services:
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
private readonly IConfiguration _configuration;
public ProductsController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet]
public IActionResult Get()
{
var pageSize = _configuration.GetValue<int>("Pagination:PageSize", 20);
return Ok(new { pageSize });
}
}Strongly Typed Configuration
Define configuration class:
public class CacheSettings
{
public int DurationSeconds { get; set; }
public string Mode { get; set; }
public bool Enabled { get; set; }
}
public class DatabaseSettings
{
public string Host { get; set; }
public int Port { get; set; }
public string Database { get; set; }
public int PoolSize { get; set; }
}
public class ApplicationSettings
{
public CacheSettings Cache { get; set; }
public DatabaseSettings Database { get; set; }
public string LogLevel { get; set; }
}Bind configuration in Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Option 1: Bind entire configuration
var applicationSettings = new ApplicationSettings();
builder.Configuration.Bind(applicationSettings);
builder.Services.AddSingleton(applicationSettings);
// Option 2: Use configuration option pattern (recommended)
builder.Services.Configure<CacheSettings>(builder.Configuration.GetSection("Cache"));
builder.Services.Configure<DatabaseSettings>(builder.Configuration.GetSection("Database"));Use in service:
public class ProductService
{
private readonly CacheSettings _cacheSettings;
public ProductService(IOptions<CacheSettings> cacheSettings)
{
_cacheSettings = cacheSettings.Value;
}
public void DoSomething()
{
if (_cacheSettings.Enabled)
{
var duration = TimeSpan.FromSeconds(_cacheSettings.DurationSeconds);
// Use cache with duration
}
}
}appsettings.json:
{
"Cache": {
"DurationSeconds": 300,
"Mode": "Distributed",
"Enabled": true
},
"Database": {
"Host": "localhost",
"Port": 5432,
"Database": "appdb",
"PoolSize": 10
},
"LogLevel": "Information"
}Secrets Management
User Secrets (Development)
Store sensitive development values outside source control:
Initialize user secrets:
cd MyService
dotnet user-secrets initSet secrets:
dotnet user-secrets set "Database:Password" "secure-dev-password"
dotnet user-secrets set "Api:Key" "dev-api-key-12345"List secrets:
dotnet user-secrets listClear secrets:
dotnet user-secrets clearSecrets are stored in %APPDATA%\Microsoft\UserSecrets\<user-secrets-id>\secrets.json (Windows) or ~/.microsoft/usersecrets/<user-secrets-id>/secrets.json (macOS/Linux).
Access in code:
var dbPassword = builder.Configuration["Database:Password"]; // From user secretsEnvironment-Specific Secrets
appsettings.Development.json (NOT in source control):
{
"Database": {
"Password": "dev-password"
},
"Api": {
"Key": "dev-api-key"
}
}Add to .gitignore:
appsettings.Development.json
appsettings.*.json # Exclude all environment-specific filesAzure Key Vault (Production)
Store production secrets in Azure Key Vault:
AppHost deployment configuration:
var builder = DistributedApplication.CreateBuilder(args);
// Reference Key Vault resource
var keyVault = builder.AddAzureKeyVault("keyvault");
// Services access secrets from Key Vault
var api = builder.AddProject<Projects.MyApi>("api")
// When deployed, connects to Key Vault automatically
.WithEnvironment("KEYVAULT_ENDPOINT", keyVault.GetProperty("endpoint"));
builder.Build().Run();Service configuration for Key Vault:
builder.Configuration.AddAzureKeyVault(
new Uri("https://mykeyvault.vault.azure.net/"),
new DefaultAzureCredential());Retrieve secrets:
var dbPassword = builder.Configuration["Database--Password"]; // Azure KV uses "--" separatorManaged Identities
In Azure, use managed identities instead of connection strings:
Enable managed identity for Container App:
az containerapp identity assign \
--name myapp \
--resource-group myapp-rg \
--system-assignedGrant Key Vault access:
az keyvault set-policy \
--name mykeyvault \
--object-id <managed-identity-id> \
--secret-permissions get listNo credentials needed; Azure handles authentication automatically.
Feature Flags
Simple Feature Flags
In AppHost:
var enableNewPricing = builder.CreateResourceBuilder(new Parameter("feature-new-pricing"))
.WithDefault("false");
var enableNewUI = builder.CreateResourceBuilder(new Parameter("feature-new-ui"))
.WithDefault("true");
var api = builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("FEATURES__NEW_PRICING", enableNewPricing)
.WithEnvironment("FEATURES__NEW_UI", enableNewUI);In configuration:
builder.Services.Configure<FeatureFlags>(
builder.Configuration.GetSection("Features"));appsettings.json:
{
"Features": {
"NewPricing": false,
"NewUI": true,
"BetaApi": false
}
}Feature flag class:
public class FeatureFlags
{
public bool NewPricing { get; set; }
public bool NewUI { get; set; }
public bool BetaApi { get; set; }
}Using Feature Flags in Code
In controller:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IOptions<FeatureFlags> _features;
public ProductsController(IOptions<FeatureFlags> features)
{
_features = features.Value;
}
[HttpPost]
public async Task<IActionResult> CreateProduct(CreateProductRequest request)
{
if (_features.NewPricing)
{
// Use new pricing engine
var pricing = new NewPricingEngine().Calculate(request);
}
else
{
// Use legacy pricing
var pricing = new LegacyPricingEngine().Calculate(request);
}
return Ok(pricing);
}
}Middleware for UI feature:
app.Use(async (context, next) =>
{
var features = context.RequestServices.GetRequiredService<IOptions<FeatureFlags>>();
if (features.Value.NewUI)
{
context.Items["UI_Version"] = "v2";
}
else
{
context.Items["UI_Version"] = "v1";
}
await next();
});Advanced Feature Flags with Launch Darkly
For more sophisticated feature management, integrate Launch Darkly:
Install NuGet package:
dotnet add package LaunchDarkly.ServerSdkConfigure in AppHost:
var launchDarklyKey = builder.CreateResourceBuilder(new Parameter("launchdarkly-key", secret: true))
.WithDefault("sdk-key-dev");
var api = builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("LAUNCHDARKLY_KEY", launchDarklyKey);Initialize in service:
builder.Services.AddSingleton<ILdClient>(sp =>
{
var key = sp.GetRequiredService<IConfiguration>()["LAUNCHDARKLY_KEY"];
return new LdClient(key);
});Use feature flag:
public class PricingService
{
private readonly ILdClient _ldClient;
public decimal CalculatePrice(int productId)
{
var user = LaunchDarkly.Sdk.User.WithKey("user123");
if (_ldClient.BoolVariation("new-pricing-enabled", user, false))
{
return CalculateNewPrice(productId);
}
else
{
return CalculateLegacyPrice(productId);
}
}
}Configuration Best Practices
Development Configuration
appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information"
}
},
"AllowedHosts": "*",
"Database": {
"Host": "localhost",
"Port": 5432
},
"Cache": {
"DurationSeconds": 60
}
}Production Configuration
Never commit production secrets or keys:
- Use Azure Key Vault
- Use environment variables
- Use managed identities
Minimal appsettings.Production.json:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning"
}
},
"AllowedHosts": ".example.com",
"Cache": {
"DurationSeconds": 3600
}
}Configuration Validation
Validate configuration at startup:
var builder = WebApplication.CreateBuilder(args);
// Validate configuration
builder.Services.AddOptions<DatabaseSettings>()
.BindConfiguration("Database")
.ValidateDataAnnotations()
.ValidateOnStart();
public class DatabaseSettings
{
[Required]
public string Host { get; set; }
[Range(1, 65535)]
public int Port { get; set; } = 5432;
[Required]
public string Database { get; set; }
}Configuration Reload
Reload configuration without restarting (for non-critical settings):
builder.Services.AddSingleton<IConfigurationRoot>(sp =>
builder.Configuration as IConfigurationRoot);
public class FeatureFlagService
{
private readonly IConfigurationRoot _configuration;
public FeatureFlagService(IConfigurationRoot configuration)
{
_configuration = configuration;
}
public bool IsFeatureEnabled(string feature)
{
// Reloads configuration each call (for development)
_configuration.Reload();
return _configuration.GetValue<bool>($"Features:{feature}", false);
}
}Common Patterns
Secrets as Environment Variables
AppHost:
var databasePassword = builder.CreateResourceBuilder(new Parameter("db-password", secret: true))
.WithDefault("dev-password");
var postgres = builder.AddPostgres("postgres", password: databasePassword)
.AddDatabase("appdb");Multi-Environment Support
AppHost detects environment from ASPNETCORE_ENVIRONMENT:
var environment = builder.Environment.IsProduction() ? "Production" : "Development";
builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("ASPNETCORE_ENVIRONMENT", environment);Service loads appropriate settings file automatically.
Configuration Override Hierarchy
Priority (highest to lowest): 1. Command-line arguments 2. Environment variables 3. User secrets (development) 4. appsettings.{Environment}.json 5. appsettings.json
Example:
# Command-line override wins
dotnet run --Database:Host=prod-db.example.com
# Or environment variable
ASPNETCORE_DATABASE__HOST=prod-db.example.com dotnet run
# Or appsettings.Production.jsonKeyed Configuration Sections
Group related settings:
{
"ConnectionStrings": {
"Primary": "Host=localhost;Database=primary",
"Secondary": "Host=localhost;Database=secondary"
},
"Logging": {
"Serilog": {
"MinimumLevel": "Information"
}
}
}Access:
var primaryConnection = builder.Configuration.GetConnectionString("Primary");
var logLevel = builder.Configuration.GetSection("Logging:Serilog:MinimumLevel").Value;Troubleshooting Configuration
Configuration Not Being Read
1. Verify file is named correctly (case-sensitive on Linux) 2. Check file is in project root 3. Ensure project includes it: <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> 4. Verify environment name matches filename
Secrets Not Injected
1. Check environment variable name in AppHost matches service code 2. Verify WithEnvironment() is called 3. Confirm secret parameter has .WithDefault() value 4. Review AppHost console output for environment variable names
Production Secrets Exposed
1. Check .gitignore includes appsettings.*.json and sensitive files 2. Review commit history: git log --all -S "password" 3. Never hardcode secrets in source 4. Use Azure Key Vault for production 5. Rotate compromised secrets immediately
References
Deploying .NET Aspire Applications
This guide covers deploying .NET Aspire applications from local development to production environments, including Azure Container Apps, Kubernetes, and other cloud platforms.
Deployment Overview
.NET Aspire applications are designed for cloud deployment with built-in support for:
- Container orchestration
- Service discovery and communication
- Configuration management
- Observability and telemetry
- Scaling and resilience
Deployment Targets
Azure Container Apps (Recommended)
Azure Container Apps provides native support for .NET Aspire with automatic provisioning of Azure resources.
Prerequisites
- Azure subscription
- Azure CLI or Azure Developer CLI (azd)
- Docker Desktop (for building images)
Using Azure Developer CLI (azd)
1. Initialize Azure Developer environment:
azd initSelect "Use code in the current directory" and choose your AppHost project.
2. Deploy to Azure:
azd upThis command:
- Provisions Azure resources (Container Apps Environment, Storage, etc.)
- Builds and pushes container images to Azure Container Registry
- Deploys services to Azure Container Apps
- Configures service connections and environment variables
3. Monitor deployment:
azd monitorOpens Application Insights for telemetry and logs.
4. Manage environment:
azd env list # List environments
azd env set <name> # Switch environment
azd down # Tear down resourcesManual Azure Deployment
1. Create Azure resources:
# Resource group
az group create --name myapp-rg --location eastus
# Container Apps environment
az containerapp env create \
--name myapp-env \
--resource-group myapp-rg \
--location eastus
# Container registry
az acr create \
--name myappregistry \
--resource-group myapp-rg \
--sku Basic \
--admin-enabled true2. Build and push images:
# Build service image
docker build -t myappregistry.azurecr.io/apiservice:latest ./MyApi
# Login to ACR
az acr login --name myappregistry
# Push image
docker push myappregistry.azurecr.io/apiservice:latest3. Deploy container apps:
az containerapp create \
--name apiservice \
--resource-group myapp-rg \
--environment myapp-env \
--image myappregistry.azurecr.io/apiservice:latest \
--target-port 8080 \
--ingress external \
--registry-server myappregistry.azurecr.io \
--registry-username <username> \
--registry-password <password>4. Configure service connections:
az containerapp update \
--name webapp \
--resource-group myapp-rg \
--set-env-vars "services__apiservice__http__0=https://apiservice.internal.eastus.azurecontainerapps.io"Azure Resource Provisioning
Aspire automatically provisions Azure resources when using azd:
AppHost configuration:
var builder = DistributedApplication.CreateBuilder(args);
// Provisions Azure SQL Database in production
var sqlServer = builder.AddAzureSqlServer("sql")
.AddDatabase("catalogdb");
// Provisions Azure Redis Cache in production
var cache = builder.AddAzureRedis("cache");
// Provisions Azure Service Bus in production
var messaging = builder.AddAzureServiceBus("messaging");
var apiService = builder.AddProject<Projects.Api>("apiservice")
.WithReference(sqlServer)
.WithReference(cache)
.WithReference(messaging);
builder.Build().Run();Deployment behavior:
- Local development: Uses containers (PostgreSQL, Redis, RabbitMQ)
- Azure deployment: Provisions managed Azure services (Azure SQL, Azure Cache for Redis, Azure Service Bus)
---
Kubernetes
Deploy Aspire apps to Kubernetes clusters (AKS, EKS, GKE, on-premises).
Generate Kubernetes Manifests
Using Aspire manifest:
dotnet run --project MyApp.AppHost -- --publisher manifest --output-path ./aspire-manifest.jsonConvert to Kubernetes YAML:
# Using aspirate tool
dotnet tool install -g aspirate
aspirate generate --aspire-manifest ./aspire-manifest.json --output-path ./k8sThis generates Kubernetes manifests including:
- Deployments
- Services
- ConfigMaps
- Secrets
- Ingress
Deploy to Kubernetes
1. Apply manifests:
kubectl apply -f ./k8s2. Verify deployment:
kubectl get pods
kubectl get services
kubectl get ingress3. Configure service discovery:
Aspire uses Kubernetes DNS for service discovery. Update service URLs:
env:
- name: services__apiservice__http__0
value: "http://apiservice.default.svc.cluster.local"Azure Kubernetes Service (AKS)
1. Create AKS cluster:
az aks create \
--resource-group myapp-rg \
--name myapp-cluster \
--node-count 3 \
--enable-addons monitoring \
--generate-ssh-keys2. Get credentials:
az aks get-credentials --resource-group myapp-rg --name myapp-cluster3. Deploy:
kubectl apply -f ./k8s4. Enable Application Insights:
az aks enable-addons \
--resource-group myapp-rg \
--name myapp-cluster \
--addons monitoring \
--workspace-resource-id <log-analytics-workspace-id>---
Docker Compose
For simple deployments or on-premises hosting.
Generate Docker Compose
Using Aspire manifest:
dotnet run --project MyApp.AppHost -- --publisher manifest --output-path ./aspire-manifest.jsonConvert to Docker Compose:
# Manual conversion or use third-party tools
# Example docker-compose.yml:version: '3.8'
services:
apiservice:
build: ./MyApi
ports:
- "8080:8080"
environment:
- ConnectionStrings__catalogdb=Server=sqlserver;Database=catalogdb;User=sa;Password=YourPassword!
- ConnectionStrings__cache=cache:6379
depends_on:
- sqlserver
- cache
webapp:
build: ./MyWebApp
ports:
- "8000:8080"
environment:
- services__apiservice__http__0=http://apiservice:8080
depends_on:
- apiservice
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourPassword!
volumes:
- sqldata:/var/opt/mssql
cache:
image: redis:latest
volumes:
- redisdata:/data
volumes:
sqldata:
redisdata:Deploy with Docker Compose
docker-compose up -d---
Cloud Run (Google Cloud)
Deploy individual services to Cloud Run.
1. Build and push images:
gcloud builds submit --tag gcr.io/PROJECT_ID/apiservice ./MyApi2. Deploy service:
gcloud run deploy apiservice \
--image gcr.io/PROJECT_ID/apiservice \
--platform managed \
--region us-central1 \
--allow-unauthenticated3. Configure service-to-service auth:
gcloud run services add-iam-policy-binding apiservice \
--member="serviceAccount:webapp@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.invoker"---
AWS (ECS / App Runner)
Deploy to AWS Elastic Container Service or App Runner.
AWS App Runner
1. Create ECR repository:
aws ecr create-repository --repository-name apiservice2. Build and push:
aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
docker build -t apiservice ./MyApi
docker tag apiservice:latest <account>.dkr.ecr.us-east-1.amazonaws.com/apiservice:latest
docker push <account>.dkr.ecr.us-east-1.amazonaws.com/apiservice:latest3. Create App Runner service:
aws apprunner create-service \
--service-name apiservice \
--source-configuration "ImageRepository={ImageIdentifier=<account>.dkr.ecr.us-east-1.amazonaws.com/apiservice:latest,ImageRepositoryType=ECR}"---
Deployment Configurations
Environment-Specific Settings
appsettings.Production.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}Environment variables in deployment:
# Azure Container Apps
az containerapp update \
--name apiservice \
--set-env-vars "ASPNETCORE_ENVIRONMENT=Production"
# Kubernetes
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"Connection String Management
Azure Key Vault:
builder.Configuration.AddAzureKeyVault(
new Uri("https://mykeyvault.vault.azure.net/"),
new DefaultAzureCredential());Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: connection-strings
type: Opaque
stringData:
catalogdb: "Server=..."
cache: "cache:6379"Reference in deployment:
env:
- name: ConnectionStrings__catalogdb
valueFrom:
secretKeyRef:
name: connection-strings
key: catalogdbService Discovery
Azure Container Apps: Service discovery is automatic within the Container Apps environment. Use internal FQDNs:
https://<service-name>.internal.<region>.azurecontainerapps.ioKubernetes: Use cluster DNS:
http://<service-name>.<namespace>.svc.cluster.localConfigure in service:
builder.Services.AddHttpClient("apiservice", client =>
{
var serviceUrl = builder.Configuration["services:apiservice:http:0"]
?? "http://apiservice";
client.BaseAddress = new Uri(serviceUrl);
});---
Scaling and Performance
Horizontal Scaling
Azure Container Apps:
az containerapp update \
--name apiservice \
--min-replicas 2 \
--max-replicas 10 \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-metadata concurrentRequests=100Kubernetes:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: apiservice-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: apiservice
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70Resource Limits
Kubernetes:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"Azure Container Apps:
az containerapp update \
--name apiservice \
--cpu 0.5 \
--memory 1.0Gi---
Observability in Production
Application Insights
Configure in service:
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
});Azure Container Apps (automatic):
az containerapp update \
--name apiservice \
--enable-dapr \
--dapr-app-id apiserviceDistributed Tracing
OpenTelemetry is configured by ServiceDefaults. Export to:
- Application Insights (Azure)
- Jaeger (self-hosted)
- Zipkin (self-hosted)
- Datadog, New Relic, etc.
Configure OTLP exporter:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
}));Logging
Configure structured logging:
builder.Logging.AddJsonConsole();Azure Container Apps: Logs automatically flow to Log Analytics
Kubernetes: Use Fluentd/Fluent Bit to ship logs to centralized storage
---
Security Best Practices
HTTPS/TLS
Azure Container Apps: Automatic HTTPS with managed certificates.
Kubernetes:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: apiservice-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- api.example.com
secretName: api-tlsSecrets Management
- Use Azure Key Vault, AWS Secrets Manager, or GCP Secret Manager
- Never commit secrets to source control
- Use managed identities for cloud resources
- Rotate secrets regularly
Network Security
Azure Container Apps:
- Enable network isolation
- Use Virtual Network integration
- Configure network security groups
Kubernetes:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: apiservice-policy
spec:
podSelector:
matchLabels:
app: apiservice
ingress:
- from:
- podSelector:
matchLabels:
app: webapp---
CI/CD Pipelines
GitHub Actions (Azure)
name: Deploy to Azure
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Install azd
run: curl -fsSL https://aka.ms/install-azd.sh | bash
- name: Azure Login
run: |
azd auth login \
--client-id "${{ secrets.AZURE_CLIENT_ID }}" \
--client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" \
--tenant-id "${{ secrets.AZURE_TENANT_ID }}"
- name: Deploy
run: azd up --no-prompt
env:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_ENV_NAME: productionAzure DevOps
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- script: |
curl -fsSL https://aka.ms/install-azd.sh | bash
displayName: 'Install azd'
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-Connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
azd up --no-prompt
env:
AZURE_ENV_NAME: production---
Troubleshooting Production Issues
Service Not Starting
- Check container logs:
kubectl logs <pod>or Azure Portal - Verify environment variables and connection strings
- Ensure container has proper resource limits
- Check health check endpoints
Service Discovery Failing
- Verify service names match between AppHost and deployment
- Check DNS resolution:
nslookup <service-name> - Ensure network policies allow communication
- Review service mesh configuration if applicable
Performance Issues
- Enable Application Insights or OpenTelemetry
- Review resource utilization (CPU, memory)
- Check database connection pooling
- Analyze distributed traces for bottlenecks
- Consider horizontal scaling
Database Connection Failures
- Verify connection strings
- Check firewall rules
- Ensure managed identity permissions (Azure)
- Review network security groups
- Test connection from container:
kubectl exec -it <pod> -- /bin/bash
---
Deployment Checklist
Before deploying to production:
- [ ] Environment variables configured for production
- [ ] Connection strings stored in secrets/key vault
- [ ] HTTPS/TLS enabled
- [ ] Health checks configured
- [ ] Logging and telemetry enabled
- [ ] Resource limits and scaling rules defined
- [ ] Database migrations tested
- [ ] Backup and disaster recovery plan in place
- [ ] Monitoring and alerting configured
- [ ] Security scanning completed
- [ ] Load testing performed
- [ ] Rollback plan documented
---
Additional Resources
Local Development with .NET Aspire
This guide covers using the Aspire dashboard, debugging, testing, and health checks for local development.
Aspire Dashboard Overview
Starting the Dashboard
Launch AppHost to start dashboard:
cd MyApp.AppHost
dotnet runDashboard opens automatically at: https://localhost:15001
If not automatic, check the AppHost console output for the URL.
Dashboard Features
Resources View
Shows all running services and resources:
- Status indicator (green = healthy, red = failed, yellow = degraded)
- Resource type (Project, Container, Azure resource, etc.)
- Port information
- Endpoints for direct access
Interact with resources:
- Click service name to view logs
- Click endpoint URL to open service
- View resource configuration
Projects Section
Lists all services (AppHost projects):
- Current health status
- Replica count if running multiple instances
- Resource consumption (memory, CPU)
- Restart options
Actions:
- Click project to view detailed logs
- View environment variables
- Monitor real-time metrics
Containers Section
Shows infrastructure resources:
- PostgreSQL databases
- Redis caches
- RabbitMQ message brokers
- etc.
Container info:
- Container ID and image
- Port mappings
- Volume mounts
- Environment variables (visible for debugging)
Traces Tab
Distributed tracing across services:
- View request flow through microservices
- See latency at each step
- Identify bottlenecks
- Export traces for analysis
Trace details:
- HTTP request/response
- Database queries
- Service-to-service calls
- Execution time breakdown
Logs Tab
Real-time and historical logs from all services:
- Filter by service, level, or keyword
- Search for specific entries
- Export logs
- Tail live logs
Log levels:
- Trace (most verbose)
- Debug
- Information
- Warning
- Error
- Critical
Metrics Tab
Operational metrics:
- Request rate
- Error rate
- Latency percentiles (p50, p95, p99)
- Resource utilization
- Custom application metrics
Local Development Workflow
1. Start AppHost
dotnet run --project MyApp.AppHostWhat starts:
- All configured services (APIs, web apps, workers)
- All infrastructure resources (databases, caches, messaging)
- Developer dashboard
- OpenTelemetry collection
2. Monitor Dashboard
Open dashboard and verify:
- [ ] All services showing green "running" status
- [ ] All resources started successfully
- [ ] No error logs in initial startup
3. Test Service Communication
From dashboard: 1. Click on web service endpoint 2. Verify page loads 3. Click API service endpoint 4. Test API endpoint
Or use curl:
curl https://localhost:7001/api/products4. Debug Issues via Dashboard
Check logs: 1. Click service in Projects section 2. Review logs for errors 3. Search for specific keywords 4. Check timestamps for correlation
Check traces: 1. Make a request through the UI 2. Navigate to Traces tab 3. Find your request 4. Expand to see service calls and timing
Monitor metrics: 1. Go to Metrics tab 2. Watch request rate and error rate 3. Check p95/p99 latencies 4. Look for resource pressure
Debugging Services
Enable Debug Logging
In development configuration:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.EntityFrameworkCore": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}Or via environment variable in AppHost:
var api = builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("Logging__LogLevel__Default", "Debug");Attach Debugger in Visual Studio
Set breakpoint in service code
Start AppHost with debugger:
dotnet run --project MyApp.AppHostAttach to service process: 1. In Visual Studio: Debug → Attach to Process 2. Find the service process (MyApp.Api) 3. Click Attach 4. Trigger the code path in dashboard 5. Breakpoint activates
Remote Debugging
For services running in containers:
AppHost configuration for debugging:
var postgres = builder.AddPostgres("postgres")
.WithEnvironment("POSTGRES_INITDB_ARGS", "--log-statement=all");View container logs via dashboard → Containers → PostgreSQL → Logs
Console Output
View service output directly:
# AppHost shows all service logs in console
dotnet run --project MyApp.AppHostLook for:
- Service startup messages
- Configuration validation output
- Database connection info
- Port assignments
Health Checks
Enable Health Checks
ServiceDefaults automatically configures health checks.
Health check endpoint: GET /health
In AppHost:
var api = builder.AddProject<Projects.MyApi>("api");
// Check service health
var health = api.GetProperty("health.status"); // In dashboardCustom Health Checks
Define health check:
public class DatabaseHealthCheck : IHealthCheck
{
private readonly IDbConnection _connection;
public DatabaseHealthCheck(IDbConnection connection)
{
_connection = connection;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
await _connection.OpenAsync(cancellationToken);
return HealthCheckResult.Healthy("Database connection successful");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy($"Database connection failed: {ex.Message}");
}
}
}Register in Program.cs:
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("database");View in dashboard:
- Projects → Select service → Health section shows all checks
- Red indicator if any check fails
Liveness and Readiness Probes
For Kubernetes deployments, distinguish between:
Liveness (restart if unhealthy):
builder.Services.AddHealthChecks()
.AddCheck("live", () => HealthCheckResult.Healthy())
.PublishHealthCheck("live");Readiness (stop receiving traffic):
builder.Services.AddHealthChecks()
.AddCheck("ready", async () =>
{
var db = /* ... */;
var canConnect = await db.CanConnectAsync();
return canConnect
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Database unavailable");
});Performance Testing
Load Testing Tools
K6 (JavaScript-based)
Install:
# macOS
brew install k6
# Linux
sudo apt-get install k6
# Windows
choco install k6Create test script (load-test.js):
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.get('https://localhost:7001/api/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}Run test:
k6 run load-test.jsView results in dashboard metrics.
Apache Benchmark (ab)
Simple HTTP load test:
# 1000 requests, 10 concurrent
ab -n 1000 -c 10 https://localhost:7001/api/productsDatabase Query Performance
Enable query logging:
For PostgreSQL in AppHost:
var postgres = builder.AddPostgres("postgres")
.WithEnvironment("POSTGRES_INITDB_ARGS", "--log-statement=all");View slow queries: 1. Dashboard → Containers → PostgreSQL → Logs 2. Search for slow queries 3. Check execution time
In service, use Entity Framework logging:
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseNpgsql(sp.GetRequiredService<IConfiguration>().GetConnectionString("appdb"))
.LogTo(Console.WriteLine, LogLevel.Information);
});Integration Testing
Test Against Local Services
Use AppHost in tests:
[TestClass]
public class ProductApiTests
{
private static DistributedApplication _app;
private static string _apiUrl;
[ClassInitialize]
public static async Task Initialize(TestContext context)
{
var builder = DistributedApplication.CreateBuilder();
var postgres = builder.AddPostgres("postgres")
.AddDatabase("testdb");
var api = builder.AddProject<Projects.MyApi>("api")
.WithReference(postgres)
.WithHttpEndpoint(name: "http", scheme: "http");
_app = builder.Build();
await _app.StartAsync();
// Get the API endpoint
var httpResource = _app.Resources
.OfType<ProjectResource>()
.First(p => p.Name == "api");
var endpoint = httpResource.GetEndpoint("http");
_apiUrl = $"http://{endpoint.Address}:{endpoint.Port}";
}
[ClassCleanup]
public static async Task Cleanup()
{
await _app.StopAsync();
}
[TestMethod]
public async Task GetProducts_Returns200()
{
var client = new HttpClient();
var response = await client.GetAsync($"{_apiUrl}/api/products");
Assert.AreEqual(StatusCode.OK, response.StatusCode);
}
}Container Resource Testing
Start only required resources:
var postgres = builder.AddPostgres("postgres", password: "testpass");
var appdb = postgres.AddDatabase("testdb");
var api = builder.AddProject<Projects.MyApi>("api")
.WithReference(appdb);Clean up after tests:
# Stop AppHost and clean containers
# Or use test fixtures to handle cleanupMock External Services
For services outside AppHost:
var mockExternalApi = builder.AddContainer("external-api", "mockserver")
.WithHttpEndpoint(port: 8080, targetPort: 8080)
.WithEnvironment("MOCK_DEFINITION_PATH", "/config");
var api = builder.AddProject<Projects.MyApi>("api")
.WithEnvironment("ExternalApi:Url", "http://external-api:8080")
.WithReference(mockExternalApi);Logging Configuration
Structured Logging
Use Serilog for rich logging:
dotnet add package Serilog
dotnet add package Serilog.AspNetCoreIn Program.cs:
builder.Host.UseSerilog((ctx, lc) =>
{
lc.WriteTo.Console()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.Enrich.WithProperty("Service", "MyApi");
});Log with context:
using (LogContext.PushProperty("UserId", userId))
{
_logger.Information("User accessed resource");
// All logs in this scope include UserId
}Log to File (Development)
Configuration:
lc.WriteTo.File(
"logs/app-.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
);View logs:
tail -f logs/app-20240101.txtCustom Log Filtering
Filter logs by service:
var logsForApi = dashboardLogs
.Where(l => l.Service == "api")
.Where(l => l.Level >= LogLevel.Warning);Performance Profiling
CPU Profiling
Use dotTrace or built-in tools:
# With dotTrace
dotrace start --app-profiling-interval=1ms
# Use application
dotrace stopOr enable Diagnostic Events:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("MyApi"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
});Memory Profiling
Track memory usage: 1. Dashboard → Projects → Select service 2. View memory graph 3. Watch for memory leaks during operation
Or use dotMemory for detailed analysis
Persistence for Development
Data Persistence
Keep database data between AppHost runs:
var postgres = builder.AddPostgres("postgres")
.WithDataVolume() // Persists data
.AddDatabase("appdb");Without persistence:
var postgres = builder.AddPostgres("postgres")
// Database lost when AppHost stops
.AddDatabase("appdb");Named Volumes
Explicit volume management:
var postgres = builder.AddPostgres("postgres")
.WithDataVolume("postgres-data") // Named volume
.AddDatabase("appdb");Clean up old volumes:
docker volume ls
docker volume rm postgres-dataNetwork Debugging
Port Conflicts
Check if ports are in use:
# Windows
netstat -ano | findstr :5432
# macOS/Linux
lsof -i :5432AppHost shows port assignments in console:
service started: listening on http://localhost:5000DNS Resolution
Test service discovery:
# From within container
docker exec -it container-name nslookup servicename
# Or
ping servicename # Should resolve in Docker networkNetwork Policy Issues
Verify services can communicate: 1. Start AppHost 2. Open service logs in dashboard 3. Look for connection errors 4. Check Docker network: docker network ls
Environment-Specific Development
Different Development Scenarios
Minimal setup (API + database only):
var postgres = builder.AddPostgres("postgres").AddDatabase("appdb");
var api = builder.AddProject<Projects.MyApi>("api")
.WithReference(postgres);
// Skip web app, workers, etc.Full system (all services):
// Everything from AppHost Program.csFeature Development
Test only changed feature:
// Disable unchanged services
// Keep dependencies needed for changes
var postgres = builder.AddPostgres("postgres").AddDatabase("appdb");
// Only the service being developed
var api = builder.AddProject<Projects.MyApi>("api")
.WithReference(postgres)
.WithEnvironment("FEATURE_NEW_API", "true"); // Enable featureTeam Development
Shared AppHost configuration:
// appsettings.json in AppHost for team defaults
// Team members override locally as neededLocal overrides:
// appsettings.Development.json (in .gitignore)
// Team members customize for their machineTroubleshooting Local Development
Dashboard Won't Connect
Check AppHost is running:
ps aux | grep "dotnet run"Verify port 15001 is not blocked:
netstat -an | grep 15001Restart AppHost:
# Stop existing process
# Start fresh
dotnet run --project MyApp.AppHostServices Can't Connect to Each Other
Check service names match:
- AppHost:
AddProject<Projects.MyApi>("api") - Service HttpClient:
new Uri("http://api")
Verify health checks pass:
- Dashboard shows green indicators
- Click service for health details
Check Docker network:
docker network inspect bridge # Or the network nameDatabase Connection Fails
Verify database is running: 1. Dashboard → Containers → PostgreSQL (or other DB) 2. Check status and logs
Test connection string:
psql -h localhost -U postgres -d appdbReset database:
# AppHost will recreate with next run
# Or clean Docker volume:
docker volume rm postgres-dataHigh Memory Usage
Check for leaks: 1. Dashboard → Metrics tab 2. Watch memory graph over time 3. Should stabilize after initial spike
Reduce memory if testing:
var postgres = builder.AddPostgres("postgres")
.WithMemoryLimit(512 * 1024 * 1024); // 512 MBBest Practices
1. Use dashboard daily - Monitor all services and identify issues early 2. Check logs first - Most issues visible in logs tab 3. Enable structured logging - Makes debugging easier 4. Test locally before pushing - Catch integration issues early 5. Use health checks - Know when services are healthy 6. Monitor metrics - Watch for performance degradation 7. Clean up volumes - Remove old data between tests 8. Document local setup - Help new team members
References
Service-to-Service Communication in .NET Aspire
This guide covers service discovery, inter-service HTTP communication, and resilience patterns in .NET Aspire applications.
Service Discovery
How It Works
When services are added to AppHost, Aspire automatically: 1. Assigns internal names (e.g., "api", "web") 2. Registers services in a service discovery mechanism 3. Injects connection information as environment variables 4. Configures service discovery clients
Services communicate using short service names instead of hardcoded URLs.
Configuration in AppHost
Define services with unique names:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.MyApi>("api");
var web = builder.AddProject<Projects.MyWeb>("web")
.WithReference(api); // web depends on api
builder.Build().Run();The name parameter ("api", "web") becomes the service discovery identifier.
HTTP Client Communication
Basic Service-to-Service Calls
Create HttpClient with service name:
// Program.cs in the service that calls another service
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("http://api");
});Use in a controller or service:
public class OrderService
{
private readonly IHttpClientFactory _httpClientFactory;
public OrderService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<ProductDto> GetProductAsync(int productId)
{
var httpClient = _httpClientFactory.CreateClient("api");
var response = await httpClient.GetAsync($"/products/{productId}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ProductDto>(json);
}
}Typed HttpClients
For better type safety and testability, use typed clients:
Define client interface:
public interface IProductApiClient
{
Task<ProductDto> GetProductAsync(int productId);
Task<IEnumerable<ProductDto>> GetProductsAsync();
}
public class ProductApiClient : IProductApiClient
{
private readonly HttpClient _httpClient;
public ProductApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ProductDto> GetProductAsync(int productId)
{
var response = await _httpClient.GetAsync($"/products/{productId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<ProductDto>();
}
public async Task<IEnumerable<ProductDto>> GetProductsAsync()
{
var response = await _httpClient.GetAsync("/products");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<IEnumerable<ProductDto>>();
}
}Register typed client:
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("http://api");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});Inject and use:
public class OrderController
{
private readonly IProductApiClient _productApiClient;
public OrderController(IProductApiClient productApiClient)
{
_productApiClient = productApiClient;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _productApiClient.GetProductAsync(id);
return Ok(product);
}
}Service Discovery with Resilience
Add Resilience Policies
ServiceDefaults automatically includes resilience policies via Microsoft.Extensions.Http.Resilience.
Services with ServiceDefaults already include:
- Retry policy (exponential backoff)
- Circuit breaker
- Timeout policy
- Hedging for safe operations
Custom resilience configuration:
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("http://api");
})
.AddStandardResilienceHandler(options =>
{
// Customize resilience policies
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.FailureRatio = 0.5;
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});Health Checks for Service Communication
Verify dependent services are healthy before making calls:
Check service health:
public class ServiceHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
public ServiceHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var httpClient = _httpClientFactory.CreateClient("api");
try
{
var response = await httpClient.GetAsync("/health", cancellationToken);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Service returned unhealthy status");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy($"Service unavailable: {ex.Message}");
}
}
}
// Register in Program.cs
builder.Services.AddHealthChecks()
.AddCheck<ServiceHealthCheck>("api-health");GRPC Service Communication
For higher-performance service-to-service communication, use gRPC.
Define gRPC Service
Create proto file (Services/Orders.proto):
syntax = "proto3";
package Orders;
service OrderService {
rpc GetOrder(GetOrderRequest) returns (OrderResponse);
}
message GetOrderRequest {
int32 id = 1;
}
message OrderResponse {
int32 id = 1;
string customer = 2;
double total = 3;
}Generate code and implement service:
public class OrderGrpcService : OrderService.OrderServiceBase
{
private readonly IOrderRepository _orderRepository;
public OrderGrpcService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public override async Task<OrderResponse> GetOrder(GetOrderRequest request, ServerCallContext context)
{
var order = await _orderRepository.GetOrderAsync(request.Id);
return new OrderResponse
{
Id = order.Id,
Customer = order.Customer,
Total = order.Total
};
}
}Register in Program.cs:
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<OrderGrpcService>();Call gRPC Service
Create gRPC client:
public interface IOrderGrpcClient
{
Task<OrderResponse> GetOrderAsync(int orderId);
}
public class OrderGrpcClient : IOrderGrpcClient
{
private readonly OrderService.OrderServiceClient _client;
public OrderGrpcClient(OrderService.OrderServiceClient client)
{
_client = client;
}
public async Task<OrderResponse> GetOrderAsync(int orderId)
{
var request = new GetOrderRequest { Id = orderId };
return await _client.GetOrderAsync(request);
}
}Register client:
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(client =>
{
client.Address = new Uri("http://orders"); // Service name from AppHost
});
builder.Services.AddScoped<IOrderGrpcClient, OrderGrpcClient>();Add gRPC to AppHost
Add gRPC endpoint for service that exposes gRPC:
var orderService = builder.AddProject<Projects.OrderService>("orders")
.WithHttpEndpoint(scheme: "http", port: 5000, targetPort: 5000)
.WithHttpsEndpoint(scheme: "https", port: 5001, targetPort: 5001);Service-to-Service Authentication
For secure service communication, use authentication between services.
Service Identity with Certificates
AppHost configuration:
var api = builder.AddProject<Projects.Api>("api")
.WithHttpsEndpoint(scheme: "https");
var web = builder.AddProject<Projects.Web>("web")
.WithReference(api);Aspire automatically manages certificates for local development.
API Key Authentication
Web service calls API with key:
Define API key in AppHost:
var apiKey = builder.CreateResourceBuilder(new Parameter("api-key", secret: true))
.WithDefault("default-dev-key");
var api = builder.AddProject<Projects.Api>("api")
.WithEnvironment("API_KEY", apiKey);
var web = builder.AddProject<Projects.Web>("web")
.WithReference(api);Retrieve and use in service:
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("http://api");
})
.ConfigureHttpClient((serviceProvider, client) =>
{
var apiKey = serviceProvider.GetRequiredService<IConfiguration>()["API_KEY"];
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
});Service-to-Service Bearer Tokens
For services that need to authenticate with each other:
AppHost passes token:
var token = builder.CreateResourceBuilder(new Parameter("service-token", secret: true))
.WithDefault("eyJhbGc..."); // JWT token
var api = builder.AddProject<Projects.Api>("api")
.WithEnvironment("SERVICE_TOKEN", token);HttpClient includes bearer token:
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("http://api");
})
.ConfigureHttpClient((serviceProvider, client) =>
{
var token = serviceProvider.GetRequiredService<IConfiguration>()["SERVICE_TOKEN"];
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
});Error Handling in Service Communication
Graceful Degradation
Handle service unavailability gracefully:
public class ResilientProductService
{
private readonly IProductApiClient _productApiClient;
private readonly ILogger<ResilientProductService> _logger;
public ResilientProductService(IProductApiClient productApiClient, ILogger<ResilientProductService> logger)
{
_productApiClient = productApiClient;
_logger = logger;
}
public async Task<ProductDto> GetProductSafeAsync(int productId)
{
try
{
return await _productApiClient.GetProductAsync(productId);
}
catch (HttpRequestException ex)
{
_logger.LogWarning($"Product API unavailable: {ex.Message}");
// Return cached data or default
return new ProductDto { Id = productId, Name = "Unavailable", Price = 0 };
}
}
}Circuit Breaker Pattern
Already included in ServiceDefaults via resilience policies:
builder.Services.AddHttpClient<IProductApiClient, ProductApiClient>(client =>
{
client.BaseAddress = new Uri("http://api");
})
.AddStandardResilienceHandler(options =>
{
options.CircuitBreaker.SamplingDurationInSeconds = 30;
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 5;
});Retry with Exponential Backoff
Configured by default, customize as needed:
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 5;
options.Retry.BackoffType = BackoffType.ExponentialBackoff;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.UseJitter = true;
});Observability in Service Communication
Distributed Tracing
Aspire automatically traces HTTP calls between services via OpenTelemetry.
View traces in dashboard: 1. Run AppHost: dotnet run --project AppHost 2. Open dashboard: https://localhost:15001 3. Navigate to "Traces" tab 4. Select a request to see full trace across services
Structured Logging
Log service calls with structured data:
public class LoggingProductApiClient : IProductApiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<LoggingProductApiClient> _logger;
public LoggingProductApiClient(HttpClient httpClient, ILogger<LoggingProductApiClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<ProductDto> GetProductAsync(int productId)
{
using var activity = new System.Diagnostics.Activity("GetProduct").Start();
activity?.SetTag("product.id", productId);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var response = await _httpClient.GetAsync($"/products/{productId}");
response.EnsureSuccessStatusCode();
stopwatch.Stop();
_logger.LogInformation("Retrieved product {ProductId} in {ElapsedMs}ms", productId, stopwatch.ElapsedMilliseconds);
return await response.Content.ReadAsAsync<ProductDto>();
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Failed to retrieve product {ProductId} after {ElapsedMs}ms", productId, stopwatch.ElapsedMilliseconds);
throw;
}
}
}Metrics
Export metrics about service communication:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddHttpClientInstrumentation();
metrics.AddAspNetCoreInstrumentation();
});View in Prometheus dashboard (if configured in AppHost).
Troubleshooting Service Communication
Service Discovery Not Working
Problem: HttpClient gets connection refused
Solution: 1. Verify service name matches AppHost definition 2. Check service is running in dashboard 3. Confirm ServiceDefaults is referenced and AddServiceDefaults() called 4. Ensure MapDefaultEndpoints() called in ASP.NET services
Timeout Issues
Problem: Service calls timeout frequently
Solution: 1. Increase timeout: .AddStandardResilienceHandler(options => options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60)) 2. Check service performance in dashboard 3. Review logs for service errors 4. Consider reducing retry attempts if connection is too slow
Authentication Failures
Problem: 401/403 errors on service-to-service calls
Solution: 1. Verify bearer token is set correctly in HttpClient 2. Check token expiration 3. Confirm API key environment variable is set in AppHost 4. Review service logs for authentication errors
High Latency
Problem: Service communication is slow
Solution: 1. Use gRPC for performance-critical calls 2. Implement caching to reduce calls 3. Use connection pooling (already configured) 4. Monitor network latency in dashboard traces 5. Consider using service mesh (in Kubernetes) for advanced routing
Common Patterns
Fan-Out Pattern
One service calls multiple services in parallel:
public class OrderService
{
private readonly IProductApiClient _productClient;
private readonly IInventoryApiClient _inventoryClient;
private readonly IShippingApiClient _shippingClient;
public async Task<OrderDto> CreateOrderAsync(CreateOrderRequest request)
{
// Call multiple services in parallel
var productTask = _productClient.GetProductAsync(request.ProductId);
var inventoryTask = _inventoryClient.CheckInventoryAsync(request.ProductId, request.Quantity);
var shippingTask = _shippingClient.CalculateShippingAsync(request.Address);
await Task.WhenAll(productTask, inventoryTask, shippingTask);
// Combine results
var product = productTask.Result;
var inventory = inventoryTask.Result;
var shipping = shippingTask.Result;
return new OrderDto { /* ... */ };
}
}Cache-Aside Pattern
Cache service responses to reduce calls:
public class CachedProductApiClient : IProductApiClient
{
private readonly IProductApiClient _innerClient;
private readonly IDistributedCache _cache;
public async Task<ProductDto> GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
var cached = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cached))
{
return JsonSerializer.Deserialize<ProductDto>(cached);
}
var product = await _innerClient.GetProductAsync(productId);
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });
return product;
}
}Circuit Breaker with Fallback
Already handled by resilience policies, but explicit example:
public class RobustProductApiClient
{
private readonly IProductApiClient _client;
private readonly IProductCache _cache;
private readonly ILogger _logger;
public async Task<ProductDto> GetProductAsync(int productId)
{
try
{
return await _client.GetProductAsync(productId);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
{
_logger.LogWarning("Product service unavailable, using cache");
var cached = await _cache.GetAsync(productId);
return cached ?? new ProductDto { Id = productId, Name = "Unavailable" };
}
}
}