Aspire Components — Integrating SQL, Redis, and Messaging
Use .NET Aspire components for database, cache, and messaging integration: Aspire SQL Server, Redis, RabbitMQ, and Azure Service Bus components — health checks, connection resilience, and telemetry included.
What Aspire Components Provide
An Aspire component wraps a third-party library (EF Core, StackExchange.Redis, etc.)
and adds:
→ Health checks (ready/live endpoints automatically updated)
→ OpenTelemetry traces and metrics (SQL queries, cache hits, message processing)
→ Resilience (retry on transient failures — configurable)
→ Standardised configuration (reads from IConfiguration with known key prefix)
Without Aspire components, you configure each of these separately.
With Aspire components, one line of registration handles all four.
Component = the library integration, not the infrastructure provisioning.
AppHost provisions infrastructure (runs SQL Server in Docker).
Component connects to it with production-grade configuration.SQL Server Component (EF Core)
// AppHost: provision SQL Server and create database
var sqlServer = builder.AddSqlServer("sqlserver")
.AddDatabase("clinical-db");
var prescriptionService = builder.AddProject<Projects.PrescriptionService>("prescription-service")
.WithReference(sqlServer); // injects ConnectionStrings__clinical-db
// PrescriptionService/Program.cs:
// NuGet: Aspire.Microsoft.EntityFrameworkCore.SqlServer
builder.AddSqlServerDbContext<PrescriptionsDbContext>(
connectionName: "clinical-db",
configureSettings: settings =>
{
settings.CommandTimeout = 30;
// Connection resilience: retry on transient SQL errors
settings.DbContextPooling = true; // enable DbContext pooling
},
configureDbContextOptions: options =>
{
options.EnableSensitiveDataLogging(builder.Environment.IsDevelopment());
});
// What this gives you automatically:
// → Health check: /health/ready includes SQL Server connectivity check
// → OpenTelemetry: all SQL queries appear as spans in the trace
// → Connection resilience: retry on transient Azure SQL errors
// → DbContext pool: configurable pool size for high concurrencyRedis Component
// AppHost: add Redis container
var redis = builder.AddRedis("redis");
var prescriptionService = builder.AddProject<Projects.PrescriptionService>("prescription-service")
.WithReference(redis);
// PrescriptionService/Program.cs:
// NuGet: Aspire.StackExchange.Redis.DistributedCaching
builder.AddRedisDistributedCache("redis",
configureSettings: settings =>
{
settings.ConnectTimeout = 3000; // 3 seconds
settings.AbortOnConnectFail = false; // degrade gracefully if Redis is down
});
// Now IDistributedCache is Redis-backed:
public sealed class PrescriptionCacheService
{
private readonly IDistributedCache _cache;
public async Task<Prescription?> GetOrLoadAsync(
PrescriptionId id, CancellationToken ct)
{
var cacheKey = $"prescription:{id.Value}";
var cached = await _cache.GetAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Prescription>(cached);
var prescription = await _repository.GetByIdAsync(id, ct);
if (prescription is not null)
{
await _cache.SetAsync(cacheKey,
JsonSerializer.SerializeToUtf8Bytes(prescription),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
}, ct);
}
return prescription;
}
}
// Health check added automatically: Redis connectivity included in /health/ready
// OpenTelemetry: cache hits/misses visible as spansRabbitMQ Component
// AppHost: add RabbitMQ container for local development
var messaging = builder.AddRabbitMQ("messaging");
var prescriptionService = builder.AddProject<Projects.PrescriptionService>("prescription-service")
.WithReference(messaging);
// PrescriptionService/Program.cs:
// NuGet: Aspire.RabbitMQ.Client
builder.AddRabbitMQClient("messaging");
// Use IConnection or IModel from DI:
public sealed class RabbitMqEventPublisher : IIntegrationEventPublisher
{
private readonly IConnection _connection;
public async Task PublishAsync<T>(T @event, CancellationToken ct) where T : class
{
using var channel = _connection.CreateModel();
channel.ExchangeDeclare("clinical.events", ExchangeType.Topic, durable: true);
var body = JsonSerializer.SerializeToUtf8Bytes(@event);
var properties = channel.CreateBasicProperties();
properties.Persistent = true;
properties.ContentType = "application/json";
properties.CorrelationId = Activity.Current?.TraceId.ToString();
channel.BasicPublish(
exchange: "clinical.events",
routingKey: typeof(T).Name.ToLowerInvariant(),
basicProperties: properties,
body: body);
}
}Azure Service Bus Component
// AppHost: for local development, use RabbitMQ or the Service Bus emulator
// For production targeting Azure Service Bus:
// AppHost:
var serviceBus = builder.AddAzureServiceBus("servicebus")
.AddTopic("prescription-events")
.AddSubscription("prescription-events", "pharmacy");
var pharmacyService = builder.AddProject<Projects.PharmacyService>("pharmacy-service")
.WithReference(serviceBus);
// PharmacyService/Program.cs:
// NuGet: Aspire.Azure.Messaging.ServiceBus
builder.AddAzureServiceBusClient("servicebus");
// Resolved from DI:
public sealed class PharmacyDispenseConsumer : BackgroundService
{
private readonly ServiceBusClient _client;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var processor = _client.CreateProcessor(
topicName: "prescription-events",
subscriptionName: "pharmacy",
new ServiceBusProcessorOptions { AutoCompleteMessages = false });
processor.ProcessMessageAsync += HandlePrescriptionEventAsync;
processor.ProcessErrorAsync += OnErrorAsync;
await processor.StartProcessingAsync(stoppingToken);
await Task.Delay(Timeout.Infinite, stoppingToken);
}
}Component Health Checks
// All Aspire components register health checks automatically.
// Customise the health check setup if needed:
builder.Services.AddHealthChecks()
.AddCheck("custom-domain-check", () =>
{
// Custom logic — e.g., check that required clinical data is loaded
var wardCount = WardRegistry.Count;
return wardCount > 0
? HealthCheckResult.Healthy($"Ward registry: {wardCount} wards loaded")
: HealthCheckResult.Unhealthy("Ward registry is empty — data not loaded");
}, tags: new[] { "ready" });
// Default Aspire health check endpoints:
app.MapDefaultEndpoints();
// Maps:
// GET /health/live → liveness (process is up)
// GET /health/ready → readiness (all dependencies available, ready for traffic)
// GET /alive → alias for liveness (used by some orchestrators)Production issue I've seen: A team's prescription service had a Redis connection that failed silently on startup — the connection string was misconfigured but the error was swallowed. The service started, reported healthy, and received traffic. On every cache miss, a
RedisConnectionExceptionwas thrown, caught, logged, and the service fell back to the database. But with 500 concurrent users and no cache, every request hit the database. The database became the bottleneck and response times degraded to 4 seconds. Adding the Aspire Redis component would have surfaced this immediately: the/health/readyendpoint would have returned unhealthy until Redis was reachable, and the deployment would have been blocked until the configuration was fixed.
Key Takeaway
Aspire components wrap third-party integrations (EF Core, Redis, RabbitMQ, Service Bus) with standardised health checks, OpenTelemetry instrumentation, and resilience — one
AddXxxcall instead of configuring each separately. The AppHost provisions infrastructure locally; components connect to it with production-grade configuration. Health checks added by components flow to/health/readyautomatically — failing dependencies prevent the service from receiving traffic. Custom business health checks (ward registry loaded, critical data present) can be added alongside component health checks.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.