Audience – Aspire integrators, advanced users, and contributors who are defining custom resource types, implementing publishers, or working across both runtime and publish workflows.
Just getting started? Jump straight to Quick Start and come back later for the deep‑dive.
A two‑minute "hello‑world" that shows the happy path.
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("pg");
var api = builder.AddProject("api").WithReference(db);
var web = builder.AddNpmApp("web").WithReference(api);
builder.Build().Run();
%% Graph showing resource dependencies
graph LR
web --> api
api --> pg
- Use
AddXyz
helper methods to declare resources (e.g.,AddPostgres
,AddProject
). - Use
.WithReference()
(or similar) to wire explicit dependencies between resources. - Call
Build().Run()
– Aspire builds the application model (graph) and executes it, handling port allocation, environment variables, and startup order.
- Quick Start
- Resource Basics
- Built‑In Resources & Lifecycle
- Standard Interfaces
- Resource Hierarchy and Parent‑Child Relationships
- Values and References
- ReferenceExpression
- Endpoint Primitives
- Context-Based Endpoint Resolution
- API Patterns
- Full Examples
- Glossary
In Aspire, a resource is the fundamental unit of modeling for distributed applications. Resources represent services, infrastructure elements, or supporting components that together compose a distributed system.
Resources in Aspire implement the IResource
interface, with most built-in resources deriving from the base Resource
class.
- Resources are inert by default — they are pure data objects that describe capabilities, configuration, and relationships. They do not manage their own lifecycle (e.g., starting, stopping, checking health). Resource lifecycle is coordinated externally by orchestrators and lifecycle hooks.
- Resources are identified by a unique name within the application graph. This name forms the basis for referencing, wiring, and visualizing resources.
Resource metadata is expressed through annotations, which are strongly-typed objects implementing the IResourceAnnotation
interface.
Annotations allow attaching additional structured information to a resource without modifying its core class. They are the primary extensibility mechanism in Aspire, enabling:
- Core system behaviors (e.g., service discovery, connection strings, health probes)
- Custom extensions and third-party integrations
- Layering of optional capabilities without inheritance or tight coupling
Example: A resource might be annotated with environment variables, endpoint information, or service discovery metadata based on what other components need.
Resources are typically added using fluent extension methods such as AddRedis
, AddProject
, or AddPostgres
.
Extension methods encapsulate:
- Construction of the resource object
- Attachment of annotations that describe defaults, discovery hints, or runtime behavior
- Relationships like wiring up dependencies (e.g., via
.WithReference()
)
This pattern improves the developer experience by:
- Setting sane defaults automatically
- Making required configuration obvious and discoverable
- Providing a product-like feel to adding infrastructure
Without extension methods, adding a resource manually would require constructing it directly, setting annotations manually, and remembering to wire relationships by hand.
var builder = DistributedApplication.CreateBuilder(args);
var pg = builder.AddPostgres("pg");
var api = builder.AddProject("backend").WithReference(pg);
var frontend = builder.AddNpmApp("frontend").WithReference(api);
In this example:
- A PostgreSQL database (
pg
) is created. - A backend service (
api
) is created and connected to the database. - A frontend app (
frontend
) is created and reverse-proxies traffic to the backend.
Each resource participates in the application graph passively, with dependencies expressed through references.
- Resources describe capabilities; they don't control them.
- Annotations add rich, extensible metadata to resources.
- Fluent extension methods guide developers toward correct and complete configurations.
- Names are the identity anchors for wiring and dependency resolution.
In Aspire, many common infrastructure and application patterns are available as built-in resource types. Built-in resources simplify modeling real-world systems by providing ready-made building blocks that automatically integrate with the Aspire runtime, lifecycle management, health tracking, and dashboard visualization.
Built-in resources:
- Handle lifecycle transitions automatically.
- Raise lifecycle events (like startup and readiness signals).
- Push status updates to the system for real-time orchestration and monitoring.
- Expose endpoints, environment variables, and metadata needed for dependent resources.
They help developers express distributed applications consistently without needing to manually orchestrate startup, shutdown, and dependency wiring.
All resources in Aspire begin in the Unknown
state when added to the application graph. This ensures that the resource graph can be fully constructed before any execution, dependency resolution, or publishing occurs.
State | Meaning |
---|---|
Unknown | Default state when first added to the graph. No execution planned yet. |
NotStarted | Defined but not yet scheduled to start. |
Waiting | Awaiting dependencies to become ready (e.g., using WaitFor ). |
Starting | Actively starting; readiness not yet confirmed. |
Running | Successfully started; may have separate application-level health probing. |
RuntimeUnhealthy | The container or host runtime environment (e.g., Docker daemon) is unavailable, preventing start-up. |
Stopping | Resource is shutting down gracefully. |
Exited | Completed execution (typically for short-lived jobs, migrations, one-shot tasks). |
Finished | Ran to successful completion (used for batch workloads or scripts). |
FailedToStart | Failed during startup initialization. |
Hidden | Present in the model but intentionally hidden from dashboard UI (e.g., infrastructure helpers). |
TerminalStates
(e.g., Finished
, Exited
, FailedToStart
) represent states where the resource has stopped progressing.
Resource states drive:
- Readiness checks to unblock dependent resources.
- Dashboard visualization and state coloring.
- Orchestration sequencing for startup and shutdown.
- Health monitoring at runtime.
Aspire provides a set of fundamental built-in resource types that serve as the foundation for modeling execution units:
Type | Purpose |
---|---|
ContainerResource | Runs Docker containers as resources. |
ProjectResource | Runs a .NET project directly (build + launch workflow). |
ExecutableResource | Launches arbitrary executables or scripts as resources. |
These types are infrastructure-oriented primitives. They model how code and applications are packaged and executed.
Note: Specialized services like Redis, Postgres, or RabbitMQ are not true "built-in" resource types in Aspire core — they are typically provided through external packages or extensions that build on
ContainerResource
or custom resource types.
Built-in types:
- Automatically participate in resource orchestration.
- Raise standard lifecycle events without manual intervention.
- Report health and readiness status.
- Expose connection endpoints for dependent services.
Custom resources must opt-in manually to these behaviors.
Aspire defines standard events to orchestrate resource lifecycles:
Event | When Emitted | Purpose |
---|---|---|
BeforeResourceStartedEvent | Just before execution begins. | Last-chance dynamic setup or validation before startup. |
ResourceReadyEvent | When the resource is considered "ready." | Unblocks dependents waiting for readiness. |
ConnectionStringAvailableEvent | When a connection string is ready. | Enables dependent resources to be wired dynamically. |
Lifecycle events allow:
- Dynamic reconfiguration just before startup.
- Dependent resource activation based on readiness.
- Wiring services together based on runtime-generated outputs.
Important: Event publishing is synchronous and blocking — event handlers can delay further execution.
Beyond events, Aspire uses asynchronous state snapshots to report resource status continuously.
- ResourceNotificationService handles snapshot updates.
- Status updates involve:
- Receiving the previous immutable snapshot.
- Mutating to a new snapshot representing the updated state.
- Publishing the new snapshot to the dashboard and orchestrators.
Snapshots:
- Always reflect the latest known status.
- Are non-blocking and do not delay orchestration.
- Drive dashboard visualization and orchestration decisions.
Events represent moment-in-time actions. Snapshots represent ongoing state.
Aspire integrates with .NET health checks to monitor the status of resources after they have started. The health check mechanism is tied into the resource lifecycle:
- When a resource transitions to the
Running
state, Aspire checks if it has any associated health check annotations (typically added via.WithHealthCheck(...)
). - If health checks are configured: Aspire begins executing these checks periodically. The resource is considered fully "ready" only after its health checks pass successfully. Once healthy, Aspire automatically publishes the
ResourceReadyEvent
. - If no health checks are configured: The resource is considered "ready" as soon as it enters the
Running
state. Aspire automatically publishes theResourceReadyEvent
immediately in this case.
This automatic handling ensures that dependent resources (using mechanisms like WaitFor
) only proceed when the target resource is truly ready, either by simply running or by passing its defined health checks.
Important: Developers should not manually publish the
ResourceReadyEvent
. Aspire manages the transition to the ready state based on the presence and outcome of health checks. Manually firing this event can interfere with the orchestration logic.
Aspire supports logging output on a per-resource basis, which is displayed in the console window and can be surfaced in the dashboard. This log stream is especially useful for monitoring what a resource is doing in real time.
For built-in resources, Aspire captures and forwards output from:
- stdout and stderr of containers (e.g., Docker)
- Process output from executables or .NET projects
For custom resources, developers can write directly to a resource’s log using the ResourceLoggerService.
This service provides an ILogger scoped to the individual resource instance, enabling human-readable, contextual logging.
var logger = resourceLoggerService.GetLogger(myResource);
logger.LogInformation("Starting provisioning…");
See the Talking Clock example for a full implementation of a custom resource with logging.
Note: A full example demonstrating custom resource logging with the Talking Clock resource can be found in the Full Examples section.
API | Description |
---|---|
ResourceLoggerService.GetLogger(IResource) | Returns a scoped ILogger. |
ResourceLoggerService.WatchAsync | Stream log lines. |
Use logs for human-readable diagnostics.
Use ResourceNotificationService
for structured state.
Aspire defines a set of optional standard interfaces that allow resources to declare their capabilities in a structured, discoverable way. Implementing these interfaces enables dynamic wiring, publishing, service discovery, and orchestration without hardcoded type knowledge.
These interfaces are the foundation for Aspire's polymorphic behaviors — enabling tools, publishers, and the runtime to treat resources uniformly based on what they can do, rather than what they are.
- Dynamic discovery: Tooling and runtime systems can automatically adapt based on resource capabilities.
- Loose coupling: Behaviors (like environment wiring, service discovery, or connection sharing) are opt-in.
- Extensibility: New resource types can integrate seamlessly into the Aspire ecosystem by implementing one or more interfaces.
Interface | Purpose |
---|---|
IResourceWithEnvironment |
Supports setting environment variables for the resource. |
IResourceWithServiceDiscovery |
Registers a service hostname and metadata for discovery by other resources. |
IResourceWithEndpoints |
Exposes ports, URLs, or connection points that other resources can consume. |
IResourceWithConnectionString |
Provides a connection string output for consumers to connect to the resource. |
IResourceWithArgs |
Supplies additional CLI arguments when launching a project or executable. |
IResourceWithEnvironment
builder.WithEnvironment("MY_SETTING", "value");
Allows setting environment variables that are passed to the resource when it starts.
IResourceWithServiceDiscovery
builder.WithReference(myResourceWithDiscovery);
Exposes the resource via DNS-style service discovery. Downstream resources can refer to it by logical name.
IResourceWithEndpoints
builder.GetEndpoint("http");
When a resource implements IResourceWithEndpoints
, it allows referencing specific endpoints (e.g., http
, tcp
) for reverse proxies or connection targets.
IResourceWithConnectionString
builder.WithReference(myDatabaseResource);
Allows wiring a database connection string into environment variables, configurations, or CLI arguments.
IResourceWithArgs
builder.WithArgs("2", "--url", endpoint);
Passes command-line arguments dynamically to an executable resource.
Source: These APIs and behaviors are defined in the Aspire.Hosting package.
By modeling behaviors through interfaces rather than concrete types, Aspire enables:
- Tooling flexibility: Publishers can wire environment variables, endpoints, and arguments generically.
- Runtime uniformity: Dashboards and orchestrators treat resources based on capabilities, not type-specific logic.
- Ecosystem extensibility: New resource types can plug into the system without modifying core code.
Interfaces allow Aspire to remain open, flexible, and adaptable as new types of services, platforms, and deployment targets emerge.
Aspire supports modeling parent-child relationships between resources to express ownership, containment, and grouping.
Parent-child relationships serve two purposes:
- Lifecycle Containment: The child's execution is tied to the parent's — starting, stopping, and failures cascade from parent to child automatically.
- Dashboard Visualization: The child appears nested beneath the parent in dashboards and visualizations, improving readability.
When a resource implements the IResourceWithParent
interface, it declares true containment — meaning its lifecycle is controlled by its parent:
- Startup: The child resource will only start after its parent starts (though readiness is independent).
- Shutdown: If the parent is stopped or removed, the child is also stopped automatically.
- Failure Propagation: If a parent enters a terminal failure state (
FailedToStart
, etc.), dependent children are stopped.
Example:
A logging sidecar container is tied to the lifecycle of a main application container — if the main app stops, the logging sidecar is also terminated.
Aspire also supports visual-only parent-child relationships using the WithParentRelationship()
method during resource construction.
Visual relationships:
- Affect only the dashboard layout.
- Do not affect lifecycle — the resources are independent operationally.
- Improve clarity by logically grouping related components.
Example:
A Redis database container and a Redis Commander admin UI container can be grouped visually, even though they start independently.
Aspire does not infer parent-child relationships automatically based on names, dependencies, or network links.
You must explicitly declare either:
IResourceWithParent
(for lifecycle and visual nesting)- or
.WithParentRelationship()
(for visual nesting only)
This explicitness ensures developers have full control over resource containment and presentation.
Scenario | Parent | Child |
---|---|---|
Main application container with logging sidecar | App container | Fluentd container |
Database with admin dashboard | Database container | Admin UI container |
Microservice with associated health monitor | API container | Health probe container |
In Aspire, configuration, connectivity details, and dependencies between distributed components are modeled using structured values. These values capture relationships explicitly—not just as simple strings—making the application graph portable, inspectable, and evolvable.
Aspire represents these relationships through a heterogeneous Directed Acyclic Graph (DAG). This graph tracks not only dependency ordering but also how structured values are passed between resources at multiple abstraction levels: configuration, connection, and runtime behavior.
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("pg");
var api = builder.AddProject("api").WithReference(db);
var web = builder.AddNpmApp("web").WithReference(api);
builder.Build().Run();
%% Graph showing resource dependencies
graph LR
web --> EndpointReference --> api
api --> ConnectionStringReference --> pg
Normally, resource references form an acyclic graph — no cycles allowed.
However, endpoint references are treated specially and may form cycles intentionally.
Endpoints are modeled as external entities:
- They are not edges in the resource dependency graph.
- They enable realistic mutual references like:
- A frontend app and an OIDC server mutually referencing each other's URLs (redirects, login callbacks).
- A backend exposing CORS settings that reference the frontend URL.
Endpoints are managed separately from strict dependency edges to allow flexible, real-world service wiring.
Resources connect to each other through:
- WithReference() calls
- Environment variables, CLI arguments, and other configurations populated by structured value references.
Each reference adds an edge to the graph, allowing Aspire to:
- Track dependency ordering.
- Propagate structured values cleanly between services.
- Validate application integrity before execution.
Important:
Aspire never infers references automatically — all value flows must be explicitly authored by developers.
Aspire distinguishes between structured values and literal values.
- Structured values preserve meaning (e.g., "this is a service URL" vs. "this is a raw string").
- Literal values are inert — they are carried unchanged across modes.
At publish time and run time:
- Structured values are either resolved (if possible) or translated into target artifacts (e.g., environment variables, argument values etc.).
- Literal values are simply copied.
Flattening values too early destroys portability, environment substitution, and cross-platform compatibility. Aspire delays flattening as long as possible to maintain graph fidelity.
Every structured value type in Aspire implements two fundamental interfaces:
Interface | When Used | Purpose |
---|---|---|
IValueProvider |
Run mode | Resolves live values when the application starts. |
IManifestExpressionProvider |
Publish mode | Emits structured expressions (like {pg.outputs.url} ) into deployment artifacts. |
This dual-interface model enables deferred evaluation:
- During publish, structured placeholders are emitted — no runtime values are resolved yet.
- During run, structured references are resolved to live values like URLs, ports, or connection strings.
Internally, value providers are attached to environment variables, CLI arguments, configuration fields, and other structured outputs during application graph construction.
Deferred evaluation guarantees that Aspire applications can be published safely, deployed flexibly, and run consistently across environments.
Type | Represents | Run Mode | Publish Mode |
---|---|---|---|
string |
A literal string value. | Same literal. | Same literal. |
EndpointReference |
A link to a named endpoint on another resource. | Concrete URL (http://localhost:5000 ). |
Target-specific endpoint translation (DNS, ingress, etc.). |
EndpointReferenceExpression |
A property of an endpoint (Host , Port , Scheme ). |
Concrete value. | Platform-specific translation. |
ConnectionStringReference |
A symbolic pointer to a resource's connection string. | Concrete string. | Token or externalized secret. |
ParameterResource |
An external input, secret, or setting. | Local dev value or environment lookup. | Placeholder ${PARAM} for substitution. |
ReferenceExpression |
A composite string with embedded references. | Concrete formatted string. | Format string preserved for substitution. |
ReferenceExpression
preserves structured value objects—endpoints, parameters, connection strings, etc.—inside an interpolated string and defers evaluation until it is safe.
Aspire evaluates the model in two distinct modes:
Phase | ReferenceExpression yields |
---|---|
Publish | Publisher‑specific placeholder text (e.g., {api.bindings.http.host} ). |
Run | Concrete value such as localhost . |
var ep = api.GetEndpoint("http");
builder.WithEnvironment("HEALTH_URL",
ReferenceExpression.Create(
$"https://{ep.Property(EndpointProperty.Host)}:{ep.Property(EndpointProperty.Port)}/health"
)
);
Publish manifest excerpt
HEALTH_URL=https://{api.bindings.http.host}:{api.bindings.http.port}/health
Run‑time value
HEALTH_URL=https://localhost:5000/health
Best practice – Avoid resolving values directly. Build the string inside
ReferenceExpression.Create
so structure is preserved.
var ep = api.GetEndpoint("http");
if (builder.ExecutionContext.IsRunMode)
{
builder.WithEnvironment("HEALTH_URL", ep.Url + "/health"); // concrete
}
else
{
builder.WithEnvironment("HEALTH_URL",
ReferenceExpression.Create($"{ep}/health")); // structured
}
A common implementation builds the connection string with ReferenceExpression
, mixing any value objects (endpoint properties, parameters, other references):
private static ReferenceExpression BuildConnectionString(
EndpointReference endpoint,
ParameterResource passwordParameter)
{
var host = endpoint.Property(EndpointProperty.IPV4Host);
var port = endpoint.Property(EndpointProperty.Port);
var pwd = passwordParameter;
return ReferenceExpression.Create(
$"Server={host},{port};User ID=sa;Password={pwd};TrustServerCertificate=true");
}
Error | Correct approach |
---|---|
Build the string first, wrap later | Build inside ReferenceExpression.Create(...) . |
Access Endpoint.Url during publish |
Use Endpoint.Property(...) in the expression. |
Mix resolved strings and placeholders | Keep the entire value inside one ReferenceExpression . |
The EndpointReference is the fundamental type used to interact with another resource's endpoint. It provides properties such as:
- Url
- Host
- Port
These properties are dynamically resolved during the application’s startup sequence. Accessing them before the endpoints are allocated results in an exception.
Resources supporting endpoints should implement IResourceWithEndpoints, enabling the use of GetEndpoint(name) to retrieve an EndpointReference. This is implemented on the built-in ProjectResource, ContainerResource and ExecutableResource. It allows endpoints to be programmatically accessed and passed between resources.
Key Example: Endpoint Access and Resolution
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
// Get a reference to the "tcp" endpoint by name
var endpoint = redis.GetEndpoint("tcp");
builder.Build().Run();
See the canonical Endpoint Primitives section for the full explanation.
The following is a short recap for quick reference.
An endpoint is allocated when Aspire resolves its runtime values (e.g., Host
, Port
, Url
) during run mode. Allocation happens as part of the startup sequence, ensuring endpoints are ready for use in local development.
In publish mode, endpoints are not allocated with concrete values. Instead, their values are represented as manifest expressions (e.g., {redis.bindings.tcp.host}:{redis.bindings.tcp.port}
) that are resolved by the deployment infrastructure.
Context | Run Mode | Publish Mode |
---|---|---|
Endpoint Values | Fully resolved (tcp://localhost:6379 ). |
Represented by manifest expressions ({redis.bindings.tcp.url} ). |
Use Case | Local development and debugging. | Deployed environments (e.g., Kubernetes, Azure). |
Behavior | Endpoints are allocated dynamically. | Endpoint placeholders resolve at runtime. |
Use the IsAllocated
property on an EndpointReference
to check if an endpoint has been allocated before accessing its runtime values.
Endpoint resolution happens during the startup sequence of the DistributedApplication. To safely access endpoint values (e.g., Url, Host, Port), you must wait until endpoints are allocated.
Aspire provides eventing APIs, such as AfterEndpointsAllocatedEvent
, to access endpoints after allocation. These APIs ensure code executes only when endpoints are ready.
var builder = DistributedApplication.CreateBuilder(args);
// Add a Redis container with a TCP endpoint
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
// Retrieve the EndpointReference
var endpoint = redis.GetEndpoint("tcp");
// Check allocation status and access Url
Console.WriteLine($"IsAllocated: {endpoint.IsAllocated}");
try
{
Console.WriteLine($"Url: {endpoint.Url}");
}
catch (Exception ex)
{
Console.WriteLine($"Error accessing Url: {ex.Message}");
}
// Subscribe to AfterEndpointsAllocatedEvent for resolved properties
builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(
(@event, cancellationToken) =>
{
Console.WriteLine($"Endpoint allocated: {endpoint.IsAllocated}");
Console.WriteLine($"Resolved Url: {endpoint.Url}");
return Task.CompletedTask;
});
// Start the application
builder.Build().Run();
- Run Mode:
IsAllocated: True Resolved Url: http://localhost:6379
- Publish Mode:
IsAllocated: False Error accessing Url: Endpoint has not been allocated.
NOTE: The overloads of WithEnvironment that take a callback run after endpoints have been allocated.
The WithReference API allows you to pass an endpoint reference directly to a target resource.
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
builder.AddProject<Projects.Worker>("worker")
.WithReference(redis.GetEndpoint("tcp"));
builder.Build().Run();
WithReference
is optimized for applications that use service discovery.
The WithEnvironment API exposes endpoint details as environment variables, enabling runtime configuration.
Example: Passing Redis Endpoint as Environment Variable
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
builder.AddProject<Worker>("worker")
.WithEnvironment("RedisUrl", redis.GetEndpoint("tcp"));
builder.Build().Run();
WithEnvironment gives full control over the configuration names injected into the target resource.
EndpointReferenceExpression
represents one field of an endpoint (Host, Port, Scheme, etc.).
Call endpoint.Property(...)
to get that field; the result is still a structured value and stays deferred until publish/run time.
Need | Pattern |
---|---|
Only one part (e.g., host) | endpoint.Property(EndpointProperty.Host) |
Compose multiple parts into one setting | Build a ReferenceExpression (see dedicated section). |
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint("tcp", 6379);
builder.AddProject("worker")
.WithEnvironment(ctx =>
{
var ep = redis.GetEndpoint("tcp");
ctx.EnvironmentVariables["REDIS_HOST"] = ep.Property(EndpointProperty.Host);
ctx.EnvironmentVariables["REDIS_PORT"] = ep.Property(EndpointProperty.Port);
});
var ep = redis.GetEndpoint("tcp");
builder.WithEnvironment("REDIS_URL",
ReferenceExpression.Create(
$"redis://{ep.Property(EndpointProperty.HostAndPort)}"
)
);
This pattern avoids resolving endpoint values prematurely and works in both publish and run modes.
public enum EndpointProperty
{
Url = 0,
Host = 1,
IPV4Host = 2,
Port = 3,
Scheme = 4,
TargetPort = 5,
HostAndPort = 6
}
Property | Meaning |
---|---|
Url | Full URL (scheme://host:port). |
Host / IPV4Host | Host name or IPv4 literal. |
Port / TargetPort | Allocated host port vs. container‑internal port. |
Scheme | http , tcp , etc. |
HostAndPort | Convenience composite (host:port ). |
EndpointReference
exposes live or placeholder values for an endpoint and provides .Property(...)
to create an EndpointReferenceExpression.
Key members:
Member | Description |
---|---|
Url , Host , Port , Scheme , TargetPort |
Concrete in run mode; undefined in publish mode. |
bool IsAllocated |
Indicates if concrete values are available (run mode). |
EndpointReferenceExpression Property(EndpointProperty) |
Creates a deferred expression for one field. |
EndpointReferenceExpression
implements the same IManifestExpressionProvider
/ IValueProvider
pair, so it can be embedded in a ReferenceExpression
or resolved directly with GetValueAsync()
.
Aspire resolves endpoints differently based on the relationship between the source and target resources. This ensures proper communication across all environments.
Source | Target | Resolution | Example URL |
---|---|---|---|
Container | Container | Container network (resource name:port ). |
redis:6379 |
Executable/Project | Container | Host network (localhost:port ). |
localhost:6379 |
Container | Executable/Project | Host network (host.docker.internal:port ). |
host.docker.internal:5000 |
Aspire resolves endpoints differently based on the execution context (e.g., run mode vs. publish mode, container vs. executable). Sometimes you want to override that resolution behavior.
Scenario
Below example shows a project that is going to setup up grafana and keycloak. We need to give the project the address for container-to-container communication between grafana and keycloak even though the target resource is a project. The project isn’t directly talking to keycloak or grafana, it's a mediator that is just setting URLs in the appropriate configuration of each container.
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Api>("api")
.WithEnvironment(ctx =>
{
var keyCloakEndpoint = keycloak.GetEndpoint("http");
var grafanaEndpoint = grafana.GetEndpoint("http");
ctx.EnvironmentVariables["Grafana__Url"] = grafanaEndpoint;
if (ctx.ExecutionContext.IsRunMode)
{
// The project needs to get the URL for keycloak in the context of the container network,
// but since this is a project, it'll resolve the url in the context of the host network.
// We get the runtime url and change the host and port to match the container network pattern (host = resource name, port = target port ?? port)
var keycloakUrl = new UriBuilder(keyCloakEndpoint.Url)
{
Host = keycloak.Resource.Name,
Port = keyCloakEndpoint.TargetPort ?? keyCloakEndpoint.Port,
};
ctx.EnvironmentVariables["Keycloak__AuthServerUrl"] = keycloakUrl.ToString();
}
else
{
// In publish mode let the endpoint resolver handle the URL
ctx.EnvironmentVariables["Keycloak__AuthServerUrl"] = keyCloakEndpoint;
}
});
builder.Build().Run();
Aspire separates resource data models from behavior using fluent extension methods.
- Resource classes define only constructors and properties.
- Extension methods implement resource creation, configuration, and runtime wiring.
This guide describes each pattern and shows a verbatim Redis example at the end. It also covers how to publish manifests via custom resources.
An AddX(...)
method executes:
- Validate inputs (
builder
,name
, required arguments). - Instantiate the data-only resource (
new TResource(...)
). - Register it with
builder.AddResource(resource)
. - Optional wiring of endpoints, health checks, container settings, environment variables, command-line arguments, and event subscriptions.
public static IResourceBuilder<TResource> AddX(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
/* optional parameters */)
{
// 1. Validate inputs
// 2. Instantiate resource
// 3. builder.AddResource(resource)
// 4. Optional wiring:
// .WithEndpoint(...)
// .WithHealthCheck(...)
// .WithImage(...)
// .WithEnvironment(...)
// .WithArgs(...)
// Eventing.Subscribe<...>(...)
}
- Endpoints:
.WithEndpoint(port: hostPort, targetPort: containerPort, name: endpointName)
- Health checks:
.WithHealthCheck(healthCheckKey)
- Container images / registries:
.WithImage(imageName, imageTag) .WithImageRegistry(registryUrl)
- Entrypoint & args:
.WithEntrypoint("/bin/sh") .WithArgs(context => { /* build args */ return Task.CompletedTask; })
- Environment variables:
.WithEnvironment(context => new("ENV_VAR", valueProvider))
- Event subscriptions:
builder.Eventing.Subscribe<EventType>(resource, handler);
Step | Call/Method | Purpose |
---|---|---|
Validate | ArgumentNullException.ThrowIfNull(...) |
Ensure non-null builder, name, and args |
Instantiate | new TResource(name, …) |
Create data-only instance |
Register | builder.AddResource(resource) |
Add resource to the application model |
Optional wiring | .WithEndpoint… , .WithHealthCheck… , .WithImage… , .WithEnvironment… , .WithArgs… , Eventing.Subscribe… |
Configure container details, wiring, and runtime hooks |
WithX(...)
methods attach annotations to resource builders.
public static IResourceBuilder<TResource> WithFoo(
this IResourceBuilder<TResource> builder,
FooOptions options) =>
builder.WithAnnotation(new FooAnnotation(options));
- Target:
IResourceBuilder<TResource>
- Action:
WithAnnotation(...)
- Returns:
IResourceBuilder<TResource>
Method | Target | Action |
---|---|---|
WithX(...) |
IResourceBuilder<TResource> |
Attaches XAnnotation via WithAnnotation |
Returns | IResourceBuilder<TResource> |
Enables fluent chaining |
Annotations are public metadata types implementing IResourceAnnotation
. They can be added or removed dynamically at runtime via hooks or events. Consumers can query annotations using TryGetLastAnnotation<T>()
when necessary.
public sealed record PersistenceAnnotation(
TimeSpan? Interval,
int KeysChangedThreshold) : IResourceAnnotation;
builder.WithAnnotation(new PersistenceAnnotation(
TimeSpan.FromSeconds(60),
100));
Concept | Pattern | Notes |
---|---|---|
Annotation Type | public record XAnnotation(...) : IResourceAnnotation |
Public to support dynamic runtime use |
Attach | builder.WithAnnotation(new XAnnotation(...)) |
Adds metadata to resource builder |
Query | resource.TryGetLastAnnotation<XAnnotation>(out var a) |
Consumers inspect annotations as needed |
Custom value objects defer evaluation and allow the framework to discover dependencies between resources.
Interface | Member | When Used | Purpose |
---|---|---|---|
IValueProvider |
ValueTask<string?> GetValueAsync(CancellationToken) |
Run mode | Resolve live values at runtime |
IManifestExpressionProvider |
string ValueExpression { get; } |
Publish mode | Emit structured expressions in manifests |
IValueWithReferences (opt.) |
IEnumerable<object> References { get; } |
Both (if needed) | Declare dependencies on other resources |
Implement
IValueProvider
andIManifestExpressionProvider
on all structured value types.
ImplementIValueWithReferences
only when your type holds resource references.
builder.WithEnvironment(context =>
new("REDIS_CONNECTION_STRING", redis.GetConnectionStringAsync));
public sealed partial class BicepOutputReference :
IManifestExpressionProvider,
IValueProvider,
IValueWithReferences
{
public string ValueExpression { get; }
public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken = default);
IEnumerable<object> IValueWithReferences.References { get; }
}
public static IResourceBuilder<T> WithEnvironment<T>(
this IResourceBuilder<T> builder,
string name,
BicepOutputReference bicepOutputReference)
where T : IResourceWithEnvironment
{ /* attaches environment variable from Bicep output */ }
Concept | Pattern | Purpose |
---|---|---|
IValueProvider |
GetValueAsync(...) |
Deferred runtime resolution |
IManifestExpressionProvider |
ValueExpression |
Structured publish-time expression |
IValueWithReferences (opt.) |
References |
Declare resource dependencies |
WithEnvironment(...) |
new("NAME", valueProvider) |
Attach structured values unflattened |
Custom resources that publish JSON manifest entries must:
- Register a callback using
ManifestPublishingCallbackAnnotation
in the constructor. - Implement the callback to write JSON via
ManifestPublishingContext.Writer
. - Use value objects (
IManifestExpressionProvider
) for structured fields.
Resources can opt out of being included in the publishing manifest entirely by calling the ExcludeFromManifest()
extension method on the IResourceBuilder<T>
. Resources marked this way will be omitted when generating publishing assets like Docker Compose files or Kubernetes manifests.
public class AzureBicepResource : Resource, IAzureResource
{
public AzureBicepResource(string name, ...) : base(name)
{
Annotations.Add(new ManifestPublishingCallbackAnnotation(WriteToManifest));
}
}
public virtual void WriteToManifest(ManifestPublishingContext context)
{
context.Writer.WriteString("type", "azure.bicep.v0");
context.Writer.WriteString("path", context.GetManifestRelativePath(path));
context.Writer.WriteStartObject("params");
foreach (var kv in Parameters)
{
context.Writer.WritePropertyName(kv.Key);
var v = kv.Value is IManifestExpressionProvider p ? p.ValueExpression : kv.Value?.ToString();
context.Writer.WriteString(kv.Key, v ?? "");
context.TryAddDependentResources(kv.Value);
}
context.Writer.WriteEndObject();
}
Step | API / Call | Purpose |
---|---|---|
Register callback | Annotations.Add(new ManifestPublishingCallbackAnnotation(WriteToManifest)) |
Hook custom JSON writer |
Implement WriteToManifest |
Use context.Writer to emit JSON properties |
Define resource manifest representation |
Structured fields | IManifestExpressionProvider.ValueExpression |
Ensure publish-time placeholders are preserved |
Convention | Rationale |
---|---|
Data-only resource classes | Separates data model from behavior |
*BuilderExtensions classes |
Groups all API methods per integration |
Public annotations | Allow dynamic runtime addition/removal |
[ResourceName] attribute |
Enforces valid resource naming at compile time |
Preserve parameter/value objects | Ensures deferred evaluation of secrets/outputs |
This section contains complete, runnable examples demonstrating key concepts.
This example shows how to create a custom resource (RedisResource
) that derives from ContainerResource
and implements IResourceWithConnectionString
. It demonstrates:
- Defining a data-only resource class.
- Implementing
IResourceWithConnectionString
with deferred evaluation usingReferenceExpression
. - Creating an
AddRedis
extension method that handles parameter validation, password management, event subscription, health checks, and container configuration using fluent APIs.
// AddRedis Extension Method
// This extension method provides a convenient way to add a Redis resource to the Aspire application model.
public static IResourceBuilder<RedisResource> AddRedis(
this IDistributedApplicationBuilder builder, // Extends the main application builder interface.
[ResourceName] string name, // The unique name for this Redis resource.
int? port = null, // Optional host port mapping.
IResourceBuilder<ParameterResource>? password = null) // Optional parameter resource for the password.
{
// 1. Validate inputs before any side effects
// Ensure the builder and name are not null to prevent downstream errors.
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);
// 2. Preserve or generate the password ParameterResource (deferred evaluation)
// If a password parameter is provided, use it. Otherwise, create a default one.
// ParameterResource allows the actual password value to be resolved later (e.g., from secrets).
var passwordParameter = password?.Resource
?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(
builder, $"{name}-password", special: false); // Creates a default password parameter if none is supplied.
// 3. Instantiate the data-only RedisResource with its password parameter
// Create the RedisResource instance, passing the name and the (potentially deferred) password parameter.
var redis = new RedisResource(name, passwordParameter);
// Variable to hold the resolved connection string at runtime.
string? connectionString = null;
// 4. Subscribe to ConnectionStringAvailableEvent to capture the connection string at runtime
// This event hook allows capturing the connection string *after* it has been resolved
// by the Aspire runtime, including potentially allocated ports and resolved parameter values.
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(redis, async (@event, ct) =>
{
// Resolve the connection string using the resource's method.
connectionString = await redis.GetConnectionStringAsync(ct).ConfigureAwait(false);
// Ensure the connection string was actually resolved.
if (connectionString == null)
{
throw new DistributedApplicationException(
$"Connection string for '{redis.Name}' was unexpectedly null.");
}
});
// 5. Register a health check that uses the connection string once it becomes available
// Define a unique key for the health check.
var healthCheckKey = $"{name}_check";
// Add a Redis-specific health check to the application's health check services.
// The lambda `_ => connectionString ?? ...` ensures the health check uses the
// connection string *after* it has been resolved by the event handler above.
builder.Services
.AddHealthChecks()
.AddRedis(_ => connectionString
?? throw new InvalidOperationException("Connection string is unavailable"), // Throw if accessed too early.
name: healthCheckKey); // Name the health check for identification.
// 6. Add & configure container using the fluent builder pattern
// Add the RedisResource instance to the application model.
return builder.AddResource(redis)
// 6.a Expose the Redis TCP endpoint
// Map the host port (if provided) to the container's default Redis port (6379).
// Name the endpoint "tcp" for reference.
.WithEndpoint(
port: port, // Optional host port.
targetPort: 6379, // Default Redis port inside the container.
name: RedisResource.PrimaryEndpointName) // Use the constant defined in RedisResource.
// 6.b Specify container image and tag
// Define the Docker image to use for the Redis container.
.WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag)
// 6.c Configure container registry if needed
// Specify a container registry if the image is not on Docker Hub.
.WithImageRegistry(RedisContainerImageTags.Registry)
// 6.d Wire the health check into the resource
// Associate the previously defined health check with this resource.
// Aspire uses this for dashboard status and orchestration.
.WithHealthCheck(healthCheckKey)
// 6.e Define the container’s entrypoint
// Override the default container entrypoint if necessary. Here, it's set to use shell.
.WithEntrypoint("/bin/sh")
// 6.f Pass the password ParameterResource into an environment variable
// Set environment variables for the container. This uses a callback to access
// the resource instance (`redis`) and its properties.
.WithEnvironment(context =>
{
// If a password parameter exists, expose it as the REDIS_PASSWORD environment variable.
// The actual value resolution happens later via the ParameterResource.
if (redis.PasswordParameter is { } pwd)
{
context.EnvironmentVariables["REDIS_PASSWORD"] = pwd;
}
})
// 6.g Build the container arguments lazily, preserving annotations
// Define the command-line arguments for the container. This also uses a callback
// to allow dynamic argument construction based on resource state or annotations.
.WithArgs(context =>
{
// Start with the basic command to run the Redis server.
var cmd = new List<string> { "redis-server" };
// If a password parameter is set, add the necessary Redis CLI arguments.
// Note: It uses the environment variable name set earlier ($REDIS_PASSWORD).
if (redis.PasswordParameter is not null)
{
cmd.Add("--requirepass");
cmd.Add("$REDIS_PASSWORD"); // Reference the environment variable.
}
// Check if a PersistenceAnnotation has been added to the resource.
// Annotations allow adding optional configuration or behavior.
if (redis.TryGetLastAnnotation<PersistenceAnnotation>(out var pa))
{
// If persistence is configured, add the corresponding Redis CLI arguments.
var interval = (pa.Interval ?? TimeSpan.FromSeconds(60))
.TotalSeconds
.ToString(CultureInfo.InvariantCulture);
cmd.Add("--save");
cmd.Add(interval); // Save interval in seconds.
cmd.Add(pa.KeysChangedThreshold.ToString(CultureInfo.InvariantCulture)); // Number of key changes threshold.
}
// Finalize the arguments for the shell entrypoint.
context.Args.Add("-c"); // Argument for /bin/sh to execute a command string.
context.Args.Add(string.Join(' ', cmd)); // Join all parts into a single command string.
return Task.CompletedTask; // Return a completed task as the callback is synchronous.
});
}
// RedisResource Class
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Aspire.Hosting.ApplicationModel;
// Data-only Redis resource derived from ContainerResource.
// It implements IResourceWithConnectionString to provide connection details.
public class RedisResource(string name)
// Inherits common container properties and behaviors from ContainerResource.
: ContainerResource(name),
// Implements this interface to indicate it can provide a connection string.
IResourceWithConnectionString
{
// Constant for the primary endpoint name, used for consistency.
internal const string PrimaryEndpointName = "tcp";
// Backing field for the lazy-initialized primary endpoint reference.
private EndpointReference? _primaryEndpoint;
// Public property to get the EndpointReference for the primary "tcp" endpoint.
// EndpointReference allows deferred access to endpoint details (host, port, URL).
// It's lazy-initialized on first access.
public EndpointReference PrimaryEndpoint
=> _primaryEndpoint ??= new(this, PrimaryEndpointName);
// Property to hold the ParameterResource representing the Redis password.
// ParameterResource allows the password value to be resolved later (e.g., from secrets).
public ParameterResource? PasswordParameter { get; private set; }
// Constructor that accepts a password ParameterResource.
public RedisResource(string name, ParameterResource password)
: this(name) // Call the base constructor.
{
PasswordParameter = password; // Store the provided password parameter.
}
// Helper method to build the ReferenceExpression for the connection string.
// ReferenceExpression captures the structure of the connection string, including
// references to endpoints and parameters, allowing deferred resolution.
private ReferenceExpression BuildConnectionString()
{
// Use a builder to construct the expression piece by piece.
var builder = new ReferenceExpressionBuilder();
// Append the host and port part, referencing the PrimaryEndpoint properties.
// .Property() ensures deferred resolution suitable for both run and publish modes.
builder.Append($"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}");
// If a password parameter exists, append it to the connection string format.
if (PasswordParameter is not null)
{
// Append the password parameter directly; ReferenceExpression handles its deferred resolution.
builder.Append($",password={PasswordParameter}");
}
// Build and return the final ReferenceExpression.
return builder.Build();
}
// Implementation of IResourceWithConnectionString.ConnectionStringExpression.
// Provides the connection string as a ReferenceExpression, suitable for publish mode
// where concrete values aren't available yet.
public ReferenceExpression ConnectionStringExpression =>
BuildConnectionString();
}
This example demonstrates creating a completely custom resource (TalkingClockResource
) that doesn't derive from built-in types. It shows:
- Defining a simple resource class.
- Implementing a custom lifecycle hook (
TalkingClockLifecycleHook
) to manage the resource's behavior (starting, logging, state updates). - Using
ResourceLoggerService
for per-resource logging. - Using
ResourceNotificationService
to publish state updates. - Creating an
AddTalkingClock
extension method to register the resource and its lifecycle hook.
// TalkingClockResource and Lifecycle Hook
// Define the custom resource type. It inherits from the base Aspire 'Resource' class.
// This class is primarily a data container; Aspire behavior is added via lifecycle hooks and extension methods.
public sealed class TalkingClockResource(string name) : Resource(name);
// Define an Aspire lifecycle hook that implements the behavior for the TalkingClockResource.
// Lifecycle hooks allow plugging into the application's startup and shutdown sequences.
public sealed class TalkingClockLifecycleHook(
// Aspire service for publishing resource state updates (e.g., Running, Starting).
ResourceNotificationService notification,
// Aspire service for publishing and subscribing to application-wide events.
IDistributedApplicationEventing eventing,
// Aspire service for getting a logger scoped to a specific resource.
ResourceLoggerService loggerSvc,
// General service provider for dependency injection if needed.
IServiceProvider services) : IDistributedApplicationLifecycleHook // Implement the Aspire hook interface.
{
// This method is called by Aspire after all resources have been initially added to the application model.
public Task AfterResourcesCreatedAsync(
DistributedApplicationModel model, // The Aspire application model containing all resources.
CancellationToken token) // Cancellation token for graceful shutdown.
{
// Find all instances of TalkingClockResource in the Aspire application model.
foreach (var clock in model.Resources.OfType<TalkingClockResource>())
{
// Get an Aspire logger specifically for this clock instance. Logs will be associated with this resource in the dashboard.
var log = loggerSvc.GetLogger(clock);
// Start a background task to manage the clock's lifecycle and behavior.
_ = Task.Run(async () =>
{
// Publish an Aspire event indicating that this resource is about to start.
// Other components could subscribe to this event for pre-start actions.
await eventing.PublishAsync(
new BeforeResourceStartedEvent(clock, services), token);
// Log an informational message associated with the resource.
log.LogInformation("Starting Talking Clock...");
// Publish an initial state update to the Aspire notification service.
// This sets the resource's state to 'Running' and records the start time.
// The Aspire dashboard and other orchestrators observe these state updates.
await notification.PublishUpdateAsync(clock, s => s with
{
StartTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.Running // Use an Aspire well-known state.
});
// Enter the main loop that runs as long as cancellation is not requested.
while (!token.IsCancellationRequested)
{
// Log the current time, associated with the resource.
log.LogInformation("The time is {time}", DateTime.UtcNow);
// Publish a custom state update "Tick" using Aspire's ResourceStateSnapshot.
// This demonstrates using custom state strings and styles in the Aspire dashboard.
await notification.PublishUpdateAsync(clock,
s => s with { State = new ResourceStateSnapshot("Tick", KnownResourceStateStyles.Info) });
await Task.Delay(1000, token);
// Publish another custom state update "Tock" using Aspire's ResourceStateSnapshot.
await notification.PublishUpdateAsync(clock,
s => s with { State = new ResourceStateSnapshot("Tock", KnownResourceStateStyles.Success) });
await Task.Delay(1000, token);
}
}, token);
}
// Indicate that this hook's work (starting the background tasks) is complete for now.
return Task.CompletedTask;
}
// Other Aspire lifecycle hook methods (e.g., BeforeStartAsync, AfterEndpointsAllocatedAsync) could be implemented here if needed.
}
// Define Aspire extension methods for adding the TalkingClockResource to the application builder.
// This provides a fluent API for users to add the custom resource.
public static class TalkingClockExtensions
{
// The main Aspire extension method to add a TalkingClockResource.
public static IResourceBuilder<TalkingClockResource> AddTalkingClock(
this IDistributedApplicationBuilder builder, // Extends the Aspire application builder.
string name) // The name for this resource instance.
{
// Register the TalkingClockLifecycleHook with the DI container using Aspire's helper method.
// The Aspire hosting infrastructure will automatically discover and run registered lifecycle hooks.
builder.Services.TryAddLifecycleHook<TalkingClockLifecycleHook>();
// Create a new instance of the TalkingClockResource.
var clockResource = new TalkingClockResource(name);
// Add the resource instance to the Aspire application builder and configure it using fluent APIs.
return builder.AddResource(clockResource)
// Use Aspire's ExcludeFromManifest to prevent this resource from being included in deployment manifests.
.ExcludeFromManifest()
// Use Aspire's WithInitialState to set an initial state snapshot for the resource.
// This provides initial metadata visible in the Aspire dashboard.
.WithInitialState(new CustomResourceSnapshot // Aspire type for custom resource state.
{
ResourceType = "TalkingClock", // A string identifying the type of resource for Aspire.
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted, // Use an Aspire well-known state.
// Add custom properties displayed in the Aspire dashboard's resource details.
Properties =
[
// Use Aspire's known property key for source information.
new(CustomResourceKnownProperties.Source, "Talking Clock")
],
// Add URLs associated with the resource, displayed as links in the Aspire dashboard.
Urls =
[
// Define a URL using Aspire's UrlSnapshot type.
new("Speaking Clock", "https://www.speaking-clock.com/", isInternal: false)
]
});
}
}
Extra API Terms | Description |
---|---|
IResourceAnnotation | Typed metadata object attached to resources. |
WithAnnotation() | Fluent method to attach typed annotations. |
ReferenceExpression | Structured formatter preserving value references. |
Term | Definition |
---|---|
Resource | Service or infrastructure element in your app. |
Annotation | Metadata attached to a resource. |
DAG | Directed acyclic graph. |
Heterogeneous DAG | DAG containing varied resource types. |
Publisher | Emits deployment artifacts from the model. |
Hoisting | Leaving a value unresolved for later substitution. |
Deferred evaluation | Computing a value only when needed. |
ResourceNotificationService | Publishes observable state updates. |
Lifecycle events | Time-based signals for resource transitions. |
ResourceLoggerService
is something I find extremely helpful - is it worth mentioning here somewhere?