dotnet / dotnet/extensions

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

Open
#7,567 1 comment 1 reaction 0 assignees View on GitHub
api-suggestion untriaged
Dominant language
C#
Stars
3.2k
Forks
894
Avg merge
1d 12h
Merged PRs (30d)
23

Description

### Background and motivation

## Background and Motivation

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

```csharp
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:

```text
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:

```text
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

```csharp
namespace Microsoft.Extensions.ServiceDiscovery;

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

### API Usage

```csharp
builder.Services.AddServiceDiscovery();

builder.Services.AddSingleton();

builder.Services.AddHttpClient()
.AddServiceDiscovery();
```

```csharp
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}";
}
}
```

```csharp
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:

```text
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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.