Building Distributed Applications with .NET Aspire: Orchestration, Service Discovery, and Observability
Bring your ASP.NET Core services, dependencies, configuration, and telemetry together with .NET Aspire
Distributed applications give us flexibility and scalability, but they also introduce a frustrating amount of operational complexity. An ASP.NET Core application may depend on several APIs, databases, caches, message brokers, and background workers, each with its own endpoint, configuration, startup requirements, and telemetry. .NET Aspire helps bring these moving pieces together. It provides a code-first way to describe a distributed application, connect its resources, discover services, coordinate startup, and observe the entire system from one place.
In this guide, we'll build a practical mental model of .NET Aspire and explore how orchestration, service discovery, health checks, and OpenTelemetry can make distributed ASP.NET Core development much easier.
The Problem with Distributed Applications
A traditional ASP.NET Core application might be relatively simple.
Browser
↓
ASP.NET Core
↓
DatabaseAs the application grows, that picture changes.
We might introduce:
Web Frontend
↓
API Gateway
↓
┌───┼─────────┐
↓ ↓ ↓
Orders Products Payments
↓ ↓ ↓
SQL Redis Message QueueSoon, running the application locally means starting several projects and infrastructure dependencies.
Developers need to know which ports services use.
Configuration files contain connection strings.
One service must start before another.
Databases and containers need to be running.
Telemetry is spread across multiple consoles.
The architecture may be well designed, but the development experience becomes increasingly difficult.
This is one of the problems Aspire is designed to solve.
What Is .NET Aspire?
Aspire provides tooling for building and operating distributed applications, with a strong emphasis on orchestration and observability.
Rather than treating each application, database, cache, and supporting service as an unrelated component, Aspire lets us describe the entire system as a collection of connected resources.
Microsoft currently describes Aspire as a code-first orchestration and observability layer for distributed applications.
That distinction matters.
Aspire is not another web framework replacing ASP.NET Core.
Your APIs remain ASP.NET Core applications.
Your background workers remain .NET workers.
Redis remains Redis.
PostgreSQL remains PostgreSQL.
Aspire provides a model that connects these components and makes the distributed application easier to develop and understand.
Think of Aspire as the Application Coordinator
Imagine an orchestra.
The musicians already know how to play their instruments.
The conductor does not replace them.
Instead, the conductor coordinates when everyone starts, how the pieces fit together, and how the entire performance behaves.
Aspire plays a similar role.
Your services already know how to perform their individual jobs.
Aspire helps coordinate the system around them.
The AppHost
The center of an Aspire application is the AppHost.
The AppHost describes your distributed application.
Instead of maintaining a collection of disconnected scripts and manually configured ports, you describe your resources and relationships in code.
A simplified AppHost might look like this:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.CatalogApi>("catalog");
builder.AddProject<Projects.WebApp>("web")
.WithReference(api);
builder.Build().Run();We now have two resources:
Catalog API
↑
│
Web AppThe important part is the relationship.
The web application references the API.
Aspire can use that relationship to provide connection information and service discovery rather than requiring the application to depend on a manually configured port.
Microsoft’s current Aspire guidance describes the AppHost as a code-first model for defining application architecture and relationships.
Resources Are More Than ASP.NET Core Projects
An Aspire resource does not have to be an ASP.NET Core project.
A distributed application might contain:
ASP.NET Core APIs
Worker services
Databases
Message brokers
Containers
Executables
Frontend applications
The AppHost becomes a map of the entire application.
For example:
Web
│
▼
Catalog API
/ \
▼ ▼
PostgreSQL RedisInstead of expecting every developer to reconstruct this architecture mentally, we can express it directly.
Adding Infrastructure
Suppose our Catalog API requires Redis.
Conceptually, our AppHost might define the cache and reference it from the API:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var api = builder
.AddProject<Projects.CatalogApi>("catalog")
.WithReference(cache);
builder.Build().Run();WithReference() tells Aspire that the API depends on the cache.
Aspire integrations can then provide the consuming application with the connection information it needs.
This is far cleaner than telling every developer:
“Start Redis manually, make sure it uses this port, then copy this connection string into your local configuration.”
Why Code-First Orchestration Matters
Infrastructure relationships often exist only in documentation.
That documentation becomes outdated.
Someone changes a port.
A new database is introduced.
A service gets renamed.
Another developer joins the team and spends half a day figuring out how everything fits together.
With Aspire, the application topology lives alongside the application.
That makes relationships:
Visible
Version controlled
Repeatable
Easier to modify
The architecture becomes something developers can run rather than something they merely read about.
Orchestration Does Not Mean Business Orchestration
There is an important distinction here.
Aspire application orchestration is not the same thing as orchestrating business workflows.
For example, Aspire does not replace the Saga pattern we explored earlier.
A Saga might coordinate:
Create Order
↓
Reserve Stock
↓
Process Payment
↓
Arrange ShippingAspire coordinates the resources required to run the application.
Start Database
↓
Start API
↓
Start FrontendThese solve completely different problems.
Controlling Startup Dependencies
Distributed applications often suffer from startup races.
Imagine the frontend starts immediately and calls the API.
The API is still starting.
The first request fails.
Or perhaps the API starts before its database is ready.
Aspire allows resource relationships to express readiness dependencies.
For example:
var api = builder
.AddProject<Projects.CatalogApi>("catalog");
builder
.AddProject<Projects.WebApp>("web")
.WithReference(api)
.WaitFor(api);WaitFor() can prevent the dependent resource from starting until the referenced resource is ready according to its lifecycle or health state. Microsoft’s current Aspire getting-started guidance demonstrates this alongside health checks to prevent startup race conditions.
This sounds like a small improvement.
Across a system with many dependencies, it becomes extremely valuable.
Service Discovery
Now we reach one of the biggest headaches in distributed development.
How does one service find another?
Suppose our Catalog API runs locally at:
https://localhost:7143
A developer might configure:
{
"CatalogApi": "https://localhost:7143"
}But another developer might receive:
https://localhost:7288
Production uses a completely different hostname.
Containers introduce another network environment.
Hardcoded addresses become fragile quickly.
Logical Service Names
Service discovery solves this problem.
Instead of thinking:
Call https://localhost:7143the application thinks:
Call catalogAspire knows where catalog actually lives.
A client can therefore use a logical service address rather than a hardcoded endpoint.
For example:
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress =
new Uri("https+http://catalog");
});Aspire’s service discovery resolves the logical service name to the appropriate endpoint at runtime. Microsoft’s documentation specifically describes this model as using logical names instead of hardcoded URLs.
Why This Changes Local Development
Imagine five developers working on the same system.
Without service discovery, everyone needs matching port configuration.
With service discovery:
Web
↓
"catalog"
↓
Service Discovery
↓
Actual Catalog EndpointThe consuming code no longer cares whether Catalog runs:
localhost:5237or somewhere else entirely.
The same basic service relationship can work across different environments because endpoint resolution is separated from application logic.
Adding a Database
Let’s make the example more realistic.
Suppose the Catalog API needs PostgreSQL.
The AppHost can model that dependency too.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var catalogDb =
postgres.AddDatabase("catalogdb");
var catalog = builder
.AddProject<Projects.CatalogApi>("catalog")
.WithReference(catalogDb);
builder.AddProject<Projects.WebApp>("web")
.WithReference(catalog)
.WaitFor(catalog);
builder.Build().Run();Our topology is now:
Web
↓
Catalog API
↓
PostgreSQL
↓
Catalog DatabaseThe code itself explains the relationship.
Adding Redis
Now suppose product information is frequently requested.
We introduce Redis.
var cache = builder.AddRedis("cache");
var catalog = builder
.AddProject<Projects.CatalogApi>("catalog")
.WithReference(catalogDb)
.WithReference(cache);The architecture becomes:
Web
│
▼
Catalog API
/ \
▼ ▼
PostgreSQL RedisNotice what is happening.
We aren’t adding Redis-specific orchestration logic to our web application.
We’re describing the dependency at the application-model level.
Aspire Integrations
Aspire provides integrations for many common infrastructure components and services.
These integrations help model resources and provide appropriate configuration to applications consuming them.
The important idea is not memorizing a list of integrations.
It is understanding the pattern:
Define Resource
↓
Reference Resource
↓
Aspire Supplies Configuration
↓
Application Uses ResourceThis creates a much more consistent way to wire distributed applications together.
Service Defaults
Distributed applications tend to repeat the same infrastructure configuration.
Every service needs:
Telemetry
Service discovery
Resilience configuration
Copying the same setup into every project creates maintenance problems.
Aspire addresses this through Service Defaults.
A Service Defaults project centralizes common configuration that can be reused across projects in the solution.
Microsoft’s Aspire templates describe Service Defaults as reusable configuration for areas including resilience, service discovery, and telemetry.
Why Service Defaults Matter
Imagine ten ASP.NET Core services.
Without shared defaults:
Orders → configure telemetry
Payments → configure telemetry
Catalog → configure telemetry
Shipping → configure telemetry
Identity → configure telemetrySomeone eventually configures one differently.
With shared defaults:
Service Defaults
/ / | \ \
↓ ↓ ↓ ↓ ↓
Orders Payments Catalog Shipping IdentityThe teams begin with a consistent baseline.
Individual applications can still add their own configuration where necessary.
Observability from the Beginning
Distributed applications are difficult to debug because a single user action may cross several services.
Imagine a checkout request:
Browser
↓
Web
↓
Orders API
↓
Inventory API
↓
Payment API
↓
DatabaseThe user sees:
Checkout failed.Where did it fail?
Without distributed observability, developers may need to inspect several log files and manually correlate timestamps.
Aspire encourages observability as part of the development experience rather than something added just before production.
The Aspire Dashboard
One of Aspire’s most useful features is its dashboard.
When you run an AppHost, the dashboard provides a centralized view of the resources in your distributed application.
Instead of switching between terminal windows, developers get a place to inspect the system.
The dashboard works with OpenTelemetry data and can surface logs, traces, metrics, resource information, and endpoints. Microsoft’s documentation notes that the dashboard launches automatically in common AppHost development workflows.
This dramatically improves the local debugging experience.
Structured Logs
Suppose the Orders API produces:
logger.LogInformation(
"Processing order {OrderId}",
order.Id);Structured logging keeps OrderId as meaningful data rather than burying it inside a text string.
Across distributed services, this becomes particularly useful because developers can search and correlate events much more effectively.
Distributed Traces
We explored OpenTelemetry and distributed tracing earlier in this series.
Aspire makes those concepts immediately useful during development.
Suppose a request travels:
Web
│
└── Orders API 35 ms
│
├── Inventory 18 ms
│
└── Payments 840 msThe problem becomes obvious.
Payments is taking most of the time.
Instead of guessing which service caused the delay, the trace shows the request’s journey.
Metrics
Logs tell us what happened.
Traces show how a request moved.
Metrics tell us how the system behaves over time.
Useful metrics might include:
Requests per second
Request duration
Failed requests
CPU usage
Memory usage
Dependency latencyAspire’s Service Defaults can configure OpenTelemetry collection for logging, tracing, and metrics, while OTLP allows telemetry to be exported to compatible monitoring systems.
Health Checks
Observability tells us what is happening.
Health checks answer another important question:
Can this service actually do its job?
An application process may technically be running while its database is unavailable.
That service isn’t truly healthy.
ASP.NET Core health checks can represent conditions such as:
API running ✓
Database reachable ✓
Redis reachable ✓
Message broker ✗Aspire can incorporate health information into resource orchestration and visibility.
A More Complete Example
Let’s imagine we’re building an online store.
Our system contains:
Web Frontend
Catalog API
Orders API
PostgreSQL
RedisThe AppHost could conceptually describe the system like this:
var builder =
DistributedApplication.CreateBuilder(args);
var postgres =
builder.AddPostgres("postgres");
var ordersDb =
postgres.AddDatabase("ordersdb");
var cache =
builder.AddRedis("cache");
var catalog =
builder.AddProject<Projects.CatalogApi>("catalog")
.WithReference(cache);
var orders =
builder.AddProject<Projects.OrdersApi>("orders")
.WithReference(ordersDb)
.WithReference(catalog)
.WaitFor(ordersDb);
builder.AddProject<Projects.WebApp>("web")
.WithReference(catalog)
.WithReference(orders)
.WaitFor(catalog)
.WaitFor(orders);
builder.Build().Run();Now look at what the application model communicates.
Web
/ \
▼ ▼
Catalog Orders
│ / \
▼ ▼ ▼
Redis DB CatalogSomeone joining the project can understand much of the topology simply by reading the AppHost.
Adding a New Developer
This is where Aspire can make a noticeable difference.
Imagine joining a project containing eight services.
Traditionally, onboarding instructions might say:
Install PostgreSQL.
Install Redis.
Configure these connection strings.
Start Orders first.
Start Catalog.
Make sure Catalog uses port 7182.
Start the frontend.
Open three terminals for logs.
Configure your tracing environment.
That is a lot of tribal knowledge.
A well-designed Aspire application can encode much more of that setup in the application model.
The developer starts the AppHost.
The system topology comes to life.
Aspire and Containers
Containers remain useful.
Aspire does not make Docker irrelevant.
Instead, it can model containerized dependencies alongside your application projects.
For local development, that means infrastructure such as Redis or PostgreSQL can participate in the same application model rather than being treated as completely separate setup tasks.
This complements the Docker and Kubernetes concepts we’ve already explored in this series.
Aspire and Kubernetes
Another important distinction:
Aspire is not Kubernetes.
Kubernetes is an orchestration platform for deploying and operating containerized workloads.
Aspire’s AppHost provides a code-first application model that can be used during development and can participate in deployment workflows.
You should not think:
Aspire OR KubernetesThink:
Application Model
↓
Deployment EnvironmentThe deployment environment might eventually involve containers, Kubernetes, Azure services, or another supported target.
Aspire’s current documentation explicitly separates application modeling from deployment topology and supports multiple deployment directions rather than tying the model to a single hosting destination.
Aspire Is Not Only for New Applications
You do not need to rebuild an existing ASP.NET Core system from scratch.
Aspire can be introduced incrementally.
For example, you might start by adding an AppHost that models:
Existing API
Existing Worker
Redis
PostgreSQLThen introduce common telemetry.
Later, add service discovery.
Then model more infrastructure.
Microsoft’s current guidance specifically supports adding Aspire to existing applications and gradually modeling containers, databases, caches, queues, workers, and other resources.
That makes adoption much less risky.
Where Aspire Helps Most
Aspire becomes particularly valuable when an application contains enough moving parts that local development starts becoming painful.
Imagine:
Frontend
API Gateway
Identity Service
Catalog Service
Orders Service
Payment Service
Worker
Redis
PostgreSQL
RabbitMQRunning all of that manually is frustrating.
Understanding it is harder.
Debugging it is harder still.
The value of a unified application model grows with the number of dependencies.
Where Aspire May Be Unnecessary
Not every ASP.NET Core application needs distributed orchestration.
Suppose you have:
Razor Pages App
↓
SQL DatabaseThe system is simple.
Adding additional tooling may provide little value.
This is an important architectural lesson we’ve repeated throughout this series:
Use complexity to solve complexity. Do not introduce complexity merely because a technology is interesting.
Common Mistake: Treating Aspire as Magic
Aspire does not make distributed systems simple.
Network failures still happen.
Databases still become unavailable.
Services still experience latency.
Distributed transactions are still difficult.
Security still matters.
What Aspire improves is our ability to model, connect, run, and observe those systems.
Good architecture remains essential.
Common Mistake: Hiding Architecture
Because Aspire makes adding resources convenient, developers may be tempted to keep adding services.
That would be the wrong lesson.
If one ASP.NET Core application solves the problem well, splitting it into six services does not automatically improve the architecture.
The AppHost should describe a sensible architecture.
It should not justify an unnecessarily complicated one.
Common Mistake: Ignoring Production Differences
A beautiful local environment does not guarantee a healthy production environment.
Production introduces:
Real network latency
Secrets
Scaling
Regional failures
Cost
Resource limits
Use Aspire to improve development and application modeling, but continue designing explicitly for the environment where the system will actually operate.
A Real-World Scenario
Imagine a logistics company building a parcel-processing platform.
Its system contains:
Operations Portal
↓
Shipment API
↓
┌─────┼──────────┐
▼ ▼ ▼
Pricing Tracking Notifications
│ │
▼ ▼
Redis PostgreSQL
Background Workers
↓
Message BrokerWithout a unified development model, every engineer must understand how to launch and configure all of these pieces.
With Aspire, the AppHost describes them as resources and relationships.
Developers start the distributed application.
Service discovery connects the services.
Health checks reveal whether dependencies are ready.
OpenTelemetry records activity.
The dashboard exposes the system’s behavior.
When a shipment request becomes slow, developers inspect its trace.
They discover that Pricing is waiting on a downstream dependency.
Instead of searching through five terminal windows, they can follow the request through the distributed system.
That is where Aspire becomes much more than a convenient project launcher.
It becomes a development environment for understanding the architecture itself.
How This Fits Our ASP.NET Core Journey
Over the course of this series, we’ve explored many pieces of distributed application architecture individually.
We’ve covered:
Microservices
Docker
Kubernetes
Event-driven architecture
gRPC
Health checks
Resilience
Distributed caching
Distributed tracing
API Gateways
Service meshes
Internal Developer Platforms
Aspire does not replace those concepts.
It provides a way to bring many of them into a more coherent developer experience.
Instead of thinking about an application as ten unrelated projects and five infrastructure components, we can model it as one distributed system.
That is an important shift.
What Aspire Gives Us
The easiest way to remember Aspire is through four questions.
What runs?
The AppHost describes the resources.
What depends on what?
References describe relationships.
How do services find each other?
Service discovery resolves logical names to endpoints.
How do we understand what is happening?
OpenTelemetry and the Aspire dashboard provide observability.
Together, those capabilities attack some of the most frustrating parts of distributed application development.
Closing Thoughts
Distributed architecture gives development teams powerful ways to scale applications, isolate responsibilities, and choose the right technology for each workload. But every new service also creates another endpoint, dependency, configuration problem, health state, and source of telemetry.
Aspire tackles that operational complexity from the developer’s perspective.
With a code-first AppHost, we can describe our distributed application as a system rather than a collection of unrelated processes. Service discovery removes the need to hardcode changing endpoints. Shared defaults give services a consistent foundation for resilience, health checks, and telemetry. OpenTelemetry and the Aspire dashboard then help us see how those services behave together.
The result is not a simpler architecture by magic.
It is an architecture that is easier to run, easier to understand, easier to observe, and easier for the next developer to work with.
And as our ASP.NET Core systems become increasingly distributed, that developer experience matters just as much as the individual services themselves.
Subscribe Now
Enjoying the series? Subscribe to ASP Today for practical ASP.NET Core tutorials, advanced architecture guides, and real-world .NET engineering strategies. Join our Substack Chat to discuss distributed applications, share development experiences, and connect with developers building modern ASP.NET Core systems.


