.NET Core has been a great ride for all of us building web- and cloud-driven apps, but managing distributed apps made for cloud native has been a mishmash of different tools. That's about to change. Seeing a problem in how large microservice architectures are deployed and managed, the ASP.NET team has taken a big swing with a solution that should help most .NET developers think about microservices and distributed applications without the dread many of us have had.

The Problem

Building distributed apps can be difficult. This has been a truth in computer science since the very beginning. Today, we talk about using containers, Kubernetes, and creating microservices. But the nature of distributed computing is still very much the same as it was in the beginning. Creating applications that are distributed immediately require some basic requirements, including how the different services can reach each other, how to manage configuration across service boundaries, and how to monitor projects across the service boundaries. The community is trying to solve this with something called cloud native.

Aspire and other cloud native frameworks solve distributed applications, but for many organizations, there's not enough benefit to justify the complexity in moving to cloud native.

Cloud Native? What's That?

The idea of cloud native comes from the basic desire to decouple services. Cloud native is an approach to run scalable applications in a variety of environments including public, private, and hybrid clouds. To achieve this, the architectural approach encourages:

  • Resiliency
  • Manageability
  • Observability

Effectively, this means your distributed application is loosely coupled, can support scaling as necessary, and is using instrumentation to be able to monitor your applications in real-time. In addition, as seen in Figure 1, cloud native leans on four principles: containers, service oriented, DevOps, and continuous delivery.

Figure 1: Principles of cloud native
Figure 1: Principles of cloud native

Cloud native is an approach to running scalable applications in a variety of environments, including public, private, and hybrid clouds.

With these principles in mind, the goal of cloud native is an application that's composed. For large distributed applications, your project will need a variety of different types of services, not just the ones your organization authors. For example, some of the common services or resources that you need can be seen in Figure 2.

Figure 2: Composing a distributed application for a cloud native app
Figure 2: Composing a distributed application for a cloud native app

In order to accomplish this, you need a way to define and share information across the different services. There are a number of approaches that work (e.g., Kubernetes). For many .NET developers, it's been a challenge to learn how to integrate your services into a cohesive application. That's where .NET Aspire comes in.

What Is .NET Aspire?

For several years now, Microsoft has been working on a sidecar system for microservices called Dapr. The goals of Dapr were to enable the same sort of cloud native support that Aspire enables. That project was donated to the Cloud Native Computing Foundation (CNCF) and is a successful open-source project. In the wake of Dapr, the Aspire framework was created by the ASP.NET Core team to solve some of these same problems.

It seems the goal of Aspire is to simplify the set-up of multiple smaller projects (microservices or not) with related components that you might be using (e.g., data stores, containers, queues, message buses, etc.). This seems especially true for working with related Azure resources (e.g., Key Vault, Blob Storage, Service Bus, etc.). This means that you can compose applications to include projects that you build with common resources for your applications (on premises or in the cloud). Primarily, it's focused on several of the pain points including:

  • Orchestration
  • Composing distributed apps
  • Service discovery
  • Configuration
  • Tooling

Before I dig into the details of how this works, let's start by looking at how you can create Aspire apps.

Tooling

In this early version of Microsoft Aspire, most of the tooling is in Visual Studio (although the dotnet CLI supports this too). Like many other projects, there's a new file template, as seen in Figure 3:

Figure 3: New .NET Aspire project templates
Figure 3: New .NET Aspire project templates

Out of the box, Visual Studio supports a simple “Application” and a “Starter Application.” The difference is how much boilerplate and sample code it includes. The Starter Application is a great way to see a multi-component project work, but I think most people will start with the simple Application as a start to a distributed application. I don't think that either of these will be the most common approach.

Instead of that, most people will just add .NET Aspire to existing projects that they want to use orchestration with. Visual Studio has this option by just right-clicking your project, as seen in Figure 4.

Figure 4: Adding .NET Aspire to an existing project
Figure 4: Adding .NET Aspire to an existing project

In both cases, you get two projects that are centered around .NET Aspire, as shown in Figure 5.

Figure 5: .NET Aspire projects
Figure 5: .NET Aspire projects

The AppHost project is both a dashboard of your running distributed app as well as where the orchestration is configured.

On the other hand, the ServiceDefaults project is a library project that creates some defaults that can be used by one or more of your services to configure commonly used services, such as Instrumentation, Metrics, HealthChecks, and others. It's simply a project you can reference to set up these different facilities for your projects, for example, when you call the service defaults in your projects (e.g., API or Blazor projects):

// Aspire Wiring
builder.AddServiceDefaults();
builder.AddRedisOutputCache("theCache");
builder.AddSqlServerDbContext<BookContext>("theDb");

Behind this small little AddServiceDefaults method is a lot of code. I encourage you to look at the code it generates, as it configures a lot of useful services that all your apps can use. A lot of these are used by the dashboard inside the AppHost. Let's take a look at the dashboard next.

The Application Dashboard

