[New article]: Configuring, Referencing, and Resolving Endpoints in .NET Aspire
Proposed topic or title
Configuring, Referencing, and Resolving Endpoints in .NET Aspire
Location in table of contents.
App Host (Orchestration) → Networking Overview
Reason for the article
This article will provide an essential guide for developers to understand how Aspire endpoints are configured, referenced, and resolved. It will demystify APIs such as WithEnvironment and explain how endpoints behave in both run and publish modes. This understanding is crucial for developers to integrate and automate resources within Aspire seamlessly. Without this knowledge, users might face challenges connecting services or leveraging the full potential of the Aspire app model.
Article abstract
This article will explore the resource endpoints, covering configuration, reference mechanisms, and resolution rules of those endpoints. It will illustrate how to use APIs like WithEnvironment to ensure consistent connectivity between resources and how endpoints are resolved between run and publish modes. Examples will guide developers through typical workflows and edge cases, ensuring robust and reliable service integrations.
Relevant searches
"Aspire endpoint resolution" "WithEnvironment API .NET Aspire" "Configuring endpoints in Aspire" "Aspire run mode vs publish mode endpoints"
This is a brain dump:
Prerequisite
This document assumes familiarity with endpoint definitions and the basics of networking in Aspire. Refer to the Networking Overview for an introduction to these concepts.
Understanding Endpoint Primitives
The EndpointReference is the fundamental type used to interact with another resource's endpoint. It provides properties such as:
- Url
- Host
- Port
- e.g
These properties are dynamically resolved during the application’s startup sequence. Accessing them before the endpoints are allocated results in an exception.
IResourceWithEndpoints
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();
Understanding Endpoint Allocation and Resolution
What Does "Allocated" Mean?
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.
Comparison: Run Mode vs. Publish Mode
| Context | Run Mode | Publish Mode |
|---|---|---|
| Endpoint Values | Fully resolved (tcp://localhost:6379). |
Represented by manifest expressions ({redis.bindings.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.
Accessing Allocated Endpoints Safely
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.
Example: Checking Allocation and Using Eventing
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();
Output
-
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 WithEnvironent that take a callback run after endpoints have been allocated.
Referencing Endpoints from Other Resources
Using WithReference
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.
Using WithEnvironment
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.
Core Primitives: .Property and ReferenceExpression
Endpoints must be handled differently in run mode and publish mode because their resolution mechanisms vary. While run mode provides fully allocated endpoints at startup, publish mode relies on manifest expressions resolved by the deployment infrastructure.
Aspire provides two essential tools to manage this difference:
-
.Property: Allows you to access individual endpoint components (e.g.,Host,Port) dynamically. -
ReferenceExpression: Enables deferred resolution of complex strings, such as URLs or connection strings.
Why .Property and ReferenceExpression Are Necessary
The Challenge
- In run mode, endpoint values (e.g.,
Host,Port) resolve during startup.- Example:
Host = localhost,Port = 6379.
- Example:
- In publish mode, endpoint values are placeholders (manifest expressions) like
{redis.bindings.tcp.host}:{redis.bindings.tcp.port}.
Attempting to directly access endpoint properties (e.g., Host, Url) in publish mode will fail because their values are undefined.
The Solution
-
.Propertydefers resolution of individual components likeHostandPort. -
ReferenceExpressiondefers resolution of entire strings like URLs or connection strings.
Using .Property
.Property ensures that each endpoint component resolves dynamically based on the context.
Example: Building a Redis Connection String
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
builder.AddProject<Worker>("worker")
.WithEnvironment(context =>
{
var endpoint = redis.GetEndpoint("tcp");
// Use .Property to access Host and Port expressions and defer evaluation
var redisHost = endpoint.Property(EndpointProperty.Host);
var redisPort = endpoint.Property(EndpointProperty.Port);
// Set these values as REDIS_HOST/PORT on the target resource
context.EnvironmentVariables["REDIS_HOST"] = redisHost;
context.EnvironmentVariables["REDIS_PORT"] = redisPort;
});
builder.Build().Run();
Why .Property Works
-
Run Mode: Resolves to values like
localhostand6379. -
Publish Mode: Defers to manifest expressions like
{redis.bindings.tcp.host}:{redis.bindings.tcp.port}.
Using ReferenceExpression
ReferenceExpression allows deferred resolution of entire values, making it ideal for constructing URLs or connection strings.
Example: Constructing a Redis URL
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
builder.AddProject<Worker>("worker")
.WithEnvironment(context =>
{
var endpoint = redis.GetEndpoint("tcp");
// Use .Property to access Host and Port expressions and defer evaluation
var redisHost = endpoint.Property(EndpointProperty.Host);
var redisPort = endpoint.Property(EndpointProperty.Port);
// Use ReferenceExpression for deferred resolution
context.EnvironmentVariables["REDIS_URL"] = ReferenceExpression.Create(
$"redis://{redisHost }:{redisPort}"
);
});
builder.Build().Run();
ReferenceExpression.Create takes an interpolated string and captures the appropriate values as expressions that can be evaluated in correct context (PS: There's a more advanced document here to write on ReferenceExpressions).
NOTE: There are overloads of WithEnvironment that directly take an interpolated string or ReferenceExpression.
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
var endpoint = redis.GetEndpoint("tcp");
// Use .Property to access Host and Port expressions and defer evaluation
var redisHost = endpoint.Property(EndpointProperty.Host);
var redisPort = endpoint.Property(EndpointProperty.Port);
builder.AddProject<Worker>("worker")
.WithEnvironment("REDIS_URL", $"redis://{redisHost }:{redisPort}");
builder.Build().Run();
Or using a ReferenceExpression directly:
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddContainer("redis", "redis")
.WithEndpoint(name: "tcp", targetPort: 6379);
var endpoint = redis.GetEndpoint("tcp");
// Use .Property to access Host and Port expressions and defer evaluation
var redisHost = endpoint.Property(EndpointProperty.Host);
var redisPort = endpoint.Property(EndpointProperty.Port);
var redisUrl = ReferenceExpression.Create($"redis://{redisHost}:{redisPort}");
builder.AddProject<Worker>("worker")
.WithEnvironment("REDIS_URL", redisUrl);
builder.Build().Run();
This is an extremely powerful technique that can be used to construct all types of different connection formats which allow integrating with applications that don't adhere to the connection string formats that .NET Aspire resources use by default.
Context-Based Endpoint Resolution
Aspire resolves endpoints differently based on the relationship between the source and target resources. This ensures proper communication across all environments.
Resolution Rules
| 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 |
Advanced Scenario: Dynamic Endpoint Resolution Across Contexts
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.
Example: Cross-Context Communication
Code Example
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();
Great article!
I did not understand the last example. Why do we need to handle run mode differently? Why not let the aspire handle like in publish mode?
It’s missing some context. This is an edge case where the default resolution rules don’t work because you’re passing around an endpoint that you want to be resolve in a different context.
The above example shows an api project that is going to setup up grafana and keycloak, we need to give it the address for container to container communication even though the target resource is a project. The project isn’t directly talking to keycloak, it’s just setting URLs in the grafana configuration.
@davidebbo Can you take a look at the above to see if I missed anything?
@davidfowl I will try to read through in the next few days. Having it as an issue comment makes it a bit hard to do inline commenting. Ideally, if you had it as a PR, it would be easier.
Agree will do
I've already shared this draft with a few colleagues and found it helpful. Would love to see this published to the live docs..
Hi @davidfowl , @davidebbo
Thanks for providing this... it is very helpful.
The following did not compile for me with a static event handler, works OK without.
// Subscribe to AfterEndpointsAllocatedEvent for resolved properties
builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(
static (@event, cancellationToken) =>
{
Console.WriteLine($"Endpoint allocated: {endpoint.IsAllocated}");
Console.WriteLine($"Resolved Url: {endpoint.Url}");
return Task.CompletedTask;
});
@SophisticatedConsulting yeah, that looks like a bug in the code. Handler can't be static if it wants to access the endpoint.
I think it would be worth it for each of the major examples to show how you actually access these in your project code
Fixed
I just realized we need to extend this article to also talk about Parameters and how they all come together to form connection strings.
My 2 cents: this article should definitely be in the official docs