Skip to content

Programmatic YARP Configuration in Aspire via New Route and Destination Resources #9878

@benjaminpetit

Description

@benjaminpetit

Description:

Configuring YARP in Aspire via code is currently verbose and unintuitive. To address this, I propose a new approach based on #9766, introducing dedicated resources for routing.

Proposed Solution:

  • Introduce new Route and Destination resources (aligned with YARP’s terminology, where "Destination" maps to "Cluster").
  • Configure these resources via fluent methods, similar to how ports are exposed on a project.
  • All configuration will be done via extension methods that modify the underlying RouteConfig or ClusterConfig objects, ensuring a consistent and expressive API surface.
  • This enables a more natural and composable way to define reverse proxy behavior directly in code, without relying on JSON blobs.

YarpRoute

public class YarpRoute(IResourceBuilder<YarpResource> parent)
{
    internal IResourceBuilder<YarpResource> ParentBuilder { get; } = parent;

    internal RouteConfig RouteConfig { get; private set; } = new RouteConfig
    {
        [...]
    };

    internal void Configure(Func<RouteConfig, RouteConfig> configure)
    {
        RouteConfig = configure(RouteConfig);
    }
}

YarpDestination

/// <summary>
/// Represents a destination for YARP routes
/// </summary>
public class YarpDestination(IResourceBuilder<YarpResource> parent, EndpointReference endpoint)
{
    internal IResourceBuilder<YarpResource> ParentBuilder { get; } = parent;

    internal ClusterConfig ClusterConfig { get; private set; } = new()
    {
        [...]
    };

    internal void Configure(Func<ClusterConfig, ClusterConfig> configure)
    {
        ClusterConfig = configure(ClusterConfig);
    }
}

Extensions

All configuration will be done with extensions. For example, a subset of the extensions for YarpRoute could be:

/// <summary>
/// Provides extension methods for configuring a YARP destination
/// </summary>
public static class YarpRouteExtensions
{
    [...]

    /// <summary>
    /// Only match requests that use these optional HTTP methods. E.g. GET, POST.
    /// </summary>
    public static YarpRoute WithMatchMethods(this YarpRoute route, params string[] methods)
    {
        route.ConfigureMatch(match => match with { Methods = methods });
        return route;
    }

    /// <summary>
    /// Only match requests that contain all of these headers.
    /// </summary>
    public static YarpRoute WithMatchHeaders(this YarpRoute route, params RouteHeader[] headers)
    {
        route.ConfigureMatch(match => match with { Headers = headers.ToList() });
        return route;
    }

    [...]

    /// <summary>
    /// Set the Metadata of the destination
    /// </summary>
    public static YarpRoute WithMetadata(this YarpRoute route, IReadOnlyDictionary<string, string>? metadata)
    {
        route.Configure(r => r with { Metadata = metadata });
        return route;
    }

    [...]
}

Usage Examples

Basic Example

In this scenario, the developer only interacts with the Route resource and passes a reference to the endpoint being proxied:

var yarp = builder.AddYarp("apigateway");

// catalog 
yarp.AddRoute("/catalog/{**catch-all}")
    .WithTransformPathRemovePrefix("/catalog")
    .WithReference(catalogService.GetEndpoint("http"));

// basket
yarp.AddRoute("/basket/{**catch-all}")
    .WithTransformPathRemovePrefix("/basket")
    .WithReference(basketService.GetEndpoint("http"));

Advanced Example

This approach allows you to explicitly create a reusable destination using AddDestination(...), reference it from multiple routes, and customize its behavior using WithForwarderRequestConfig(...):

var yarp = builder.AddYarp("apigateway");

var backend = yarp.AddDestination(backendService.GetEndpoint("http"))
    .WithForwarderRequestConfig(new ForwarderRequestConfig
    {
        ActivityTimeout = TimeSpan.FromSeconds(30),
        Version = HttpVersion.Version20
    });

yarp.AddRoute("/somepath/{**catch-all}")
    .WithMatchMethods("GET", "HEAD")
    .WithTransformPathRemovePrefix("/somepath")
    .WithReference(backend);

yarp.AddRoute("/anotherpath/{**catch-all}")
    .WithMatchMethods("POST")
    .WithMaxRequestBodySize(1000)
    .WithTransformPathRemovePrefix("/anotherpath")
    .WithTransformRequestHeader("x-my-header", "custom-value")
    .WithReference(backend);

Benefits:

  • Aligns with Aspire’s developer-friendly philosophy.
  • Reduces boilerplate and improves discoverability.
  • Makes routing logic easier to test, reuse, and extend.
  • Enables fine-grained control over forwarding behavior and destination reuse.
  • Future-proof design: Extending this model to support middleware configuration (e.g., authentication, rate limiting, logging) will be straightforward and will not introduce breaking changes, thanks to the composable and fluent API structure.

Metadata

Metadata

Assignees

Labels

area-integrationsIssues pertaining to Aspire Integrations packages

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions