.NET Aspire — Aspiring for Sanity in Cloud Development

Marek Sirkovský
11 min readJun 16, 2024

Have you heard about the new superhero in the .NET world — Aspire? .NET Aspire is an ambitious project that aims to provide a developer-friendly way of building modern, cloud-based distributed applications.

Disclaimer: I won’t provide a step-by-step guide since Microsoft did a great job writing clear documentation. I will only show you a few exciting concepts and where I think Aspire failed to deliver its promises. If you haven’t read the .NET Aspire overview, do it before continuing with my post.

Is Aspire considered experimental?

If you’ve watched Microsoft Build 2024, you know .NET Aspire has moved beyond its experimental phase and is now available for general use.

Aspire, which originated from a real experiment called Tye, represents the next iteration of this project. Tye was a playground for the .NET team to experiment with new ideas and concepts. Now retired, Tye ultimately led to the development of Aspire.

Note: To work with Aspire, you must download the latest Visual Studio 2022 or JetBrains Rider.

What is Aspire?

It’s a bit tricky to wrap one’s head around what Aspire really is. Aspire promises to facilitate working with services and cloud resources in local environments. You may say we already have various tools such as containers, docker-compose, Rancher, Docker Desktop, and many more for this task. You’re right, but Aspire goes one step further to enhance the developer experience. Aspire claims all you need is your IDE, C#, and docker runtime running in the background (e.g., Docker Desktop). All of this is enough to run and debug your multi-service application and easily consume external services like Redis or databases on your local machine.

What does Aspire bring to the table?

Guardrails

When you want to simplify something complex, you must introduce guardrails to prevent certain mistakes and help developers do complex things faster with a smaller codebase or steps.
Aspire has chosen the same approach. Aspire is a highly opinionated framework with advanced tooling. Aspire bets on containers, Open Telemetry, and extensions for IDEs (Visual Studio 2022 or JetBrains Rider).
Aspire introduces patterns and limitations to make developing cloud-native applications easier to begin and maintain. However, as you will see later in this blog post, you pay for this with flexibility.

NuGet packages

Aspire’s first building block is components, which are just NuGet packages. Aspire components are designed to integrate your application with various cloud services and platforms. Microsoft has chosen several services for Aspire, including Azure services and open-source projects. Microsoft already prepared components for SQL Server, RabbitMQ, and Kafka. You can see the complete list here.

Tooling

Components are useful, but they alone are not enough. Tooling is what makes Aspire so interesting. The tooling comprises a set of command-line tools or IDE extensions that assist in working with Aspire components. Currently, it supports Visual Studio 2022, JetBrains Rider, and .NET CLI.

The tooling covers the following areas:

  • Orchestrating multiple services
  • Running and debugging your .NET applications
  • Distributing shared settings via environment variables
  • Starting and stopping Docker containers for Cloud resources (Redis, SQL Server, etc.)
  • Monitoring and tracing your application

Pretty cool, right? Nevertheless, there are certain aspects in which Aspire falls short of its promises.

Let’s start with deployment.

Deployment

Aspire is not only about local development. It’s also about deployment.
You can run dotnet build with a special target to generate the Aspire Manifest.

An example of Aspire manifest:

{
"resources": {
"api": {
"type": "project.v0",
"path": "../WebApplication1/WebApplication1.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http"
},
"https": {
"scheme": "https",
"protocol": "tcp",
"transport": "http"
}
}
},
"eventhubs": {
"type": "azure.bicep.v0",
"path": "eventhub.bicep",
"parameters": {
"eventHubNamespaceName": "mynamespace",
"principalId": "",
"principalType": "",
"eventHubs": ["hub1"]
}
}
}
}

This example shows a manifest with two resources: a .NET project and an Azure Event Hub configuration.

The manifest is an intermediate format created for Aspire. Third-party deployment tools can use the Aspire Manifest to deploy applications to various cloud platforms.

Does it sound too good to be true?

Yes, it does, so let’s see what Aspire overpromises and underdelivers.

Support is poor

Today, you can deploy your Aspire application to Azure Container Apps and Kubernetes (via Aspir8). Native support for other cloud services (Amazon, Google Cloud) is currently almost non-existent, but it may grow over time.

Customization of deployment

However, even if you’re okay with using Azure Container Apps or Kubernetes, there are other issues. One of them is customization.

When you open an example of Aspire projects, you will see something like this:

var builder = DistributedApplication.CreateBuilder(args);

