Introduction

Modern applications are often made up of multiple services and infrastructure components. An application may contain a web frontend, APIs, databases, caches, message brokers, and other supporting services.

Managing all these components during development can become complicated. Developers may need to configure connection strings, service URLs, health checks, logging, and other infrastructure settings before the application can run correctly.

.NET Aspire provides a development experience for building and running distributed cloud-native applications with .NET. It helps developers define application resources, connect services and dependencies, configure common cloud-native features, and inspect the application through the Aspire dashboard.

In this article, we will understand .NET Aspire, explore its main components, and build a simple distributed application using an API and a database resource.

What Is .NET Aspire?

.NET Aspire is a cloud-native development stack and tooling experience for .NET applications.

It is particularly useful for distributed applications where multiple services and infrastructure resources need to work together.

A typical Aspire solution can contain projects such as:

MyAspireApp
│
├── MyAspireApp.AppHost
├── MyAspireApp.ServiceDefaults
├── MyAspireApp.Api
└── MyAspireApp.Web

The exact structure depends on the project template and the requirements of the application.

The AppHost project defines the resources that make up the distributed application. The ServiceDefaults project can provide shared configuration for common concerns such as service discovery, resilience, health checks, and telemetry.

Why Use .NET Aspire?

Building a distributed application locally can require several separate configuration steps.

For example:

Web Application
       |
       v
API Service
       |
       +--------> Database
       |
       +--------> Cache
       |
       +--------> Message Broker

Without a common development model, developers may have to start each dependency separately and maintain configuration for every service.

.NET Aspire provides a way to describe these resources together.

This can make the local development environment easier to understand and reproduce.

Some of the main benefits include:

Understanding the Main Components

.NET Aspire applications commonly use several important components.

AppHost

The AppHost is responsible for describing the distributed application and its resources.

For example:

var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.MyAspireApp_Api>("api");

builder.AddProject<Projects.MyAspireApp_Web>("web")
       .WithReference(api);

builder.Build().Run();

This example defines two application resources:

The web application has a reference to the API.

The AppHost therefore describes the relationship between the services.

ServiceDefaults

The ServiceDefaults project is intended to centralize common configuration shared by application services.

Depending on the generated project and Aspire version, it can include configuration for:

An application can typically enable the shared configuration using:

builder.AddServiceDefaults();

This avoids duplicating common configuration across multiple projects.

Integrations

.NET Aspire provides integrations for different infrastructure services.

Examples can include:

The available integrations depend on the Aspire version and installed integration packages.

Creating a .NET Aspire Application

Start by creating a new .NET Aspire application using the Aspire project template available in your development environment.

A typical Aspire solution contains an AppHost project along with one or more application projects.

The AppHost acts as the entry point for running the distributed application locally.

After creating the solution, open the AppHost project and examine its Program.cs file.

Adding an ASP.NET Core API

Suppose the application contains an ASP.NET Core API.

The API can be added to the AppHost as an application resource:

var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.MyAspireApp_Api>("api");

builder.Build().Run();

Here, AddProject() adds the API project to the distributed application.

The string "api" acts as the resource name.

Additional services can be added in the same AppHost.

For example:

var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.MyAspireApp_Api>("api");

var web = builder.AddProject<Projects.MyAspireApp_Web>("web")
                 .WithReference(api);

builder.Build().Run();

Now the application contains both a web application and an API.

Adding a Database

A distributed application often requires a database.

For example, PostgreSQL can be represented as an Aspire resource:

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres")
                      .AddDatabase("appdb");

var api = builder.AddProject<Projects.MyAspireApp_Api>("api")
                 .WithReference(postgres);

builder.Build().Run();

The exact integration package and API can vary depending on the Aspire version.

The important concept is that the database becomes part of the application's resource model.

The resulting architecture looks like:

              .NET Aspire AppHost
                      |
              +-------+-------+
              |               |
              v               v
           Web App          API
                              |
                              v
                          PostgreSQL

Connecting an Application to the Database

After adding the database resource, the API can consume the connection information through configuration.

For example, an application using PostgreSQL can register a data source:

builder.Services.AddNpgsqlDataSource(
    builder.Configuration.GetConnectionString("appdb")!);

The exact implementation depends on the database provider and data-access technology being used.

The important advantage is that developers do not need to manually hard-code development-specific database addresses throughout the application.

Service Discovery

Distributed applications often need services to communicate with each other.

For example:

Frontend
   |
   v
Order API
   |
   v
Payment API

Hard-coding URLs for these services can make development and deployment more difficult.

Service discovery allows services to refer to other resources using logical names.

For example, the AppHost can define the relationship:

var paymentApi =
    builder.AddProject<Projects.PaymentApi>("payment-api");

builder.AddProject<Projects.OrderApi>("order-api")
       .WithReference(paymentApi);

The application can then use the service discovery configuration provided by the Aspire environment rather than relying on manually maintained development URLs.

Developer-Time Orchestration

One of the main purposes of Aspire is improving the development experience for distributed applications.

Consider an application with:

Web
 |
 +---- API
 |
 +---- Redis
 |
 +---- PostgreSQL

Normally, a developer may need to start and configure these dependencies individually.

With Aspire, these resources can be described in the AppHost and started as part of the distributed application.

This provides a central view of the application's resources during local development.

It is important to distinguish this developer-time orchestration from production orchestration. Aspire does not simply replace production platforms such as Kubernetes in every scenario.

Aspire Dashboard

When an Aspire application is running, developers can use the Aspire dashboard to inspect the application's resources and telemetry.

Depending on the configured services and telemetry, the dashboard can provide information about:

