title | description | ms.date | ms.topic | uid |
---|---|---|---|---|
.NET Aspire orchestration overview |
Learn the fundamental concepts of .NET Aspire orchestration and explore the various APIs for adding resources and expressing dependencies. |
11/08/2024 |
overview |
dotnet/aspire/app-host |
.NET Aspire provides APIs for expressing resources and dependencies within your distributed application. In addition to these APIs, there's tooling that enables several compelling scenarios. The orchestrator is intended for local development purposes and isn't supported in production environments.
Before continuing, consider some common terminology used in .NET Aspire:
- App model: A collection of resources that make up your distributed application (xref:Aspire.Hosting.DistributedApplication), defined within the xref:Aspire.Hosting.ApplicationModel namespace. For a more formal definition, see Define the app model.
- App host/Orchestrator project: The .NET project that orchestrates the app model, named with the *.AppHost suffix (by convention).
- Resource: A resource is a dependent part of an application, such as a .NET project, container, executable, database, cache, or cloud service. It represents any part of the application that can be managed or referenced.
- Integration: An integration is a NuGet package for either the app host that models a resource or a package that configures a client for use in a consuming app. For more information, see .NET Aspire integrations overview.
- Reference: A reference defines a connection between resources, expressed as a dependency using the xref:Aspire.Hosting.ResourceBuilderExtensions.WithReference* API. For more information, see Reference resources or Reference existing resources.
Note
.NET Aspire's orchestration is designed to enhance your local development experience by simplifying the management of your cloud-native app's configuration and interconnections. While it's an invaluable tool for development, it's not intended to replace production environment systems like Kubernetes, which are specifically designed to excel in that context.
.NET Aspire empowers you to seamlessly build, provision, deploy, configure, test, run, and observe your distributed applications. All of these capabilities are achieved through the utilization of an app model that outlines the resources in your .NET Aspire solution and their relationships. These resources encompass projects, executables, containers, and external services and cloud resources that your app depends on. Within every .NET Aspire solution, there's a designated App host project, where the app model is precisely defined using methods available on the xref:Aspire.Hosting.IDistributedApplicationBuilder. This builder is obtained by invoking xref:Aspire.Hosting.DistributedApplication.CreateBuilder%2A?displayProperty=nameWithType.
// Create a new app model builder
var builder = DistributedApplication.CreateBuilder(args);
// TODO:
// Add resources to the app model
// Express dependencies between resources
builder.Build().Run();
The app host project handles running all of the projects that are part of the .NET Aspire project. In other words, it's responsible for orchestrating all apps within the app model. The project itself is a .NET executable project that references the 📦 Aspire.Hosting.AppHost NuGet package, sets the IsAspireHost
property to true
, and references the .NET Aspire SDK:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<IsAspireHost>true</IsAspireHost>
<!-- Omitted for brevity -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
</ItemGroup>
<!-- Omitted for brevity -->
</Project>
The following code describes an app host Program
with two project references and a Redis cache:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var apiservice = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireApp_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WaitFor(cache)
.WithReference(apiService)
.WaitFor(apiService);
builder.Build().Run();
The preceding code:
- Creates a new app model builder using the xref:Aspire.Hosting.DistributedApplication.CreateBuilder%2A method.
- Adds a Redis
cache
resource named "cache" using the xref:Aspire.Hosting.RedisBuilderExtensions.AddRedis* method. - Adds a project resource named "apiservice" using the xref:Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject%2A method.
- Adds a project resource named "webfrontend" using the xref:Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject%2A method.
- Specifies that the project has external HTTP endpoints using the xref:Aspire.Hosting.ResourceBuilderExtensions.WithExternalHttpEndpoints%2A method.
- Adds a reference to the
cache
resource and waits for it to be ready using the xref:Aspire.Hosting.ResourceBuilderExtensions.WithReference%2A andWaitFor
methods. - Adds a reference to the
apiservice
resource and waits for it to be ready using the xref:Aspire.Hosting.ResourceBuilderExtensions.WithReference%2A andWaitFor
methods.
- Builds and runs the app model using the xref:Aspire.Hosting.DistributedApplicationBuilder.Build%2A and xref:Aspire.Hosting.DistributedApplication.Run%2A methods.
The example code uses the .NET Aspire Redis hosting integration.
To help visualize the relationship between the app host project and the resources it describes, consider the following diagram:
:::image type="content" source="../media/app-host-resource-diagram.png" lightbox="../media/app-host-resource-diagram.png" alt-text="The relationship between the projects in the .NET Aspire Starter Application template.":::
Each resource must be uniquely named. This diagram shows each resource and the relationships between them. The container resource is named "cache" and the project resources are named "apiservice" and "webfrontend". The web frontend project references the cache and API service projects. When you're expressing references in this way, the web frontend project is saying that it depends on these two resources, the "cache" and "apiservice" respectively.
.NET Aspire projects are made up of a set of resources. The primary base resource types in the 📦 Aspire.Hosting.AppHost NuGet package are described in the following table:
Method | Resource type | Description |
---|---|---|
xref:Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject%2A | xref:Aspire.Hosting.ApplicationModel.ProjectResource | A .NET project, for example, an ASP.NET Core web app. |
xref:Aspire.Hosting.ContainerResourceBuilderExtensions.AddContainer%2A | xref:Aspire.Hosting.ApplicationModel.ContainerResource | A container image, such as a Docker image. |
xref:Aspire.Hosting.ExecutableResourceBuilderExtensions.AddExecutable%2A | xref:Aspire.Hosting.ApplicationModel.ExecutableResource | An executable file, such as a Node.js app. |
xref:Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter%2A | xref:Aspire.Hosting.ApplicationModel.ParameterResource | A parameter resource that can be used to express external parameters. |
Project resources represent .NET projects that are part of the app model. When you add a project reference to the app host project, the .NET Aspire SDK generates a type in the Projects
namespace for each referenced project. For more information, see .NET Aspire SDK: Project references.
To add a project to the app model, use the xref:Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject%2A method:
var builder = DistributedApplication.CreateBuilder(args);
// Adds the project "apiservice" of type "Projects.AspireApp_ApiService".
var apiservice = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");
A reference represents a dependency between resources. For example, you can probably imagine a scenario where you a web frontend depends on a Redis cache. Consider the following example app host Program
C# code:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
builder.AddProject<Projects.AspireApp_Web>("webfrontend")
.WithReference(cache);
The "webfrontend" project resource uses xref:Aspire.Hosting.ResourceBuilderExtensions.WithReference%2A to add a dependency on the "cache" container resource. These dependencies can represent connection strings or service discovery information. In the preceding example, an environment variable is injected into the "webfronend" resource with the name ConnectionStrings__cache
. This environment variable contains a connection string that the webfrontend
uses to connect to Redis via the .NET Aspire Redis integration, for example, ConnectionStrings__cache="localhost:62354"
.
In some cases, you might want to wait for a resource to be ready before starting another resource. For example, you might want to wait for a database to be ready before starting an API that depends on it. To express this dependency, use the WaitFor
method:
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var postgresdb = postgres.AddDatabase("postgresdb");
builder.AddProject<Projects.AspireApp_ApiService>("apiservice")
.WithReference(postgresdb)
.WaitFor(postgresdb);
In the preceding code, the "apiservice" project resource waits for the "postgresdb" database resource to enter the xref:Aspire.Hosting.ApplicationModel.KnownResourceStates.Running?displayProperty=nameWithType. The example code shows the .NET Aspire PostgreSQL integration, but the same pattern can be applied to other resources.
Other cases might warrant waiting for a resource to run to completion, either xref:Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited?displayProperty=nameWithType or xref:Aspire.Hosting.ApplicationModel.KnownResourceStates.Finished?displayProperty=nameWithType before the dependent resource starts. To wait for a resource to run to completion, use the WaitForCompletion
method:
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var postgresdb = postgres.AddDatabase("postgresdb");
var migration = builder.AddProject<Projects.AspireApp_Migration>("migration")
.WithReference(postgresdb)
.WaitFor(postgresdb);
builder.AddProject<Projects.AspireApp_ApiService>("apiservice")
.WithReference(postgresdb)
.WaitForCompletion(migration);
In the preceding code, the "apiservice" project resource waits for the "migration" project resource to run to completion before starting. The "migration" project resource waits for the "postgresdb" database resource to enter the xref:Aspire.Hosting.ApplicationModel.KnownResourceStates.Running?displayProperty=nameWithType. This can be useful in scenarios where you want to run a database migration before starting the API service, for example.
.NET Aspire hosting integrations and client integrations are both delivered as NuGet packages, but they serve different purposes. While client integrations provide client library configuration for consuming apps outside the scope of the app host, hosting integrations provide APIs for expressing resources and dependencies within the app host. For more information, see .NET Aspire integrations overview: Integration responsibilities.
To express a xref:Aspire.Hosting.ApplicationModel.ContainerResource you add it to an xref:Aspire.Hosting.IDistributedApplicationBuilder instance by calling the xref:Aspire.Hosting.ContainerResourceBuilderExtensions.AddContainer%2A method:
var builder = DistributedApplication.CreateBuilder(args);
var ollama = builder.AddContainer("ollama", "ollama/ollama")
.WithBindMount("ollama", "/root/.ollama")
.WithBindMount("./ollamaconfig", "/usr/config")
.WithHttpEndpoint(port: 11434, targetPort: 11434, name: "ollama")
.WithEntrypoint("/usr/config/entrypoint.sh")
.WithContainerRuntimeArgs("--gpus=all");
For more information, see GPU support in Docker Desktop.
var builder = DistributedApplication.CreateBuilder(args);
var ollama = builder.AddContainer("ollama", "ollama/ollama")
.WithBindMount("ollama", "/root/.ollama")
.WithBindMount("./ollamaconfig", "/usr/config")
.WithHttpEndpoint(port: 11434, targetPort: 11434, name: "ollama")
.WithEntrypoint("/usr/config/entrypoint.sh")
.WithContainerRuntimeArgs("--device", "nvidia.com/gpu=all");
For more information, see GPU support in Podman.
The preceding code adds a container resource named "ollama" with the image "ollama/ollama". The container resource is configured with multiple bind mounts, a named HTTP endpoint, an entrypoint that resolves to Unix shell script, and container run arguments with the xref:Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerRuntimeArgs%2A method.
When the app host is run, the xref:Aspire.Hosting.ApplicationModel.ContainerResource is used to determine what container image to create and start. Under the hood, .NET Aspire runs the container using the defined container image by delegating calls to the appropriate OCI-compliant container runtime, either Docker or Podman. The following commands are used:
- docker container create: Creates a new container from the specified image, without starting it.
- docker container start: Start one or more stopped containers.
These commands are used instead of docker run
to manage attached container networks, volumes, and ports. Calling these commands in this order allows any IP (network configuration) to already be present at initial startup.
- podman container create: Creates a writable container layer over the specified image and prepares it for running.
- podman container start: Start one or more stopped containers.
These commands are used instead of podman run
to manage attached container networks, volumes, and ports. Calling these commands in this order allows any IP (network configuration) to already be present at initial startup.
Beyond the base resource types, xref:Aspire.Hosting.ApplicationModel.ProjectResource, xref:Aspire.Hosting.ApplicationModel.ContainerResource, and xref:Aspire.Hosting.ApplicationModel.ExecutableResource, .NET Aspire provides extension methods to add common resources to your app model. For more information, see Hosting integrations.
It's common to express dependencies between project resources. Consider the following example code:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var apiservice = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireApp_Web>("webfrontend")
.WithReference(cache)
.WithReference(apiservice);
Project-to-project references are handled differently than resources that have well-defined connection strings. Instead of connection string being injected into the "webfrontend" resource, environment variables to support service discovery are injected.
Method | Environment variable |
---|---|
WithReference(cache) |
ConnectionStrings__cache="localhost:62354" |
WithReference(apiservice) |
services__apiservice__http__0="http://localhost:5455" services__apiservice__https__0="https://localhost:7356" |
Adding a reference to the "apiservice" project results in service discovery environment variables being added to the frontend. This is because typically, project-to-project communication occurs over HTTP/gRPC. For more information, see .NET Aspire service discovery.
To get specific endpoints from a xref:Aspire.Hosting.ApplicationModel.ContainerResource or an xref:Aspire.Hosting.ApplicationModel.ExecutableResource, use one of the following endpoint APIs:
- xref:Aspire.Hosting.ResourceBuilderExtensions.WithEndpoint*
- xref:Aspire.Hosting.ResourceBuilderExtensions.WithHttpEndpoint*
- xref:Aspire.Hosting.ResourceBuilderExtensions.WithHttpsEndpoint*
Then call the xref:Aspire.Hosting.ResourceBuilderExtensions.GetEndpoint* API to get the endpoint which can be used to reference the endpoint in the WithReference
method:
var builder = DistributedApplication.CreateBuilder(args);
var customContainer = builder.AddContainer("myapp", "mycustomcontainer")
.WithHttpEndpoint(port: 9043, name: "endpoint");
var endpoint = customContainer.GetEndpoint("endpoint");
var apiservice = builder.AddProject<Projects.AspireApp_ApiService>("apiservice")
.WithReference(endpoint);
Method | Environment variable |
---|---|
WithReference(endpoint) |
services__myapp__endpoint__0=https://localhost:9043 |
The port
parameter is the port that the container is listening on. For more information on container ports, see Container ports. For more information on service discovery, see .NET Aspire service discovery.
In the preceding section, the WithReference
method is used to express dependencies between resources. When service endpoints result in environment variables being injected into the dependent resource, the format might not be obvious. This section provides details on this format.
When one resource depends on another resource, the app host injects environment variables into the dependent resource. These environment variables configure the dependent resource to connect to the resource it depends on. The format of the environment variables is specific to .NET Aspire and expresses service endpoints in a way that is compatible with Service Discovery.
Service endpoint environment variable names are prefixed with services__
(double underscore), then the service name, the endpoint name, and finally the index. The index supports multiple endpoints for a single service, starting with 0
for the first endpoint and incrementing for each endpoint.
Consider the following environment variable examples:
services__apiservice__http__0
The preceding environment variable expresses the first HTTP endpoint for the apiservice
service. The value of the environment variable is the URL of the service endpoint. A named endpoint might be expressed as follows:
services__apiservice__myendpoint__0
In the preceding example, the apiservice
service has a named endpoint called myendpoint
. The value of the environment variable is the URL of the service endpoint.
Some situations warrant that you reference an existing resource, perhaps one that is deployed to a cloud provider. For example, you might want to reference an Azure database. In this case, you'd rely on the Execution context to dynamically determine whether the app host is running in "run" mode or "publish" mode. If you're running locally and want to rely on a cloud resource, you can use the IsRunMode
property to conditionally add the reference. You might choose to instead create the resource in publish mode. Some hosting integrations support providing a connection string directly, which can be used to reference an existing resource.
Likewise, there might be use cases where you want to integrate .NET Aspire into an existing solution. One common approach is to add the .NET Aspire app host project to an existing solution. Within your app host, you express dependencies by adding project references to the app host and building out the app model. For example, one project might depend on another. These dependencies are expressed using the xref:Aspire.Hosting.ResourceBuilderExtensions.WithReference%2A method. For more information, see Add .NET Aspire to an existing .NET app.
The .NET Aspire app host exposes several life cycles that you can hook into by implementing the xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook interface. The following lifecycle methods are available:
Order | Method | Description |
---|---|---|
1 | xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook.BeforeStartAsync%2A | Executes before the distributed application starts. |
2 | xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook.AfterEndpointsAllocatedAsync%2A | Executes after the orchestrator allocates endpoints for resources in the application model. |
3 | xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook.AfterResourcesCreatedAsync%2A | Executes after the resource was created by the orchestrator. |
While the app host provides life cycle hooks, you might want to register custom events. For more information, see Eventing in .NET Aspire.
To register a life cycle hook, implement the xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook interface and register the hook with the app host using the xref:Aspire.Hosting.Lifecycle.LifecycleHookServiceCollectionExtensions.AddLifecycleHook* API:
:::code source="snippets/lifecycles/AspireApp/AspireApp.AppHost/Program.cs":::
The preceding code:
- Implements the xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook interface as a
LifecycleLogger
. - Registers the life cycle hook with the app host using the xref:Aspire.Hosting.Lifecycle.LifecycleHookServiceCollectionExtensions.AddLifecycleHook* API.
- Logs a message for all the events.
When this app host is run, the life cycle hook is executed for each event. The following output is generated:
info: LifecycleLogger[0]
BeforeStartAsync
info: Aspire.Hosting.DistributedApplication[0]
Aspire version: 9.0.0
info: Aspire.Hosting.DistributedApplication[0]
Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
Application host directory is: ..\AspireApp\AspireApp.AppHost
info: LifecycleLogger[0]
AfterEndpointsAllocatedAsync
info: Aspire.Hosting.DistributedApplication[0]
Now listening on: https://localhost:17043
info: Aspire.Hosting.DistributedApplication[0]
Login to the dashboard at https://localhost:17043/login?t=d80f598bc8a64c7ee97328a1cbd55d72
info: LifecycleLogger[0]
AfterResourcesCreatedAsync
info: Aspire.Hosting.DistributedApplication[0]
Distributed application started. Press Ctrl+C to shut down.
The preferred way to hook into the app host life cycle is to use the eventing API. For more information, see Eventing in .NET Aspire.
The xref:Aspire.Hosting.IDistributedApplicationBuilder exposes an execution context (xref:Aspire.Hosting.DistributedApplicationExecutionContext), which provides information about the current execution of the app host. This context can be used to evaluate whether or not the app host is executing as "run" mode, or as part of a publish operation. Consider the following properties:
- xref:Aspire.Hosting.DistributedApplicationExecutionContext.IsRunMode%2A: Returns
true
if the current operation is running. - xref:Aspire.Hosting.DistributedApplicationExecutionContext.IsPublishMode%2A: Returns
true
if the current operation is publishing.
This information can be useful when you want to conditionally execute code based on the current operation. Consider the following example that demonstrates using the IsRunMode
property. In this case, an extension method is used to generate a stable node name for RabbitMQ for local development runs.
private static IResourceBuilder<RabbitMQServerResource> RunWithStableNodeName(
this IResourceBuilder<RabbitMQServerResource> builder)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
builder.WithEnvironment(context =>
{
// Set a stable node name so queue storage is consistent between sessions
var nodeName = $"{builder.Resource.Name}@localhost";
context.EnvironmentVariables["RABBITMQ_NODENAME"] = nodeName;
});
}
return builder;
}
The execution context is often used to conditionally add resources or connection strings that point to existing resources. Consider the following example that demonstrates conditionally adding Redis or a connection string based on the execution context:
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.ExecutionContext.IsRunMode
? builder.AddRedis("redis")
: builder.AddConnectionString("redis");
builder.AddProject<Projects.WebApplication>("api")
.WithReference(redis);
builder.Build().Run();
In the preceding code:
- If the app host is running in "run" mode, a Redis container resource is added.
- If the app host is running in "publish" mode, a connection string is added.
This logic can easily be inverted to connect to an existing Redis resource when you're running locally, and create a new Redis resource when you're publishing.