PostgreSQL in .NET Aspire: From Container to DbContext
PostgreSQL is a popular choice for .NET applications, and Aspire provides a smooth path from declaring a database container in your AppHost to injecting a fully configured DbContext into your services. No manual connection string management required.
Declaring PostgreSQL in the AppHost
Start by adding PostgreSQL to your AppHost:
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin();
var catalogDb = postgres.AddDatabase("catalogdb");
var api = builder.AddProject<Projects.CatalogApi>("catalog-api")
.WithReference(catalogDb)
.WaitFor(postgres);
builder.Build().Run();
This declares a PostgreSQL server with a database named catalogdb. The .WithPgAdmin() call adds a pgAdmin container alongside PostgreSQL, giving you a web-based database management UI during development.
The WaitFor ensures the API does not start until PostgreSQL is accepting connections.
Configuring the PostgreSQL Container
You can customise the container for specific needs:
var postgres = builder.AddPostgres("postgres")
.WithImageTag("16")
.WithDataVolume("postgres-data")
.WithEnvironment("POSTGRES_INITDB_ARGS", "--encoding=UTF8");
The WithDataVolume call persists data between container restarts, which is useful when you do not want to re-seed your database every time you restart the AppHost.
Consuming PostgreSQL with Npgsql
On the service side, install the Aspire Npgsql component:
dotnet add package Aspire.Npgsql
Register the data source:
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddNpgsqlDataSource("catalogdb");
You can now inject NpgsqlDataSource for raw ADO.NET access:
public class ProductRepository
{
private readonly NpgsqlDataSource _dataSource;
public ProductRepository(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
}
public async Task<List<Product>> GetAllAsync()
{
await using var conn = await _dataSource.OpenConnectionAsync();
await using var cmd = new NpgsqlCommand(
"SELECT id, name, price FROM products", conn);
await using var reader = await cmd.ExecuteReaderAsync();
var products = new List<Product>();
while (await reader.ReadAsync())
{
products.Add(new Product
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Price = reader.GetDecimal(2)
});
}
return products;
}
}
Using Entity Framework Core
Most applications prefer Entity Framework Core over raw SQL. Install the Aspire EF Core component:
dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
Register your DbContext:
builder.AddNpgsqlDbContext<CatalogContext>("catalogdb");
Your CatalogContext is a standard EF Core context:
public class CatalogContext : DbContext
{
public CatalogContext(DbContextOptions<CatalogContext> options)
: base(options) { }
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
}
Inject and use it as usual:
app.MapGet("/products", async (CatalogContext db) =>
await db.Products
.Include(p => p.Category)
.ToListAsync());
app.MapGet("/products/{id}", async (int id, CatalogContext db) =>
await db.Products.FindAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
Running Migrations
For EF Core migrations, a common Aspire pattern is to run them at startup using a migration service:
public class MigrationWorker : BackgroundService
{
private readonly IServiceProvider _services;
private readonly ILogger<MigrationWorker> _logger;
public MigrationWorker(IServiceProvider services, ILogger<MigrationWorker> logger)
{
_services = services;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<CatalogContext>();
_logger.LogInformation("Applying migrations...");
await context.Database.MigrateAsync(stoppingToken);
_logger.LogInformation("Migrations applied successfully");
}
}
Register it in your service:
builder.Services.AddHostedService<MigrationWorker>();
Health Checks and Telemetry
The Aspire PostgreSQL components register health checks and Npgsql telemetry automatically. You will see database query spans in the Aspire dashboard traces, complete with command text and duration.
The health check verifies that the database is reachable. If PostgreSQL goes down, the /health endpoint reports unhealthy and the dashboard shows the degraded state.
Configuration Options
Fine-tune the EF Core integration through configuration:
builder.AddNpgsqlDbContext<CatalogContext>("catalogdb", settings =>
{
settings.DisableRetry = false;
settings.CommandTimeout = 30;
});
Or configure the underlying Npgsql connection:
builder.AddNpgsqlDbContext<CatalogContext>("catalogdb",
configureDataSourceBuilder: dataSource =>
{
dataSource.EnableDynamicJson();
});
From container to DbContext, Aspire eliminates the friction of working with PostgreSQL in .NET. The AppHost manages the container lifecycle, the component wires up the connection, and you focus on writing queries.