The .NET Aspire series continues with integrations. We’ll add SQL Server and Redis integrations and show how Aspire handles it for us.
This is the fourth part of a six-part exploratory series on .NET Aspire.
Hello again, friends.
In the first three installments of this .NET Aspire series, we did the following:
Today we’ll turn infrastructure into C# code. We’ll grab my hacked-together SQL Server container out of Docker Compose, drop it into Aspire and speed up reads with a Redis cache. Both resources spin up, wire up and light up in the dashboard. No port-mapping gymnastics required.
Enough talk! Bring on the code.
My SQL Server container works in Docker Compose but it’s a second-class citizen. By inviting it to Aspire, we get:
AppHost
, one launch profile and one CI pipeline.docker compose up
step: Hit F5 (or dotnet run
) and SQL starts with everything else.To get this party started, let’s navigate to our AppHost
project and update Program.cs
. After you review the code, we’ll walk through it in detail.
var builder = DistributedApplication.CreateBuilder(args);
var password = builder.AddParameter("password", secret: true);
var server = builder.AddSqlServer("server", password, 1433)
.WithDataVolume("guitar-data")
.WithLifetime(ContainerLifetime.Persistent);
var db = server.AddDatabase("guitardb");
var api = builder.AddProject<Projects.Api>("api")
.WithReference(db)
.WaitFor(db);
// The following code is the same as before
builder.AddProject<Projects.Frontend>("frontend")
.WithReference(api)
.WaitFor(api)
.WithExternalHttpEndpoints();
builder.Build().Run();
In this example:
var password = builder.AddParameter("password", secret: true);
AddParameter
creates a parameter resource. When we set the secret
parameter to true
, we tell Aspire to treat the value like a secret and makes it easy to override with an environment variable or user secrets during local development.
In our secrets.json
(or appsettings.json
), I read it in using the following structure:
{
"Parameters": {
"password": "Dave#123"
}
}
Obviously, this is only suitable for local development. When deploying to servers, you’ll want a better security model like Azure Key Vault. In that case, you can update your code to the following:
var keyVault = builder.AddAzureKeyVault("key‑vault");
builder.Configuration.AddAzureKeyVaultSecrets("key‑vault");
var sqlPassword = builder.AddParameter("sql-password", secret: true);
For this example:
var server = builder.AddSqlServer("server", password, 1433)
.WithDataVolume("guitar-data")
.WithLifetime(ContainerLifetime.Persistent);
What just happened?
AddSqlServer
pulls the official mcr.microsoft.com/mssql/server:2022-latest
image, sets SA_PASSWORD
to our password, and maps it to port 1433.WithDataVolume("guitar-data")
mounts a named Docker volume so our container survives rebuilds.WithLifetime(ContainerLifetime.Persistent)
tells Aspire to reuse the same instance between runs. This avoids slow startup times and having to run migrations every time.This one-liner …
var db = server.AddDatabase("guitardb");
… is syntactic sugar for writing a CREATE DATABASE guitardb
command when we first launch, and also generates a strongly typed connection string resource. That connection string can now be referenced by any project that needs it.
We’ve added the database, but it’s empty and we need to seed it. Starting with Aspire 9.2, you can use the WithCreationScript
method.
With a script (or set of scripts) in hand, make sure you’re copying it to the AppHost
output directory by adding this to your project file:
<ItemGroup>
<None Include="..\Api\data\createdb.sql" Link="createdb.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
We can now update the AppHost’s Program.cs
database code to the following.
var password = builder.AddParameter("password", secret: true);
var server = builder.AddSqlServer("server", password, 1433)
.WithDataVolume("guitar-data")
.WithLifetime(ContainerLifetime.Persistent);
var scriptPath = Path.Join(Path.GetDirectoryName(typeof(Program).Assembly.Location), "createdb.sql");
var db = server.AddDatabase("guitardb").WithCreationScript(File.ReadAllText(scriptPath));
This is just one of many ways you can see data. To learn more, read the Microsoft Docs.
Finally, this code registers the API to the database.
var api = builder.AddProject<Projects.Api>("api")
.WithReference(db)
.WaitFor(db);
The WithReference(db)
injects two things into the container:
ConnectionStrings__guitardb
The WaitFor(db)
line means Aspire won’t even docker run
the API until SQL Server reports it as healthy.
Because we already rely on Service Defaults, the API can pick up builder.Configuration.GetConnectionString("db")
automatically. The only code we need to touch is making sure EF Core is registered to use the database in the API project’s Program.cs
:
builder.Services.AddSqlServer<GuitarStoreContext>("guitardb");
That single line allows the AppHost
project to spin up the SQL Server container and inject its connection string into the API at launch. Service Defaults then wires up logging, tracing, metrics and resilience. As a result, the AddSqlServer<GuitarStoreContext>("guitardb")
call grabs the injected connection string, registers the DbContext
and plugs into our observability out of the box.
When we fire up the app, we now see the database up and running alongside our app.
If we review the details of the API resource, we see the injected connection string, too.
Our telemetry lights up: if we browse to our traces, we can see a request end-to-end.
Our database is officially out of Docker Compose and into Aspire! We can now delete the old docker-compose.yml
and use SQL Server alongside the rest of our project with a single F5 command (or dotnet run
).
We can now move onto our Redis integration to speed up our reads.
Our /guitars
endpoint is read-heavy. To make it lightning-fast (and to avoid round-trips to the database), we’ll add Redis as a cache layer and let Aspire wire things up.
To wire Redis in AppHost
, we’ll add this right after the SQL snippet.
var cache = builder.AddRedis("cache")
.WithRedisInsight()
.WithLifetime(ContainerLifetime.Persistent);
What happens here? We pull a Redis container from docker.io/library/redis
that helps to avoid any local installs or port wrangling. We also get a PING
health probe that is viewable from the /health
endpoint and the developer dashboard.
Then, the connection string ConnectionStrings:cache
gets injected automatically into any project that calls WithReference(cache)
.
Optionally, we can also include our favorite Redis admin UI, as Aspire supports Redis Insight or Redis Commander. I’ve added Redis Insight. When I do so, Aspire grabs docker.io/redis/redisinsight
to allow me to easily work with my Redis instance.
Much like with our SQL Server container, we add .WithLifetime(ContainerLifetime.Persistent)
to tell Aspire, “Don’t tear down the container when I stop debugging.” The result is much faster start-ups and durable state between sessions—great for day-to-day development, demos or long-running background jobs. If you ever need a clean slate, you can still prune the container manually or switch the lifetime back to the default ContainerLifetime.Transient
mode.
Here’s my entire Program.cs
in AppHost
:
var builder = DistributedApplication.CreateBuilder(args);
var password = builder.AddParameter("password", secret: true);
var server = builder.AddSqlServer("server", password, 1433)
.WithDataVolume("guitar-data")
.WithLifetime(ContainerLifetime.Persistent);
var db = server.AddDatabase("guitardb");
var cache = builder.AddRedis("cache")
.WithRedisInsight()
.WithLifetime(ContainerLifetime.Persistent);
var api = builder.AddProject<Projects.Api>("api")
.WithReference(db)
.WithReference(cache)
.WaitFor(db);
builder.AddProject<Projects.Frontend>("frontend")
.WithReference(api)
.WaitFor(api)
.WithExternalHttpEndpoints();
builder.Build().Run();
How do we update the API to use Redis? Let’s sprinkle some minimal caching over the GET /guitars
endpoint. After installing the StackExchange.Redis
package, we can add a decorator to use output caching.
app.UseOutputCache();
app.MapGet("/guitars",
[OutputCache(Duration = 30)] async (GuitarStoreContext db) =>
await db.Guitars
.OrderBy(g => g.Brand).ThenBy(g => g.Model)
.Select(g => new { g.Id, g.Brand, g.Model, g.Series, g.Price, g.SalePrice })
.ToListAsync());
The output caching middleware can now intercept requests after Aspire registers the Redis store. For testing, we are using a 30-second TTL, but once we go live we can adjust it through configuration.
Let’s now run the solution and open the dashboard. Our resources keep building up.
If we browse to our Redis Insight URL, we can see the cache!
When we fire off some calls, we now see an end-to-end request completing in under 3.43ms. Not bad!
With SQL Server and Redis in play, our dashboard now shows a dependency graph pointing to both SQL and Redis nodes.
Before Aspire (and Docker containers), can you imagine all the work getting a new developer access to a local environment with multiple APIs, logging, a SQL server and a Redis instance?
Now, here’s all we need to do:
git clone https://github.com/davesguitarshop
cd inventory
dotnet run --project GuitarShop.AppHost
With one single command we restore NuGet packages, build projects, start our SQL Server and Redis containers, launch our dashboard and open a browser tab at https://localhost:5001
. No SQL installations, no docker compose up
and no “What ports do we need to use again?” What a world.
While developer happiness is huge, the real payoff comes when we push up to the cloud—to Azure Container Apps, for example. Because the same AppHost
definition travels with us, the pipeline only needs:
az containerapp up \
--name guitar-shop \
--resource-group guitar-shop-rg \
--subscription guitar-shop-prd
As a result, Aspire’s Azure Container App extension will map:
AddSqlServer
- Azure SQL (or bring your own image)AddRedis
- Azure Cache for Redis (or your own image)We will have much more in Part 6—stay tuned!
We just replaced a huge docker-compose.yml
, a half‑dozen environment‑variable gymnastics and a README full of “run this before that” commands with just a few lines of C#—all discoverable from IntelliSense. Are you not entertained?
Key takeaways:
AddSqlServer
and AddRedis
, Aspire handles images, ports and health.In Part 5, we’ll dive into service discovery and orchestration—how Aspire lets services talk to each other without hard‑coding URLs and how Polly policies keep things resilient when the network gets mad.
Until then, happy coding and see you soon!
Dave Brock is a software engineer, writer, speaker, open-source contributor and former Microsoft MVP. With a focus on Microsoft technologies, Dave enjoys advocating for modern and sustainable cloud-based solutions.