I recently spun up a “Hello World” Aspire application, mostly to see what all the fuss was about. This wasn’t a deep dive — just enough to get a sense of what it’s like to develop and deploy with Aspire.

As expected with anything new, there were some bumps along the way, a few surprises, and some interesting architectural choices that I’m still chewing on.

Initial Stumbling Blocks

Running Locally with HTTPS

The first issue I encountered was running the app locally over HTTPS. The default Aspire project expects HTTPS out of the box, so when I ran the application, it failed due to a lack of trusted development certificates. To fix this, I had to open a Developer Command Prompt and run the following command:

dotnet dev-certs https --trust

After doing that, the application ran as expected under https://localhost.

First-Time ClickOps Publish Woes

I decided to try publishing straight from Visual Studio — yes, I went full ClickOps on this one. The first attempt failed without much insight into why, but the second attempt worked. I honestly do not know why. I spent a few minutes trying to figure out where the error was coming from but then just thought to smash Publish again and it worked. I wish I would have saved the error message.

First impressions

Bicep CDK

During the process, I noticed that Aspire initialized the “Bicep provider,” which suggests that Aspire is using Bicep templates to provision the necessary Azure infrastructure. This is extremely similar to how Terraform CDK works. Essentially you have C# compile down into Terraform JSON (no not even HCL). Technically HCL can compile down into the JSON too but who wants to author that way?

Anyways, the Aspire approach is similar to a CDK approach in that you use your application programming language (C#, Python, whatever) and essentially configure your infrastructure. A big difference between traditional CDK is the lack of control of the infrastructure you configure. There is a high level of abstraction put on the resources you provision (I think by design). However, they do poke holes in this abstraction to allow you to configure some things. I have not explored how deep this rabbit hole goes but I suspect the level of configurability is going to be extremely low compared with working in Bicep directly — it almost has to be — unless, they expose the ability actually configure ARM Schema in C# but…dear God, why? I suppose that isn’t much different then the actual CDK approach — but thats why I stay away from CDK. I conciously don’t want to use an application programming language to describe infrastructure. I don’t trust myself not to wrap it into insane levels of C# abstractions until I can’t figure out what the crap its doing. Infrastructure configuration needs to be simple. Thats why DSLs are preferrable.

But there is something about Aspire’s approach that is reminiscent of Terraform. Unlike Bicep or the AzAPI provider that exposes the infrastructure developer to the raw ARM schema — Aspire is smoothing the contours of the Azure platform. They are applying an opinionated layer that makes things simple and easy and allowing the user to unwrap that abstraction if they so choose. This is essentially what the azurerm Terraform provider does — it makes Azure Resources human readable by abstracting away much of the complexity —flattening unnecessarily verbose object structures, specifying sane defaults for the most common scenarios and renaming things so they make more sense and have a bit more consistency. It’s funny, these are all the same principals that I love about Terraform and the azurerm provider and its what makes Aspire great. Who would have thought! Aspire and Terraform are rather odd bedfellows, indeed!

Infrastructure Naming Conventions

When prompted for an environment name, I used “dev,” and Aspire created a resource group named rg-dev. I appreciate this kind of naming convention—makes it easy to know what’s what, and frankly, it soothes my OCD. However, it is rather simplistic compared to my two part naming convention I typically use with Terraform. I suppose I can just concatenate application_name and environment_name and create a single name and think about it that way.

What’s in the Box

The initial deployment provisioned a Log Analytics workspace, an Azure Container Registry, a container app environment, and two container apps based on the Aspire Hello World template: a “Web Frontend” and an “API Service.” So far, so good.

Let’s Talk about the .NET

One of the first things I noticed in the generated code was the absence of traditional controllers. Instead, everything is wired up directly in Program.cs, like this:

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

I see the intent here — reduce the ceremony, streamline the code — but it’s a bit jarring at first. It feels like everything is expected to live in one giant Program.cs. I’m not sure if the idea is to create multiple microservices, each with its own isolated Program file, or to just pile on endpoints into one monolithic entry point. Either way, it’s not clear yet how this scales for larger applications.

Then there was this record:

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

I initially mistook this for some sort of Entity Framework model, but it’s just a simple C# record. Neat, but also a bit unexpected if you’re coming from more conventional MVC-style projects.

Adding Azure Storage to the Mix

Next, I tried adding Azure Storage integration. I started by running:

dotnet add package Aspire.Hosting.Azure.Storage

I mistakenly added this package to the ApiService project when it should have gone into the AppHost. Once corrected, I used the AddAzureStorage extension method in AppHost/Program.cs to configure a storage account:

var tables = builder.AddAzureStorage("storage")
    .AddTables("tables");

It appears you can only attach one storage type (Tables, Blobs, or Queues) per call to AddAzureStorage. So if you want both tables and blobs, you’re looking at provisioning multiple storage accounts. Not ideal, but manageable.

Running the publish process again deployed the new storage account to my rg-dev resource group—so, success. Running the ClickOps publish process again to rg-dev (yes, I’ll change it later). I now have a storage account!

Wiring Up Table Storage in the API Service

To consume the storage in the API service, I had to install the same Aspire package in the ApiService project: dotnet add package Aspire.Hosting.Azure.Storage Then, I registered the Azure Table client in the service’s Program.cs:

builder.AddAzureTableClient("tables");

Here, “tables” refers back to the named table service in the AppHost. If I wanted to have different services with their own storage accounts, the model seems flexible enough.

For instance:

AppHost/Program.cs:

var tables = builder.AddAzureStorage("storage")
    .AddTables("tenants");

var tables = builder.AddAzureStorage("storage")
    .AddTables("users");

Then in each service:

TenantService/Program.cs

builder.AddAzureTableClient("tenants");

UserService/Program.cs

builder.AddAzureTableClient("users");

This setup assumes the strings are magic identifiers that link to specific resource declarations in the AppHost. It seems logical, though I haven’t fully tested this scenario yet.

Architectural Considerations and Constraints

One area that gives me pause is the tight coupling between infrastructure and application code. Aspire encourages putting everything — services, configuration, infrastructure — into a single solution. This means one codebase, one solution file, one resource group, and a unified deployment model. It’s like they took Terraform CDK, dropped it into .NET and layered on an application model.

While this works for composable monoliths or tightly integrated apps, it’s not ideal for how I usually build microservices. I prefer to isolate infrastructure per service to create clear blast radius boundaries and simplify RBAC for operations. Aspire’s centralized dashboard and solution structure imply a different architectural philosophy than I am usually accustomed to — but one not too foreign for this OG .NET developer who’s used to seeing *.sln files with 100+ projects in them.

A couple of things I am going to be looking for are:

  1. Authentication / Authorization between services — a critical aspect of an application model and one where there is a significant amount of friction and toil today. I hope to strap Azure AD B2C to this thing.
  2. Synchronous Service Integration — generating C# client libraries and automatically injecting them into the client APIs would be a huge win as this is another place where there is significant toil in auto-generating client libraries and distributing them to all the dependent services (plus managing the dependency graph).
  3. Async Service Integration — I wanna see if I can build event-driven systems easier using Azure Storage Queues, EventGrid, or ServiceBus. Managing event models and pub-sub across my microservices.

Next Steps

I’m planning to build out a sample app using Aspire. The target use case is a consumer-facing, ad-supported application — lots of users, little revenue per user— so keeping operational costs near zero is critical. Aspire’s out-of-the-box provisioning and streamlined setup might help me get there, but I’ll be watching closely to see how flexible it is as the app grows.