Microsoft just introduced .NET Aspire as part of .NET 8. It was released for general availability in May 2024. According to Microsoft, .NET Aspire is an opinionated, cloud ready stack for building observable, production ready, distributed applications. It does this by providing:
Orchestration: It improves your local development of your distributed applications by simplifying the app's configurations, interconnections through service discovery and provide you with real-time comprehensive dashboard for app monitoring, logging, health check and inspection. It also provides a single starting point for your entire applications and their dependencies.
Components: Simplify the connection to popular services such as Redis, SQL, Rabbit MQ and many others. Through orchestration many services can be configured through standardize configuration patterns and for the one that are cloud-natives can also have health check and telemetry added upon.
Project Template & Tooling: .NET Aspire includes tooling to help us create and configure in an easier manner such as reading to use template, cloud-native apps .
In this article, I want to focus first on Orchestration to get our feet wet and show you all the fun and cool stuff we can explore with very little effort on our part.
To start off, I have two separate .NET 8 projects call library-api and library-ui.
library-api: This is a .NET Core web API project which serves as the back-end API for my library management system. It exposes endpoints for managing library data such as books, authors.
library-ui: This is .NET Core web application project which serves as the front-end user interface for my library management system. It contains Razor Pages and MVC controllers to handle user interactions and display data.
Currently, when I want to run things locally, I must open the library-api project in a separate Visual Studio 2022 instance and run it, and similarly, my library-ui runs on another Visual Studio 2022 instance. While this is not that big of a deal, if we replace this setup with many microservices, each with its own configuration, you can see how things can get complicated really quickly. This is where .NET Aspire's Orchestration can help us.
Before we begin, we must have the following requirements:
Visual Studio 2022 version 17.10 or higher.
.NET 8.0
Now that you are ready, let's create an empty solution which will house our two projects - library-api & library-system.
Add the projects you want to be part of .NET Aspire, in my case I want the front end and back end projects which makes up my web application.
Right click either the UI project and select Add > .NET Aspire Orchestrator Support.
You'll see the confirmation that let you know .NET Aspire will add two new projects to your solution. I will go into them in details later.
AppHost solution: This is a project which responsible for connecting and configuring the different projects and services within our application. It provides a centralized entry point for our application.
ServiceDefault solution: This is a shared project which manages configurations reused across projects in the solution, covering aspects like resilience, service discovery and telemetry.
If you open the library-apsire.AppHost's program.cs
file, you would see the following code. In here you can see it reference the ui project via AddProject()
. Essentially this let .NET Aspire knows about the UI project itself.
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.library_ui>("library-ui");
builder.Build().Run();
Right click on the API project and select Add > .NET Aspire Orchestrator Support and you would see something similar to the following pop up.
Once clicked OK, you can see there are 4 projects in my solutions
Now when checking the AppHost's Program.cs
file you will see the API project being referenced.
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.library_ui>("library-ui");
builder.AddProject<Projects.library_api>("library-api");
builder.Build().Run();
Now we are almost done, but we must tell .NET Aspire about the dependency of our project. In my case, my UI project needs to have my API project running in order to send requests to the backend logic. Therefore, I explicitly tell Aspire that my UI depends on my API project. This can be extended to provide a descriptive relationship between your projects if you have a more complex case of dependency.
var libraryUI= builder.AddProject<Projects.library_ui>("library-ui")
.WithExternalHttpEndpoints()
.WithReference(libraryApi);
You can now run your solution. Notice how the project library-aspire.AppHost is our our start up project. It will start our API solution and at the same time brings us to a .NET Aspire dashboard
Here you can see it shows the two applications I have as well as their statuses, base URL, and links to logs and other information.
Upon clicking on the UI's endpoint, it brings me to my UI page where I browse around, add more books ect..
Once I check back on my dashboard, I can check the console logs for each of my project.
I can also view the structure logs, the trace of my http request from the client to the server or the metric details of different part of my application.
Apart from the nice-looking overview dashboard of our application stack as a whole, running our projects as part of .NET Aspire orchestration immediately adds the following method in the Extension.cs
of the ServiceDefaults
project. It conveniently adds health check capabilities for all our endpoints across all projects without us having to code each one individually.
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
if (app.Environment.IsDevelopment())
{
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
If you send a GET request to either /Alive or /Health endpoint, you'd get an appropriate response from the server.
Here is the health check end point of my API application.
Here is the healthcheck end point of my UI application.
That's pretty neat for having all that in your local environment, right? Well, the part that excites me more is the service discovery.
So far, we have configured our API application to run on port 7072. You can check the launchSettings.json
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7073;http://localhost:5249",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
In our UI, we have to configure this either in its configuration file or directly in the code. I chose to define this as part of the HTTP client initialization in my Program.cs
builder.Services.AddHttpClient<IBookService, BookService>(client =>
{
client.BaseAddress = new Uri("https://localhost:7072");
});
Pretty straightforward, right? Well, only up to a point. Imagine if you have many microservices as your dependencies. Each time you change its port or other settings, you must update it in your consuming project's configuration file or, in my case, inside the code to reflect the new port. Otherwise, my UI project won't be able to make API calls
To address this issue, .NET Aspire service discovery and named endpoints will help us. The above code, where I register my BookService
as an HTTP client, can be changed to:
builder.Services.AddHttpClient<IBookService, BookService>(client =>
{
client.BaseAddress = new Uri("https+http://library-api");
});
By stating the name library-api
, we explicitly tell the app host that the reference base address we want to use is the base URL of our API application. Now, let's change our API base url to run on port 7073 instead of 7072
Once we run our .NET Aspire, the change is picked up seamlessly without us having to make similar updates to all other projects that depend on our API application. Once you run it, the dashboard now shows the new port, and everything is wired up for us to ensure our application still works.
In conclusion, I've just shown you one small aspect of .NET Aspire. It helps consolidate our dependencies and makes it easier to run applications locally with a centralized management dashboard. It also eliminates the tedious work of maintaining endpoint configurations across all our applications, minimizing mistakes and maintenance efforts.
In future articles, I will focus on other features of .NET Aspire, such as components, and demonstrate how easy and quick it is to switch out traditionally tricky components within your application, such as databases, caching, Azure cloud components, and many others.
Happy reading!