Skip to content

[API Proposal]: Add extensibility point for service discovery resolver key generation #7567

@Ipmants

Description

@Ipmants

Background and motivation

Background and Motivation

HttpServiceEndpointResolver currently caches and reuses resolver instances using a key derived from:

request.RequestUri.GetLeftPart(UriPartial.Authority)

This works well when the logical authority fully describes the service discovery context. However, there are scenarios where endpoint resolution depends on additional request-specific metadata.

In our case, we use the standard HttpClient flow together with the existing service discovery infrastructure. Some requests contain special discovery-related query parameters, prefixed with X_, which influence how endpoints should be resolved.

For example:

https://storage-api/reports/image.png?X_DiscoveryTenant=tenant-a&X_DiscoveryRegion=eu-west&format=png

In this request:

  • format is an ordinary application parameter,
  • X_DiscoveryTenant and X_DiscoveryRegion are intended to participate in service discovery.

Different combinations of discovery parameters may require independent resolver entries and endpoint watchers. However, because the current implementation only considers the URI authority, the following requests all share the same resolver instance:

https://storage-api/reports/image.png?X_DiscoveryTenant=tenant-a&X_DiscoveryRegion=eu-west

https://storage-api/reports/image.png?X_DiscoveryTenant=tenant-b&X_DiscoveryRegion=eu-west

Today, applications requiring this behavior must implement workarounds outside of the service discovery infrastructure, such as:

  • encoding discovery metadata into the authority portion of the URI,
  • maintaining an external cache that maps a deterministic key to discovery metadata,
  • creating multiple HttpClient instances for combinations of discovery parameters,
  • avoiding ResolvingHttpDelegatingHandler and HttpServiceEndpointResolver altogether by implementing a custom DelegatingHandler that directly uses ServiceEndpointResolver.

The latter approach allows applications to implement their own resolver reuse strategy. However, it also requires reimplementing functionality that is currently provided by ResolvingHttpDelegatingHandler, including selecting an endpoint from the resolver result set and translating the selected endpoint into the final request URI (for example, functionality equivalent to GetUriWithEndpoint). As a result, applications must duplicate parts of the existing service discovery pipeline rather than extending it through a supported customization point.

These approaches add complexity and move resolver identity concerns outside of the service discovery implementation.

Providing an extensibility point for resolver key generation would allow advanced scenarios to participate in the existing resolver lifecycle management while preserving the current behavior as the default.

This proposal is not intended to change endpoint selection behavior itself. Rather, it aims to provide applications with a way to control how resolver instances are identified and reused when additional request-specific discovery metadata is involved.

API Proposal

namespace Microsoft.Extensions.ServiceDiscovery;

public interface IServiceDiscoveryKeyProvider
{
    string GetKey(HttpRequestMessage request);
}

API Usage

builder.Services.AddServiceDiscovery();

builder.Services.AddSingleton<IServiceDiscoveryKeyProvider, QueryBasedServiceDiscoveryKeyProvider>();

builder.Services.AddHttpClient<MyClient>()
    .AddServiceDiscovery();
public sealed class QueryBasedServiceDiscoveryKeyProvider : IServiceDiscoveryKeyProvider
{
    public string GetKey(HttpRequestMessage request)
    {
        var uri = request.RequestUri!;

        // Some simplified example implemenation
        var query = string.Join(
            "&",
            uri.Query
                .TrimStart('?')
                .Split('&', StringSplitOptions.RemoveEmptyEntries)
                .Select(part => part.Split('=', 2))
                .Where(parts => parts[0].StartsWith("X_", StringComparison.OrdinalIgnoreCase))
                .OrderBy(parts => parts[0], StringComparer.OrdinalIgnoreCase)
                .Select(parts => parts.Length == 2
                    ? $"{parts[0]}={parts[1]}"
                    : parts[0]));

        return string.IsNullOrEmpty(query)
            ? uri.GetLeftPart(UriPartial.Authority)
            : $"{uri.GetLeftPart(UriPartial.Authority)}?{query}";
    }
}
await httpClient.GetAsync(
    "https://storage-api/reports/image.png?X_DiscoveryTenant=tenant-a&X_DiscoveryRegion=eu-west&format=png");

await httpClient.GetAsync(
    "https://storage-api/reports/image.png?X_DiscoveryRegion=eu-west&X_DiscoveryTenant=tenant-a&format=jpeg");

await httpClient.GetAsync(
    "https://storage-api/other-resource?X_DiscoveryTenant=tenant-b&X_DiscoveryRegion=eu-west");

Generated resolver keys:

https://storage-api?X_DiscoveryRegion=eu-west&X_DiscoveryTenant=tenant-a

https://storage-api?X_DiscoveryRegion=eu-west&X_DiscoveryTenant=tenant-a

https://storage-api?X_DiscoveryRegion=eu-west&X_DiscoveryTenant=tenant-b

Expected behavior:

  • The first two requests reuse the same resolver instance because they produce the same resolver key.
  • The third request uses a different resolver instance because the discovery metadata differs.
  • The request path and ordinary application query parameters do not participate in resolver key generation.
  • Only query parameters intended for service discovery (for example, those prefixed with X_) are included in the resolver key.
  • Discovery parameters are ordered deterministically before generating the resolver key.

Alternative Designs

Instead of introducing a dedicated IServiceDiscoveryKeyProvider abstraction, resolver key generation could be exposed through an options-based delegate.

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationuntriaged

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions