Introduction to .NET Aspire

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

.NET Aspire Overview

Akis Papaditsas
Chief Enginner, Monex Insight