Revised .NET Aspire Lessons Learned After One Year

I started to use .NET Aspire when the first preview was launched. I wrote a blog post about my first impressions at the end of 2023. Now, it's good to revisit this and update the lessons learned story.

I held a presentation titled "Shift to .NET Aspire - Lessons Learned" in the Finland Azure User Group on 28.1.2025. This blog post summarizes my presentation, especially from the lessons learned point of view. You can find the presentation on this page if you want to know more about the project, context, and background.

In the presentation, I demonstrated our progress in transforming a monolithic application into microservices using .NET Aspire and Azure Container Apps. Transformation project has started, and there is still a lot to do. The presentation and this blog post reflect what we have faced so far.

In this transformation, we are aiming to especially improve developer experience and productivity.

💡
Overall, .NET Aspire is all about keeping developers happy, improving the developer experience (DevEx), and productivity.

Lessons Learned 1 - .NET Aspire enables a great development/developer experience (DevEx)

The current solution consists of four different repositories where the application and infrastructure code are separated. The goal is to consolidate all C# application and Bicep infrastructure code into a monorepo and decouple the microservices. Currently, microservices are tightly coupled with each other, which makes development and maintenance difficult. Also, debugging the current solution requires running three Visual Studio projects simultaneously, and switching between them. You can imagine; it's cumbersome.

The primary goal of this transformation is to improve the developer experience of this solution. We aim to create a simple monolithic development experience while still allowing for the delivery of independent microservices to the Azure cloud.

.NET Aspire is a key component in making this possible efficiently. Arguably, all of this has been possible earlier, but now .NET Aspire makes it much easier and more comfortable.

.NET Aspire enables all of this:

👉 Microservices with monolith development experience.
👉 Easy to develop, manage, and fast to debug.
👉 Effective deployment to dev environments via AZD.
👉 Easy to spin up new container-based services without extensive infrastructure and deployment pipeline work.
👉 .NET Aspire dashboard for monitoring logs and traces.

Lessons Learned 2 - .NET Aspire App model (SDK)

.NET Aspire App model is a crucial part of enabling the superior developer experience. App model itself is an important topic, so let's talk about it in this own section.

