In this post, I will be writing about .NET Aspire, which has been gaining significant traction since its initial release earlier this year. I'll explain its architecture and features and provide a few examples of how it can be used to improve our development experience when working with cloud-native applications. The current version is 9.0. It's still in its early stages, and we can expect rapid changes and the introduction of more innovative features and tools.
What is .NET Aspire?
.NET Aspire is a cloud-native framework for .NET that simplifies the development of observable, production-ready, distributed applications. It can be used for any application, from a simple website connecting to a database to a large application comprising multiple interconnected parts like different front-ends, multiple APIs, cache layers, databases, etc. .NET Aspire truly excels in effortlessly orchestrating these complex architectures and paves the way for creating local development environments for such infrastructures. It comes as a set of NuGet packages and offers a collection of tools, patterns, and integrations with popular services. It abstracts common functions such as service discovery, observability, container setup, environment variable configuration, and various low-level details that are typically encountered when developing cloud applications.
.NET Aspire Features
Orchestration
The primary purpose of using .NET Aspire is to simplify the orchestration of the various pieces of a complex system. To achieve this, Aspire offers various APIs to express the resources of our application and the connections between them. It also offers several tools to accelerate development such as VS and VS Code extensions or CLI commands. When discussing orchestration, it's crucial to define the key entities that bind together the different parts of our application:
- App Host project. This is the .NET project where the orchestration takes places. This is where the different pieces of our system are defined as resources along with the connections between them
- Resource. A resource is an individual part of our application. It can be a different .NET project containing code for an API or a frontend, a container, a database, a cloud service etc
- Integration. Integrations are NuGet packages that can be referenced by other resources and provide a seamless way to add popular services in our systems such as Redis, MySQL, RabbitMQ etc
- Reference. A reference is an API to define a connection between resources.
Integrations
.NET Aspire provides a streamlined approach to integrating various services and components into our applications. These integrations can range from simple configuration providers to complex cloud-native services. Aspire provides the integrations as a collection of NuGet packages to support a wide variety of services like Redis, MySQL, Postgres, ElasticSearch, OpenAI, lots of Azure features and recently AWS CDK support. The list of integrations has expanded since the initial inception of Aspire with lots of officially supported Integrations and also community maintained ones which can be found under the Community Toolkit Integrations. Developers are also given the ability to create custom integrations tailored to their specific domain needs, enabling them to reuse components across different systems within their infrastructure.
Dashboard
The dashboard is a powerful tool that provides real-time insights into our application's running state, health, and performance. This web interface offers a comprehensive view of various aspects, such as logs, traces, and allows us to start, stop, and view the environment configurations of services. It's integrated with the Aspire app host project and is automatically launched when we run our project.
Various Development Aids
Besides the core features mentioned above, .NET Aspire comes with various aids that greatly enhance our development experience.
- Service discovery. With service discovery we can seamlessly connect the different moving parts of our application, create references between projects, connect to specific endpoints etc
- Health Checks. Aspire offers Health Check endpoints to provide information about the running status of the different parts of our application and can be used to trigger alerts, verify availability of dependencies, provide information to services like Load Balancers etc
- Telemetry. With the OpenTelemetry integration capturing logs, metrics and traces is enabled seamlessly and can also be exposed via exporters to external monitoring tools like New Relic.
- Launch profiles. Different profiles can be defined in the app host and service projects that allow us to separate environment configurations and easily launch different environments while developing locally
Demo time
Set up
Aspire comes with .NET 9. Install it from here
A container runtime is required. For this demo we used Docker Desktop
Finally we need to install the Aspire templates via the command line:
dotnet new install Aspire.ProjectTemplates
Creating the projects
With all the prerequisites in place we can create our first .NET Aspire project via the following command:
dotnet new aspire --output AspireSample
This will create 2 new projects under the AspireSample folder:
- The App Host that is responsible for orchestration
- The Service Defaults that handles core cloud-native functions like telemetry and health checks.
For the purpose of this demo we will create a simple frontend application using React/Next.js and name it demo-ui
npx create-next-app@latest
We will also create a backend project to serve as our API to showcase the simplicity of wiring different pieces together. For this we will use ASP .NET:
dotnet new webapi --output DemoAPI
Interconnecting our infrastructure
With the setup we performed in the previous steps we now have most of the code already generated for us. We will just need to leverage the tools Aspire offers to us to do the wiring. Our goal is our frontend to display the data returned from our API. No surprises here, our API is the famous Weather Forecast .NET sample created by the webapi
template. So let's look at the code
Adding integrations
First we need to install the integrations we will be leveraging in our infrastructure by adding the related NuGet packages to our app host project
cd AspireSample.AppHost
Since our frontend is a React app we will need to add the NodeJS integration
dotnet add package Aspire.Hosting.NodeJS
Although we will not be leveraging Redis features in our example, we will add the integration to demonstrate the simplicity of adding a cache layer to our projects:
dotnet add package Aspire.Hosting.Redis
We also need to add a project reference for our API C# project
dotnet add reference ..//DemoAPI
Wiring things
Next what we need to do is to connect the different pieces of our simple infrastructure together. We do this in the Program.cs
file in the app host project. With the code below, we define our resources and wire them together:
- We create a Redis cache. This will actually start a Redis Server which we can connect to using CLI or Redis Insight
- We create our API resource and with reference to the Redis cache. This will inject the Redis connection string to our API as an environment variable.
- We create our frontend resource by using the AddNpmApp extension passing the API endpoint as an environment variable by using the WithEnvironment extension
var builder = DistributedApplication.CreateBuilder(args); // Create a Redis cache var cache = builder.AddRedis("cache"); // Create the API resource and add the Redis cache as a reference var api = builder.AddProject<Projects.DemoAPI>("demoapi") .WithReference(cache); // Create the frontend resource and pass the API endpoint as an environment variable // Instruct it to execute the Next.js dev script to start the app builder.AddNpmApp("frontend-react", "../demo-ui/", "dev") .WithEnvironment("REACT_APP_API_URL", api.GetEndpoint("https")) .WithExternalHttpEndpoints(); // Run the application builder.Build().Run();
Enable CORS
The next step would be to enable CORS in our simple ASP .NET Web API to allow our frontend to consume the data. We need to add a few lines to our Program.cs
file in our API project to allow this:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddCors(); builder.Services.AddAuthorization(); // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseCors( options => options.WithOrigins("http://localhost:3000").AllowAnyMethod().AllowAnyHeader() ); app.UseHttpsRedirection(); app.UseAuthorization(); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast"); app.Run(); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }
Implement the frontend changes
The final step is to implement the UI changes to consume the API data and display it in our simple frontend page
Since this is a Next.js project we need to add the following to the next.config.ts
file to access the API URL environment variable on the client side:
import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, env: { API_URL: process.env.REACT_APP_API_URL, } }; export default nextConfig;
Finally in our page.tsx
file we need to add the following code to fetch from our API and create a simple user interface to display the data.
"use client"; import { useEffect, useState } from "react"; export default function Home() { const [forecasts, setForecasts] = useState([]); const apiServer = process.env["API_URL"]; console.log('apiServer is', apiServer); useEffect(() => { const requestWeather = async () => { const weather = await fetch(apiServer + "/WeatherForecast"); console.log(weather); const weatherJson = await weather.json(); console.log(weatherJson); setForecasts(weatherJson); }; requestWeather(); }, [apiServer]); return ( <div> <header> <h1>Aspire Demo - Forecast</h1> <table> <thead> <tr> <th>Date</th> <th>Summary</th> <th>Temp. (C)</th> <th>Temp. (F)</th> </tr> </thead> <tbody> {( forecasts ?? [ { date: "N/A", summary: "No forecasts", temperatureC: "", temperatureF: "", }, ] ).map((forecast) => { return ( <tr key={forecast.date}> <td>{forecast.date}</td> <td>{forecast.summary}</td> <td>{forecast.temperatureC}</td> <td>{forecast.temperatureF}</td> </tr> ); })} </tbody> </table> </header> </div> ); }
Summary
I hope you enjoyed the read and found this tool interesting. Although it is still in early stages, it already offers rich features and it is production ready. It is definitely a promising technology and something everyone should keep an eye on.
References
Akis Papaditsas
Chief Enginner, Monex Insight