var sql = builder.AddSqlServer("sql")
.WithDataVolume()
.AddDatabase("sqldata");
builder
.AddProject<Projects.AspireApp007_ApiService>("apiservice")
.WithReference(sql);
builder.Build().Run();

This code runs your application locally as a normal .NET API project, with the SQL database running as a container in a local Docker runtime. But can you guess what happens when you run the following command?

azd up

The Azd CLI is a command-line interface from Microsoft Azure that allows developers to manage and automate Azure services.

The code above will automatically create several new Azure resources in your subscription:

Running azd up creates a new resource group and deploys multiple cloud resources. However, the purchasing model used for the SQL database remains unclear.

I couldn’t find any information about that in the documentation, so I inspected the container properties.

You can see that azd chose to use 0.5 CPU cores and 1Gi memory size. It’s probably a reasonable default setting for testing or local development. But if you need to update it to the production version, you must use the following command:

azd infra synth

That creates several Yaml files you can customize to adjust your cloud resource settings. For example, if you want the container for SQL to use more memory, you need to update the {name of the resource}.tmpl.yaml file like this:

...
containers:
- image: {{ .Image }}
name: sql
# add these three lines
resources:
cpu: "1" # Set the CPU limit (e.g., 1 CPU)
memory: "2Gi" # Set the memory limit (e.g., 2 GiB)
...

If you ask me, this process seems a bit cumbersome, but on the bright side, the Aspire team claims that you’ll be able to configure it using C# in the future.

Local orchestrator

Aspire local orchestrator is meant for the development environment only. This means that developers use the Aspire orchestrator for local development but Kubernetes for production. That seems like the recommended approach at this time.

Let me illustrate the issue with an example. The Aspire Manifest doesn’t reflect the replica settings, which means that any deployment tool effectively ignores WithReplicas line.

var apiService = builder
.AddProject<Projects.AspireApp007_ApiService>("apiservice")
.WithReference(sql)
.WithReplicas(3); // this is only for a local development

Locally, you have three replicas, but to have three replicas in your Kubernetes cluster, you still need a manual update of your deployment.

Let’s wrap up the deployment section by saying that support for local development is more feature-rich than the deployment part. The local orchestrator and other development-related features work really well, while deployment seems like just an additional bonus.

Is Aspire only for mono repo at the moment?

Yes and no. Aspire is a development tool, so it needs to load all projects locally to maximize benefits. This one is tough since large applications consist of hundreds of microservices. Rarely do you want to run hundreds of microservices on your local machine.

The team behind Aspire is aware of this issue and working on documentation that demonstrates possible approaches and patterns. However, if you have a complex microservices architecture, you need to devise your own way of using Aspire.

This brings me to a tricky issue that may be difficult to spot. Aspire looks really good, so it may lead you to adjust your microservice architecture to align with the Aspire application model. Developers may tend to include multiple services in one solution and use only Aspire-backed cloud components. Don’t get me wrong, I’m not saying it isn’t good, but it might be a suboptimal solution.

For example, consider a situation where a development team is building a high-traffic IoT platform that requires handling large volumes of unstructured data. The ideal choice for this scenario would be a NoSQL database. However, since Aspire supports SQL databases natively, the team decided to use an SQL database instead. This decision led to performance bottlenecks and scaling challenges. Deciding which cloud resource to use is complex enough today, and the new Aspire tooling adds an extra layer of complexity.

Unsupported cloud resources

As mentioned, Aspire includes native support for various cloud resources, such as Redis, SQL Server, and Azure Service Bus. What if you require a resource that is not supported, such as a NoSQL database like Cassandra? Aspire won’t support every database or service. You must manually wire up these unsupported resources, which means you are missing out on the key benefits of Aspire, such as observability and easy local development.
Microsoft’s decision process for including cloud resources in Aspire is unknown to me. I can only guess it’s a mix of popularity, Microsoft’s ownership and partnership, and other factors.

On a more positive note, there is an option to write your own Aspire wrapper — here’s a tutorial. It doesn’t look super complex, so I assume that authors of cloud resources may write these Aspire wrappers in the future.

One surprising fact is that Aspire 1.0 does not support Azure Functions, which is quite surprising considering that Azure Functions are a key feature of Azure. However, support for Azure Functions has already been included in the roadmap.

Aspire as a marketing tool

