Containerize an ASP.NET Core BFF and Angular frontend using Aspire

profile
Tim Deschryver
timdeschryver.dev

In this article, we use the bff-aspnetcore-angular template from Damien Bowden as a starting point, and integrate Aspire into the project to improve the local development experience and prepare it for containerized deployment.

Note

For more information about the template, see Damien's article Implement a secure web application using nx Standalone Angular and an ASP.NET Core server.

The template implements a BFF (Backend For Frontend) architecture. As Damien has said it in his article "The project implements a secure web application using Angular and ASP.NET Core. The web application implements the backend for frontend security architecture (BFF) and deploys both technical stack distributions as one web application. HTTP only secure cookies are used to persist the session. Microsoft Entra ID is used as the identity provider and the token issuer.". Other security features include the use of CSP (Content Security Policy) headers and Anti-Forgery tokens.

The template and the related articles are already a great starting point for building a secure web application using ASP.NET Core and Angular. We recently used it to create a new project, and integrating Aspire improved the development experience. It also gave us a straightforward way to package the application for deployment with Docker.

If you have not looked at the template and the related articles yet, the project consists of an Angular application and an ASP.NET application. The Angular application is served from the ASP.NET application, and the ASP.NET application acts as a BFF for the Angular application by handling authentication and API requests. While running locally, the ASP.NET application proxies requests to the Angular development server. In production, the Angular application is served as static files from the wwwroot folder of the ASP.NET application.

Let's go through the steps. If you're just interested in containerizing the application, you can directly jump to the Dockerize the project section.

Integrate Aspire into the project

I followed the same approach described in my A minimal way to integrate Aspire into your existing project article.

Within the existing solution, create a new Aspire AppHost project, which will be used to run both the ASP.NET Server project and the Angular project. Then, configure the AppHost to serve the ASP.NET Server project and the Angular project.

To serve the Angular project, we can use the AddJavaScriptApp method from the Aspire.Hosting.JavaScript packages, which first needs to be installed in the AppHost project.

Then, we can configure the AppHost to serve the ASP.NET and the Angular project by adding the following code to the AppHost.cs file.

Because the Angular project is served from the ASP.NET application, we need to configure the relationship between the two projects. We can do this by adding a reference to the Angular project in the ASP.NET project and configuring the ASP.NET project to serve the Angular application as static files.

Creating this relation between the two projects and create an services__bff-angular__http__0 environment variable that contains the URL of the Angular application, which can be used in the ASP.NET application to configure the static file serving. In the current version this URL is hardcoded.

ServiceDefaults

You can also create a new ServiceDefaults project to configure the standard middleware for all services in one place, for example, to add OpenTelemetry support. After creating the project, add a reference to the ServiceDefaults project in the Server project and call the AddServiceDefaults method in the Program.cs file to apply the default configuration to the server project.

Server changes

To reroute the Angular routes to the Angular development server, the template uses YARP (Yet Another Reverse Proxy). To know the URL of the Angular development server, the template uses a hardcoded URL in the appsettings.Development.config file. This is not ideal, as it can lead to issues when running the project in different environments. By using the environment variable created by the AppHost, we can make the configuration more flexible and avoid hardcoding URLs. The bff-angular is the name of the Angular project given in the AppHost.cs file.

This doesn't work out of the box, because YARP can't resolve the bff-angular URL. To make it work, we need to configure YARP to use the environment variable created by the AppHost for the Angular development server URL. This can be done by installing the Microsoft.Extensions.ServiceDiscovery.Yarp package in the Server project and configuring YARP to use the environment variable for the Angular development server URL.

After the installation, configure YARP to discover destinations using the AddServiceDiscoveryDestinationResolver() extension method.

To render the Angular application, also update the _Host.cshtml file to load the index.html file from the Angular development server in development. Again, this uses the environment variable instead of the UiDevServerUrl configuration value that was previously hard coded.

Angular changes

In the original template, the Angular application was built into the wwwroot folder so it's automatically included when the ASP.NET application is deployed. Because this isn't the default behavior when building an Angular application and it's also not very visible (added to the angular.json configuration), I prefer to use Aspire's infrastructure.

Therefore I removed the output path configuration from the angular.json file. Now when the application is built, it reverts back to the default behavior to add the output build into the dist folder of the Angular project.

Dockerize the project

So far, all these changes improve the development experience, but they also make it easy to containerize the application with Docker. By using Aspire's infrastructure, we can build and run the application without having to manually wire the configuration of the individual projects.

First, we need to add the Aspire.Hosting.Docker package to the AppHost project, which adds the container publishing capabilities we need.

Next, replace the AppHost contents with the following code, which configures the application for container publishing.

Here we see some new concepts, let's go through them:

The AddDockerComposeEnvironment method creates a new Docker Compose setup that can run multiple services together. When the image is built, we do not want to include the Aspire Dashboard, so we disable it with the WithDashboard(false) method.

For the Angular project, we add a ContainerFilesSourceAnnotation, which tells Aspire where to find the Angular build output as a source for published container files.

For the ASP.NET project, we use the PublishWithContainerFiles method to include output of the Angular application within the ASP.NET application's image. This copies the content (the dist folder) from the Angular application to the wwwroot folder during publishing.

The WithExternalHttpEndpoints method configures the container to expose the HTTP endpoints of the ASP.NET application outside the container. Because the Angular application is served from the ASP.NET application, its content is also exposed through the same HTTP endpoint. If we don't do this, the ASP.NET application will be running in the container, but we won't be able to access it from outside the container.

Finally, we use the PublishAsDockerComposeService method to add this project as a service in the Docker Compose setup. This allows us to run the application with Docker Compose.

Resulting in the following docker-compose.yaml file being generated after running aspire publish:

The publish commands also generated the .env file containing the variables used in the docker-compose.yaml file, in this case, the BFF_SERVER_IMAGE and BFF_SERVER_PORT variables.

Note that we don't we only create a reference between the Angular and ASP.NET project in development, but not in publish mode. This is because in publish mode, the Angular application is built and included in the ASP.NET application's image. This is required, otherwise you will receive an error during publishing that the Angular project can't be found, because it's not included in the publish context. Howerver, at the time of writing this post it does seem that this issue is resolved in the latest version of Aspire (13.2.1), so you might be able to include the reference in publish mode as well.

Next steps

If you want to customize the Docker image further, you can set image tags, set up a container registry, and integrate it with GitHub Actions for automated builds and deployments.

Conclusion

By integrating the template with Aspire first, we improve the local development experience without changing the core BFF architecture. Service discovery removes hardcoded frontend URLs, and the same setup makes it much easier to package the application for Docker-based deployment. The result is a cleaner development workflow today and a more predictable path to containerized hosting tomorrow.

Using the best practices already set out by Damien's template, this gives us a solid foundation for building secure web applications with ASP.NET Core and Angular, while also leveraging Aspire's capabilities for orchestration and containerization.

You can find the full project on GitHub, or inspect the commit that turns Damien's original template into an Aspire project with Docker support.

Outgoing links

Feel free to update this blog post on GitHub, thanks in advance!

Join My Newsletter (WIP)

Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.

Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

Buy Me a Coffee at ko-fi.com PayPal logo

Share this post