For a distributed application, this can be useful when investigating problems that cross multiple services.

Example: Troubleshooting a Slow Request

Consider the following application:

Browser
   |
   v
Web Application
   |
   v
Order API
   |
   v
Database

Suppose a user reports that loading an order page is slow.

Instead of checking each application independently, a developer can investigate the request across the distributed application.

The request might conceptually look like:

Browser Request
      |
      v
Web Application
      |
      | 120 ms
      v
Order API
      |
      | 90 ms
      v
Database

The telemetry can help determine which part of the request is taking the most time.

The values above are only illustrative. Actual performance depends on the application, infrastructure, database queries, network conditions, and workload.

Health Checks

A service can be running while one of its dependencies is unavailable.

For example:

API Process: Running
Database: Unavailable

A health check can help expose this state.

A basic health-check registration can look like:

builder.Services.AddHealthChecks();

Applications can also register checks for specific dependencies.

Health information is useful when monitoring distributed applications because it provides more information than simply knowing whether a process is running.

Observability in .NET Aspire

Observability is an important part of distributed application development.

The three commonly discussed telemetry signals are:

Metrics

Metrics provide numerical measurements such as:

Logs

Logs provide detailed information about application events and errors.

Traces

Distributed traces show how a request moves between services.

For example:

User Request
     |
     v
Web Application
     |
     v
Order API
     |
     v
Database

Tracing can help developers understand where time is being spent during a distributed request.

.NET Aspire works with the .NET observability ecosystem and provides a dashboard experience for inspecting telemetry during development.

.NET Aspire and Resilience

Distributed applications can experience temporary failures.

For example:

Order API
    |
    X
Payment API

A network request may fail temporarily because of a transient network problem or service availability issue.

Resilience strategies can help applications handle certain transient failures.

Depending on the application and generated Aspire configuration, developers can use resilience mechanisms such as:

These mechanisms should be configured according to the actual behavior of the dependency. Retrying every failure indiscriminately can make an existing problem worse.

.NET Aspire and Containers

Containers are frequently used in cloud-native applications.

Aspire can work with containerized resources during development.

A distributed application could therefore contain:

AppHost
   |
   +---- API
   |
   +---- PostgreSQL Container
   |
   +---- Redis Container

This allows developers to define infrastructure dependencies as part of the development environment.

Container configuration still needs to be designed appropriately for the target deployment environment.

.NET Aspire Integrations

Integrations allow applications to work with infrastructure and services through a consistent development model.

Examples include:

For example, a database resource can be added to the AppHost and referenced by an application:

var database = builder.AddPostgres("postgres")
                      .AddDatabase("applicationdb");

builder.AddProject<Projects.MyAspireApp_Api>("api")
       .WithReference(database);

This creates an explicit relationship between the API and database resources.

.NET Aspire and Production Deployment

.NET Aspire provides a strong development experience, but production deployment requires additional planning.

Before deploying a distributed application, teams need to consider:

The development environment and production environment do not necessarily have to use the same infrastructure.

The important goal is to make the application's dependencies explicit and manageable.

Common Mistakes

Treating Aspire as a Traditional Web Framework

Aspire is not a replacement for ASP.NET Core.

ASP.NET Core is used to build web applications and APIs, while Aspire provides tooling and an application model for distributed cloud-native development.

Hard-Coding Service URLs

Avoid hard-coding development URLs when services can use service discovery.

This makes the application easier to configure across environments.

Hard-Coding Secrets

Database passwords, API keys, and other secrets should not be stored directly in source code.

Use appropriate configuration and secret-management mechanisms.

Assuming Local Development Equals Production

A local Aspire environment is designed to improve development and testing.

Production infrastructure should be evaluated separately based on reliability, security, scalability, and operational requirements.

Collecting Unnecessary Telemetry

More telemetry is not always better.

Collect metrics, logs, and traces that help developers and operators understand the behavior of the system while controlling unnecessary data volume.

Best Practices

When working with .NET Aspire:

  1. Keep application composition in the AppHost.

  2. Keep business logic inside application services.

  3. Use service discovery instead of hard-coded development URLs.

  4. Keep secrets outside source code.

  5. Add health checks for important dependencies.

  6. Use structured logging.

  7. Use distributed tracing for service-to-service communication.

  8. Configure resilience according to dependency behavior.

  9. Keep infrastructure dependencies explicit.

  10. Verify Aspire integration compatibility with the version of .NET and Aspire being used.

Example Project Structure

A simple distributed application can be organized as:

MyAspireApp/
│
├── MyAspireApp.AppHost/
│   └── Program.cs
│
├── MyAspireApp.ServiceDefaults/
│   └── Extensions.cs
│
├── MyAspireApp.Api/
│   ├── Program.cs
│   └── Controllers/
│
└── MyAspireApp.Web/
    ├── Program.cs
    └── Pages/

The exact structure can vary depending on the project template and application requirements.

Conclusion

.NET Aspire provides a development experience designed for distributed and cloud-native .NET applications.

Its AppHost allows developers to describe application services and infrastructure resources in one place. Integrations make it easier to connect services to resources such as databases and caches, while service discovery reduces the need to manage development-specific service URLs manually.

The Aspire dashboard also provides a convenient way to inspect resources, logs, metrics, traces, and health information during development.

The main value of .NET Aspire is not simply adding another framework to a .NET application. It is providing a consistent way to compose, run, connect, and observe distributed application components during development.

For developers working on applications containing multiple services and infrastructure dependencies, understanding Aspire can make the local development and troubleshooting experience considerably easier.