You may also view Aspire as a marketing tool. I don’t think it’s a secret that Microsoft is trying to get more developers to use the .NET platform and Azure Cloud. Minimal API is an excellent example of this. It’s not built for old-school .NET developers who enjoy dealing with a certain level of complexity — Controllers, routing, program.cs vs startup.cs, etc. — I’m just partially joking. Minimal API appears to be designed for new developers transitioning from the Node.js or Python ecosystems.

I see Aspire as one of these Microsoft onboarding campaigns.
Simplified tools are more welcoming to beginners, who might be discouraged by the complexity of traditional ASP.NET applications.

The NET Aspire opinionated stack also removes much of the guesswork involved in setting up cloud-native applications and, more importantly(from Microsoft’s perspective), choosing the right sky-blue cloud.

Efficient Integration Testing

Aspire offers a more efficient approach to integration testing by enabling testing against real cloud resources without the need to deploy the application to the cloud. This way, you can catch bugs that might be missed during testing against mock data. This well-known meme represents it nicely:

However, be aware. Some resources are too heavy to run in the local environment, and some can’t even be run locally, like Azure Service Bus — it’s been seven years, and we’re still waiting.

The Aspire team is thinking about this issue as well, and it looks like they might introduce the full and slim version of Aspire components. This means that in the future, there might be a way to run a slim version of a resource that contains just dummy data. At this point, we will come full circle:

  1. Before Aspire: Test against mock data
  2. After Aspire 1.0: Test against a real resource
  3. Aspire in the future: Test against a slim service with mocked data

But I’m just speculating here. Let’s rather see where you can utilize Aspire.

Aspire use cases

I can see a few suitable use cases for the Aspire 1.0:

  1. A new application containing a few microservices. The term a few is crucial. If you have numerous microservices, you’ll end up with many components in Aspire, making it challenging to manage them in a single repository.
  2. Experimenting with new cloud resources. When you want to learn or try out a new cloud service and Aspire supports it, it might be easier to just create a new Aspire app and add a few lines of C# code to test the new component. Before Aspire, you would need to find a C# client, a Docker container, manage settings, etc. Aspire-prepared components provide a huge benefit for these use cases.
  3. Existing applications consist of up to three microservices with a few cloud resources supported by Aspire (SQL, Redis). I tried to convert one of my simpler applications to the Aspire application model. Even though the Aspire template didn’t recognize that I was using Directory.Packages.props, incorrectly placed Service Defaults, and shared things in the wrong namespaces, it only took me a few minutes to get it to work. The number of issues was surprisingly lower than expected.

I can think of more potential uses, but I have decided not to include them here due to the limitations of Aspire.

Ambitions of Aspire

One thing I’m not sure about is Aspire’s ambitions. Currently, it seems focused on C# projects and Azure. However, Aspire also includes support for NodeJS. The NodeJS support, despite its limitations (no replica support), hints at Aspire’s ambitious goals in the future.

// NodeJS:
var apiService = builder
.AddProject<Projects.AspireApp007_ApiService>("apiservice")
.WithReference(sql)
.WithReplicas(3);

builder
.AddNodeApp("NodeTest", "./start.js")
.WithReplicas(3) // the method is missing

Aspire also supports both the Docker host and the Podman host. I tested my sample apps with Podman and Docker, which worked for both. Although I know Podman aims to be compatible with the Docker API, it’s still a nice feature and a thoughtful touch from Aspire.

If you check the GitHub issues for Aspire, you’ll find numerous new requests for functionality, and quite often, the Aspire developers respond positively to them. I mentioned full vs. slim resources and the definition of cloud deployment in C# code. But there are more ideas, such as supporting non-NET microservices, integration with Pulumi, AWS and GCP integration, and many more.

The verdict

Aspire has great ambitions. In these times, it may be a useful tool for new projects running in Azure.

There are still a few issues:

  • One of the limitations is that all microservices need to be in .NET 8. Classic .NET is not currently supported and probably won’t be supported in the future.
  • If you have a few microservices and one or two cloud resources(DB, message bus, etc.), you're in a good place. However, if you maintain a complex monolith or hundreds of microservices, finding a good place to start testing and evaluating Aspire would be tricky.
  • Aspire might affect your overall architecture. Developers might prefer Aspire-backed resources to ones that are more suitable for their use case but require more maintenance.
  • Aspire still needs additional functionality to become a more universal product. The current version of Aspire is heavily focused on Azure and C#.

These factors are limiting, so I anticipate a slow adoption of Aspire. Nevertheless, I look forward to seeing how Aspire tackles various challenges to become a universally accepted and proven tool. As I see it now, the Aspire team has a significant amount of work ahead.

--

--