.NET Aspire App model or .NET Aspire SDK is the heart of service orchestration in your distributed application. It defines what projects or containers are running in your solution and what the dependencies are between them. I think it can be said that .NET Aspire App model (C#) is a counterpart to Docker Compose (YAML).

Let's briefly compare the Docker Compose and .NET Aspire App Model.

Service orchestration using Docker Compose (YAML):

Service orchestration using .NET Aspire App Model (C#):

Overall, the .NET Aspire App model is pretty intuitive and easy to use. I love the possibility to use C# to configure service orchestration. Overall, I prefer the C#-based configuration model over the traditional YAML-based Docker Compose. The .NET Aspire way of configuring service dependencies and injecting variables is great. It's super easy to define project dependencies and variables using this model when developing the solution in a monorepository.

As said, in this our transformation project, we are hosting these microservices in the Azure Container App Environment. The .NET Aspire App model actually enables you to configure most infrastructure-related settings via C#. Let's touch on this topic more in the next section.

During the year, the App model has evolved and improved a lot. Over one year ago, Azure Functions weren't supported, but now they are. Just mentioning one big change.

👉 The .NET Aspire App Model is not complete, and it is evolving all the time.
👉 You can extend the model with extensions.
👉 Check extensions: Aspire.Hosting.Azure.*
👉 Learn the App Model and how it affects YAML and Bicep definitions of the Container App (read more about azd infra synth later in this post)
👉 Install Aspire.Hosting.Azure.AppContainers if you prefer Bicep over YAML when configuring Azure Container Apps.

Lessons Learned 3 - Configure infrastructure using C#

I briefly mentioned in the previous section the possibility of affecting infrastructure via the .NET App Model (C#). Let's talk about this more. Basically, you have at least a couple of options to do this.

OPTION 1: You can use existing Bicep modules and inject the modules into the .NET Aspire App model using the AddBicepTemplate method. This is very convenient when you want to use the existing Bicep modules.

Traditional SQL Server Bicep module:

Create a reference to a Bicep module via C#:

OPTION 2: Install the necessary extensions and configure the infrastructure resources using C#.

Create Azure SQL Server resource via C#:

💡
.NET Aspire provides comprehensive support for modifying infrastructure-specific settings via C#, but there are still limitations. For example, modification of Azure Container App-specific CPU and Memory settings is not supported. These configurations must be manually modified to Bicep or YAML files.

This is an interesting declarative way to set up the infrastructure directly using C#. Remember there are still limitations, and everything may not be supported. Overall, it's important to learn how the .NET Aspire App model configuration (C#) changes affect the actual infrastructure configuration in Bicep and/or YAML.

👉 Interesting option, but it has limitations.
👉 In certain cases, manual editing of Bicep is needed anyway.
👉 Follow how it formulates and evolves.
👉 In the transformation project, we’ll stick with the manually created Bicep modules model and evaluate the C# model later (too early now).

Lessons Learned 4 - Learn AZD to get the best out of .NET Aspire

Azure Developer CLI (aka. AZD) is an essential "add-on" to simplify infrastructure provisioning and application deployment. AZD simplifies the deployment pipelines (YAML) and makes it easy to deploy solutions to Azure from the local environment.

The azd infra synth command is a useful tool that converts C# .NET Aspire App configuration into Bicep and YAML files (YAML-based configuration model is used for Azure Container Apps). The command is in alpha stage at the moment, and it is mostly for experimenting and testing.

👉 .NET Aspire and AZD are a good match.
👉 AZD infra synth (alpha version) is great, but at the moment it's only for experimenting.
👉 Be careful with the azd infra synth (know what you're doing and always diff changes).
👉 At the moment, azd infra synth is a great learning tool to identify how .NET Aspire model changes (C#) affect Bicep or YAML.
👉 Learn workflow differences of the azd up, azd provision, and azd deploy (e.g, Aspire manifest is updated only during azd up).

Lessons Learned 5 - Service Discovery

Service Discovery enables services to find and communicate with each other dynamically using logical names in a microservices (distributed) solution. Practically, this means that, e.g, in a reverse proxy configuration you can point to microservices by their logical names instead of using fully qualified domain names (FQDN) or localhost with a specific port in a local development environment.

This capability is crucial for simplifying application configuration in complex microservice applications, and enhancing the developer experience. By default, in a .NET Aspire solution, necessary NuGet packages are already in place (check the ServiceDefaults project).

Service Discovery is not a .NET Aspire-specific feature. You can use it in other solutions as well.

👉 Install Microsoft.Extensions.ServiceDiscovery
👉 Install Microsoft.Extensions.ServiceDiscovery.Yarp (reverse proxy extension)

Service dependencies:

In this sample, the frontend is dependent on two microservices (location and weather). Basically, the WithReference method creates a dependency between services. It creates an environment variable in the container that points to the service location.

Lessons Learned 6 - Cumbersome application configuration management

Application configuration refers to the ability to set specific settings for each microservice that influence its behavior. Setting can be sensitively considered a secret or just plain text that is visible to all developers.

Configuration management in .NET Aspire/AZD is still a bit cumbersome, as I mentioned also in a previous Lessons Learned blog post over a year ago. Overall, it has improved, but there is still space to clarify this totality.

At the moment, there are multiple application configuration-related files that you need to know:

  • Running application locally => appsettings.development.json (by default).
  • Deployment (azd up) to Azure from a local environment => .env files + config.json.
  • Deployment pipeline (azd up in YAML) => Variable Groups
💡
Note! The observation below is based on a setup where all configurations are stored in ADO variable groups, either as plain text or as secrets. Secrets are stored in the App Container's secret configuration.

Especially, configuration using variable groups and secret variables in deployment pipelines needs some extra attention. I initially thought I could add secret variables to variable groups and then reference them in the YAML deployment template as environment variables. But the configuration is not working like that. In the below sample, Keycloak password is a variable holding a secret value.

The key here is the environment variable called AZD_INITIAL_ENVIRONMENT_CONFIG. Configuring this variable in the ADO variable group allows you to use it correctly in the setup. The variable should contain the content of the local config.json file, but secrets must be in plain text.

👉 Consider using alternative solutions for secret management (e.g, KeyVault) at the application level, at least for now.
👉 Lack of documentation, especially regarding this configuration topic. Follow discussions on Github.

Comments