Elevating BFF Apps with .NET Aspire

This post has been republished via RSS; it originally appeared at: New blog articles in Microsoft Community Hub.

Back in April I wrote a post on how to implement the Backend for Frontend (BFF) pattern with Azure Container Apps. At the time I said I would avoid bringing in .NET Aspire as it was in preview and earlier explorations pointed to it being somewhat buggy in those preliminary stages.

 

About a month ago at Build .NET Aspire went GA so that raises the question anew - should I use Aspire? Let's revisit our sample app and take a closer look.

 

.NET Aspire.NET Aspire

Revisit that post to read about the BFF concept and implementation details: https://techcommunity.microsoft.com/t5/microsoft-developer-community/implementing-a-blazor-based-bff-in-azure-container-apps/ba-p/4111208

 

All the code can be found here:
https://github.com/ahelland/Container-Apps-BFF/tree/main/src/BFF_Aspire

 

Adding Aspire to an existing project is quite easy - right-click and choose to add ".NET Aspire Orchestrator Support".

Aspire_01.png

 

Hitting F5 brings up the app and dashboard. But wait, the WeatherAPI doesn't work.

Aspire_02.png

 

We originally made the choice to have the API as a separate solution and that boundary still applies. Aspire isn't able to step out of the boundary of the web app solution by itself. You can sort of wire in other csproj files manually, but I guess it's a design decision if you want to do this. Personally I feel this can lead to scenarios where a solution isn't as independent and complete as it should be. Do note that in this specific case there's an additional minor snag because Aspire currently does not support APIs/apps where you checked "Place solution and project in the same directory" during scaffolding. In this sample it is not critical to maintain the API totally separate from the rest of the solution so I went with the easy fix to bring it all into one solution.

 

But what if you want/need these two to be separate entities? Aspire can bring in containers instead of the csproj files so if that is a logical boundary you can work with I would say that is a feasible path.

 

Ok, we're still able to run our app - that's nice and dandy.

Aspire_03.png

 

Any other value adds? Well, yes actually there is.

Service Discovery

 

In the first iteration of our code we used a somewhat janky approach switching between the local API address of the weather API and the one running in the cluster:

var apiBaseAddress = "http://localhost:5041"; //For ACA if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production") { apiBaseAddress = "http://weatherapi"; } builder.Services.AddHttpClient<IWeatherForecaster, ServerWeatherForecaster>(httpClient => { httpClient.BaseAddress = new(apiBaseAddress); });

 

On a larger scale It's not practical to do if checks like this, and of course if the port number changes for some reason it will break. It works when running in Container Apps because a service discovery mechanism is running in the cluster. Our previous approach was basically to not use service discovery in debug mode and now .NET Aspire is able to fill this role for us.

 

How about us changing the code to:

builder.Services.AddHttpClient<IWeatherForecaster, ServerWeatherForecaster>(httpClient => { httpClient.BaseAddress = new("https://weatherapi"); });

 

There's a few extra lines to make this work - in the Program.cs for the BFF_Web_App.AppHost project you have these two lines:

builder.AddProject<Projects.WeatherAPI>("weatherapi"); builder.AddProject<Projects.BFF_Web_App>("bff-web-app");

 

Tweak this to the following:

var weatherapi = builder.AddProject<Projects.WeatherAPI>("weatherapi"); builder.AddProject<Projects.BFF_Web_App>("bff-web-app") .WithReference(weatherapi);

 

Adding references means Aspire takes care of injecting the correct name resolution for us. Note that we only added a reference to the Web App project as the API project does not rely on making outbound calls. For a larger project you shold figure out which project needs what based on mapping out the communication paths.

 

Application settings

Another "trick" we pulled that isn't really recommended is how we handled appsettings - basically hardcoding inline:

builder.Services.AddAuthentication() .AddJwtBearer("Bearer", jwtOptions => { // The API does not require an app registration of its own, but it does require a registration for the calling app. // These attributes can be found in the Entra ID portal when registering the client. jwtOptions.Authority = "https://sts.windows.net/{TENANT ID}/"; jwtOptions.Audience = "api://{CLIENT ID}"; });

We could have put this into the appsettings.json file instead of course, but laziness :) Thing is, the Aspire AppHost project brings its own appsettings. So we can do the following (in the AppHost's appsettings.json):

{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Aspire.Hosting.Dcp": "Warning" } }, "Parameters": { "TenantId": "guid", "ClientId": "guid", "ClientSecret": "secret" } }

In the Program.cs for the AppHost you need to bring them in as variables:

 

var tenantId = builder.AddParameter("TenantId"); var clientId = builder.AddParameter("ClientId"); var clientSecret = builder.AddParameter("ClientSecret", secret:true);

 

And afterwards we can invoke them like this (exemplified by the Weather API's Program.cs, but same goes in the Web App):

var tenantId = builder.Configuration.GetValue<string>("TenantId"); var clientId = builder.Configuration.GetValue<string>("ClientId"); builder.Services.AddAuthentication() .AddJwtBearer("Bearer", jwtOptions => { jwtOptions.Authority = $"https://sts.windows.net/{tenantId}/"; jwtOptions.Audience = $"api://{clientId}"; });

 

Is there a point to doing this instead of the classic appsettings per csproj? In this solution we use the same clientId in two projects - clearly it's easier maintaining this in one place. For settings that only apply to one project it might not make as much sense apart from the value of centralizing settings. While I didn't do it here you can also wire up the AppHost to look up secrets in a Key Vault solving another dev challenge.

 

There's only so much you can do in a small sample like this, but Aspire allows you to bring up containers, databases and more using the same pattern. (Meaning you can easily bring in frontends written in other languages for instance.) Visit the (updated) eShop sample for more on that.

https://github.com/dotnet/eShop

 

Should you run out and implement this immediately? Well, if you have a working setup you feel happy with this isn't going to magically improve anything, but if you are in the early phases of setting up a new project I would certainly attempt to bring in Aspire if manual wiring is the alternative. Otherwise plan to add in a maintenance sprint or bring in it when upgrading to .NET 9 in a few months or something.

 

What about deployments to the cloud? I kinda saw that one coming :)

 

Aspire plus Azure equals ?

In early demos we saw how Aspire-based solutions were being deployed to Azure at the click of a few buttons. This lead many to think that it's this great developer focused deployment vehicle. Actually…no.

 

Aspire in itself is focused on enhancing the developer experience locally on your developer computer. You don't have to deploy to Azure, on-prem or in any way alter your current CI/CD processes. You don't deploy Aspire itself either. It does however enable other tools like the Azure Developer CLI to process the Aspire manifest and turn it into Bicep code for creating Azure resources.

 

There's also a community effort called Aspir8 that takes care of generating Kubernetes deployment packaging. It is apparently in the process of being brought into the MS-hosted Aspire repo so we'll see how that integrates, but still think of Aspire as something that runs on your machine only.

 

OK - so technically Aspire isn't responsible for pushing my code to Azure, but I can still have developers releasing their code this way? Well…

 

If you have the Azure Developer CLI (azd) generate the IaC for you the code should still be deployed to Azure DevOps or GitHub and have pipelines/actions run there as a general practice. (Azure sandbox subscriptions are an exception, but I'm not diving into those concepts for now.) Azd will even generate these for you.

 

The Azure Developer CLI is able to understand things like you needing a container environment, container registry, etc. But for obvious reasons it is not able to infer that you want to force all outbound traffic through an Azure Firewall and stand up an Azure Application Gateway to handle the inbound traffic. And even with the knowledge it takes quite a bit more Bicep code to stand up all of that in addition to the added cost.

 

Azure Developer CLI

Creating the necessary bits for playing with code isn't complicated.
(Installing azd: https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd?tabs=winget-windows%2Cbrew-mac%2Cscript-linux&pivots=os-windows)

Run azd init:

Azd_01.png

 

Run azd infra synth:

Azd_02.png

 

Pausing for a moment here. This is the folder and file structure from my previous blog post:

BFF_Infra_Old.png

 

This is what azd generated:

BFF_Infra_New.png

 

Which sort of leads to the question of whether this is me over-engineering things or the azd team under-engineering? As so often the answer is possible a place in-between. If what you want to do is to run a quick test and debug session to verify your code works what azd generates is perfectly fine. If what you want is a solution that is more production-like I believe my Bicep is better suited. (I have simplified my implementation to be suited for a blog post so I must stress that it is not production grade on my part either, but it has a structure you can work from.)

 

Currently you are driven towards deploying the services as container apps. As I also based myself on that it's comparable in this context, but maybe you want something else. The Kubernetes tooling I mentioned can bring the containers to a different runtime environment, but if what you are looking for is non-containerized Azure Functions, Azure App Services, or something else you're not really supported by azd for now. To be fair when Project Tye was previewed a few years ago it was about solving the pain of microservices development, not monoliths. (Project Tye being an experiment that was discontinued, but is/was the inspiration for .NET Aspire.)

 

And that's sort of the general state of azd at the moment - dev friendly and quick to get going. Thumbs up. Governance, scalable structure, etc. Not so much.

 

A quick azd up will deploy and confirm that it works. Both the web app and the api are deployed with internal ingress only though, so you will need to flip to external ingress on the web app to be able to browse to it:

Ingress_01.png

 

There is one thing I'd like to point out with the azd approach that I like. In my approach with levels the container apps are called "Level-4" and there's a copy & paste job adding to the list for each app in your solution. This can be done as a templated loop in a CI/CD pipeline, but it's still a slight disconnect between the lower level infra and the developer stuff. With the azd & Aspire combo you will notice that in addition to the root infra folder there is also an infra folder underneath the Aspire AppHost project:

Azd_03.png

 

This contains yaml files that are specific to the individual apps and are easy to tweak. For instance the ingress setting can easily be set to external here instead of using the Azure Portal. You can even reference Bicep files in the Aspire orchestration code:
https://learn.microsoft.com/en-us/dotnet/aspire/deployment/azure/custom-bicep-templates?tabs=dotnet-cli

 

I think that might be a topic to dive deeper into at a later point, but as you can see there are new options for integrating app code and infra code tighter.

 

Look at the rest of the code here:

https://github.com/ahelland/Container-Apps-BFF/tree/main/src/BFF_Aspire 

 

Aspire Dashboard

I stated a few paragraphs ago that Aspire itself only pertains to your local environment and is not deployed to Azure even if it plays nice with azd. That wasn't a lie, but while the orchestration only applies locally there is actually a component from the observability puzzle you can deploy. The Aspire Dashboard:

Dashboard_01.png

 

Azure Monitor and Application Insights have their use and this doesn't remove the need for those tools, but I think this is a nice addition for giving the rather direct access to traces and console logs. (Console logs are available directly in the Azure Portal for Container Environments as well, but it's slightly more clunky.) Sure, there are other tools in the observability stack, but this one didn't cost me any additional calories to get going with and it brings parity between the things on my local computer and what I can do after deployment.

 

All in all I believe .NET Aspire is in a much better state than at the start of the year and you should do a proof of concept or two to see if it fits into your developer workflow.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.