When you run a .NET Aspire application, it launches a dashboard that you can use to monitor the applications in your project. For example, Figure 6 shows the initial dashboard.

Figure 6: The dashboard
Figure 6: The dashboard

From this view, you can launch the endpoints, and view the environments and logs to see how each service is being launched. In addition, you can use the Monitoring options on the left to view traces and metrics (that were defined in the Service Defaults) to see how the application is doing in real time, as seen in Figure 7.

Figure 7: Tracing the distributed application
Figure 7: Tracing the distributed application

You can even see performance metrics in Figure 8.

Figure 8: Metrics of your distributed application
Figure 8: Metrics of your distributed application

Now you've seen how the tooling and the dashboard work, but I think it's important to understand how .NET Aspire can manage each part of your distributed applications. Let's see that next.

Orchestration

The job of orchestration is to think of your application as a single slate of services. This means being able to deploy an entire set of services and dependencies as a single entity. When you think about a microservice implementation, one of the difficult things is to handle connecting the different services. To do this, Microsoft Aspire creates something called an AppHost, which is responsible for orchestrating your application. As you can see in Figure 9, the AppHost represents a context around which the services are housed.

Figure 9: The AppHost's relationship to the other services
Figure 9: The AppHost's relationship to the other services

Do not confuse the context of the AppHost as a container or wrapper that holds all of the components together, but instead as a conductor of an orchestra. It's responsible for starting, configuring, and connecting the difference services together.

How Orchestration Works

If you've used ASP.NET Core, you'd usually create an application by using WebApplication.CreateBuilder(). For Aspire, you can do this in a similar fashion:

// AppHost
var builder = DistributedApplication.CreateBuilder(args);

Instead of wiring up services and a pipeline, you add the parts of your application that you need. For example:

builder.AddRedisContainer("theCache");

builder.AddProject<AddressBook_Api>("theapi");
builder.AddProject<AddressBook_Blazor>("frontend");

builder
  .Build()
  .Run();

This is an additional project that's run to orchestrate the entire application. In this example, you're orchestrating the three components (or services) together. The types of components that Aspire supports is quite varied. This doesn't mean that you are limited to just those components, either. In the preview, there are a lot of supported components. Many of these include simplified hosting or Azure hosting, but Microsoft is working with teams across cloud providers (i.e., Google and AWS) to add components for those services too.

As of the preview, you can already use some of the most common components. A sampling of these can be seen in Table 1.

Because .NET Aspire can also be used to deploy to Azure, there are components specific to Azure resources. Some of these common Azure components can be seen in Table 2.

Now you know how to compose the distributed app by using separate components, but how do you handle connecting the apps?

Service Discovery

In the earlier example, you can just add a Redis container in setting up the builder. What does that mean? When this application is run, it deploys a standard Redis docker container and can connect it to your project. For example, when you add the API project, you could send it a reference to the Redis container:

// AppHost
builder.AddRedisContainer("theCache");

builder.AddProject<AddressBook_Api>("theApi").WithReference(cache)

This allows the other applications to use the cache using similar calls in the project setup:

// API Project
builder.AddRedisOutputCache("theCache");

Note that the cache uses the name that was defined when you added Redis. In this way, the connection to the Redis isn't needed in the API project. This is what is meant by Service Discovery. It works identically for Blazor apps, too. This means that you have to deal with shared configuration less often. For project types that don't have native support for Aspire, you can still coordinate them through environment variables:

builder.AddNpmApp("frontEnd", 
                  "../addressbookapp", 
                  "dev")
  .WithReference(api);

The call With.Reference shares the api service's URL to the app using environment variables. For example, this Vue app can use the environment variable (assuming you're going to run this as a server-side node project):

// https://vitejs.dev/config/export default defineConfig({
    ...
    define: {
        "process.env.APP_URL": JSON.stringify(
          process.env["services__theApi__1"]
        )
    },
    ...
})

Where Are We?

To be sure, these are early days in the lifecycle of .NET Aspire. I'm impressed. Instead of always needing to drop down into multiple containers and maybe even Kubernetes, .NET now has an orchestration engine for its own distributed applications. I'm sincerely curious to see what this looks like when it ships.

Table 1: Common components

Component TypeMethod to Add the Service
Docker containerAddDocker()
MongoDb serverAddMongoDb()
MySql serverAddMySql()
Postgres serverAddPostgres()
SQL serverAddSqlServer()
RedisAddRedisContainer()
C# projectAddProject()
RabbitMQAddRabbitMQ()

Table 2: Azure components

Component TypeMethod to Add the ServiceNuGet Package
Blob storageAddAzureBlobService()Aspire.Azure.Storage.Blobs
KeyVaultAddAzureKeyVaultSecrets()Aspire.Microsoft.Azure.Cosmos
Table storageAddAzureTableService()Aspire.Azure.Security.KeyVault
Service busAddAzureServiceBus()Aspire.Azure.Messaging.ServiceBus
CosmosDBAddAzureCosmosDB()Aspire.Azure.Data.Tables