Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New article]: Configuring, Referencing, and Resolving Endpoints in .NET Aspire #2340

Open
davidfowl opened this issue Dec 30, 2024 · 7 comments
Labels
doc-idea Indicates issues that are suggestions for new topics [org][type][category] ⌚ Not Triaged Not triaged

Comments

@davidfowl
Copy link
Member

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"

@davidfowl davidfowl added the doc-idea Indicates issues that are suggestions for new topics [org][type][category] label Dec 30, 2024
@dotnetrepoman dotnetrepoman bot added the ⌚ Not Triaged Not triaged label Dec 30, 2024
@davidfowl
Copy link
Member Author

davidfowl commented Dec 31, 2024

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>(
    static (@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:

  1. .Property: Allows you to access individual endpoint components (e.g., Host, Port) dynamically.
  2. 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.
  • 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

  • .Property defers resolution of individual components like Host and Port.
  • ReferenceExpression defers 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 localhost and 6379.
  • 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();

@JobaDiniz
Copy link

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?

@davidfowl
Copy link
Member Author

davidfowl commented Dec 31, 2024

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.

@davidfowl
Copy link
Member Author

@davidebbo Can you take a look at the above to see if I missed anything?

@davidebbo
Copy link
Contributor

@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.

@davidfowl
Copy link
Member Author

Agree will do

@afscrome
Copy link

I've already shared this draft with a few colleagues and found it helpful. Would love to see this published to the live docs..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
doc-idea Indicates issues that are suggestions for new topics [org][type][category] ⌚ Not Triaged Not triaged
Projects
None yet
Development

No branches or pull requests

4 participants