-
Notifications
You must be signed in to change notification settings - Fork 839
Description
Background and motivation
HTTP requests to downstream services often require consistent classification and metadata handling for telemetry, routing, and policy application. The HttpDependencyMetadataResolver
provides a highly optimized solution using trie-based lookups to efficiently map HTTP requests to their corresponding metadata.
By exposing this as a public abstract class, we allow applications to:
- Leverage the core functionality without re-implementing complex matching logic
- Extend or customize the implementation for specific environments
- Integrate with existing service discovery or configuration systems
An abstract class is chosen mainly because we must support .NET Framework, where default interface implementations are unavailable. An interface alone would force every consumer who needs even a minor extension (alternate host resolution, custom path normalization, dynamic metadata refresh) to re‑implement the entire resolution pipeline, duplicating the high‑performance trie logic and risking divergence. The abstract base lets us lock down and reuse the optimized core (host suffix trie, route/method matching, wildcard handling) while exposing just enough surface for advanced scenarios that need controlled overrides, and it remains mockable for tests without leaking internals. This gives safe extensibility under old target frameworks and eliminates boilerplate that a pure interface would impose there.
API Proposal
namespace Microsoft.Extensions.Http.Diagnostics;
/// <summary>
/// Resolves metadata for HTTP requests based on hostname, path, and method patterns.
/// </summary>
/// <remarks>
/// This class provides a high-performance way to identify HTTP requests by mapping them to previously
/// configured metadata using specialized trie-based data structures. This enables efficient lookup
/// of service information, operation names, and other metadata for telemetry and policy application.
/// </remarks>
public abstract class HttpDependencyMetadataResolver
{
if NET462
/// <summary>
/// Gets request metadata for the specified HTTP web request.
/// </summary>
/// <param name="requestMessage">The HTTP web request.</param>
/// <returns>The resolved <see cref="RequestMetadata"/> if found; otherwise, <see langword="null"/>.</returns>
public virtual RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage);
#else
/// <summary>
/// Gets request metadata for the specified HTTP request message.
/// </summary>
/// <param name="requestMessage">The HTTP request message.</param>
/// <returns>The resolved <see cref="RequestMetadata"/> if found; otherwise, <see langword="null"/>.</returns>
public virtual RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage);
#endif
}
HttpDependencyMetadataResolver
conditionally exposes a single GetRequestMetadata
overload per target framework: on modern targets it provides the HttpRequestMessage
version (for the recommended HttpClient
/ IHttpClientFactory
pipeline), and on .NET Framework (net462) it instead provides the HttpWebRequest
version to preserve legacy compatibility. Conditional compilation ensures only the relevant API is surfaced, while the internal resolution logic (method, host, path -> metadata) remains identical across target frameworks. This avoids duplicate surface area, keeps call sites clean, and still delivers consistent classification behavior on every supported runtime.
The API proposal includes an extension method to register the HttpDependencyMetadataResolver
and its default implementation in the service collection:
namespace Microsoft.Extensions.DependencyInjection
/// <summary>
/// Extensions for telemetry utilities.
/// </summary>
public static class HttpDiagnosticsServiceCollectionExtensions
{
/// <summary>
/// Adds services required for HTTP dependency metadata resolution.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddStandardHttpDependencyMetadataResolver(this IServiceCollection services);
}
API Usage
The example below demonstrates a comprehensive HTTP request processing flow that showcases how the proposed abstract class enables flexible dependency resolution. The flow illustrates how the HttpDependencyMetadataResolver
abstract class would serve as a core building block in an HTTP processing pipeline.
Register extension methods
// Registers only the resolver (no providers). Must add providers separately before first use.
builder.Services.AddDownstreamDependencyMetadata<PaymentApiMetadata>()
.AddDownstreamDependencyMetadata<StorageApiMetadata>()
.AddStandardHttpDependencyMetadataResolver();
Example metadata providers (minimal):
internal sealed class PaymentApiMetadata : IDownstreamDependencyMetadata
{
public string DependencyName => "PaymentsService";
public ISet<string> UniqueHostNameSuffixes { get; } =
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "payments.example.com" };
public ISet<RequestMetadata> RequestMetadata { get; } = new HashSet<RequestMetadata>
{
new("GET", "/v1/orders/{orderId}", "GetOrder"),
new("POST", "/v1/orders", "CreateOrder")
};
}
internal sealed class StorageApiMetadata : IDownstreamDependencyMetadata
{
public string DependencyName => "StorageService";
public ISet<string> UniqueHostNameSuffixes { get; } =
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "storage.internal.local" };
public ISet<RequestMetadata> RequestMetadata { get; } = new HashSet<RequestMetadata>
{
new("GET", "/blobs/{container}/{blob}", "GetBlob")
};
}
Metrics publishing delegating handler using the default resolver:
public sealed class DependencyMetricsHandler : DelegatingHandler
{
private static readonly Meter Meter = new("MyCompany.Http.Outgoing");
private static readonly Histogram<double> DurationMs =
Meter.CreateHistogram<double>("http.outgoing.duration.ms", unit: "ms");
private static readonly Counter<long> RequestCount =
Meter.CreateCounter<long>("http.outgoing.requests");
private readonly HttpDependencyMetadataResolver _resolver;
private static readonly HttpRequestOptionsKey<RequestMetadata> MetaKey =
new("DependencyMetadata");
private static readonly HttpRequestOptionsKey<long> StartTicksKey =
new("DependencyStartTicks");
public DependencyMetricsHandler(HttpDependencyMetadataResolver resolver) => _resolver = resolver;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
var meta = _resolver.GetRequestMetadata(request);
if (meta != null)
{
request.Options.Set(MetaKey, meta);
}
var start = Stopwatch.GetTimestamp();
request.Options.Set(StartTicksKey, start);
HttpResponseMessage response = null!;
Exception error = null;
try
{
response = await base.SendAsync(request, ct).ConfigureAwait(false);
return response;
}
catch (Exception ex)
{
error = ex;
throw;
}
finally
{
PublishMetrics(request, response, error, start, meta);
}
}
private static void PublishMetrics(
HttpRequestMessage request,
HttpResponseMessage? response,
Exception? error,
long startTicks,
RequestMetadata? meta)
{
var elapsedMs = Stopwatch.GetElapsedTime(startTicks).TotalMilliseconds;
var dependency = meta?.DependencyName ?? "unknown";
var operation = meta?.RequestName != null && meta.RequestName != TelemetryConstants.Unknown
? meta.RequestName
: (meta?.RequestRoute ?? "unknown");
var statusCode = response?.StatusCode != null
? ((int)response.StatusCode).ToString()
: (error is null ? "0" : "ERR");
var outcome = error is null
? (response != null && response.IsSuccessStatusCode ? "success" : "failure")
: "exception";
DurationMs.Record(
elapsedMs,
new KeyValuePair<string, object?>("dependency", dependency),
new KeyValuePair<string, object?>("operation", operation),
new KeyValuePair<string, object?>("status_code", statusCode),
new KeyValuePair<string, object?>("outcome", outcome),
new KeyValuePair<string, object?>("method", request.Method.Method));
RequestCount.Add(
1,
new KeyValuePair<string, object?>("dependency", dependency),
new KeyValuePair<string, object?>("operation", operation),
new KeyValuePair<string, object?>("outcome", outcome));
}
}
Additional example that uses resolver directly in the application code:
public sealed class PaymentClient
{
private readonly HttpClient _http;
private readonly HttpDependencyMetadataResolver _resolver;
public PaymentClient(HttpClient http, HttpDependencyMetadataResolver resolver)
{
_http = http;
_resolver = resolver;
}
public async Task<HttpResponseMessage> GetOrderAsync(string id, CancellationToken ct = default)
{
var req = new HttpRequestMessage(HttpMethod.Get, $"https://example.com/v1/orders/{id}");
var meta = _resolver.GetRequestMetadata(req); // enrich logs if desired
return await _http.SendAsync(req, ct).ConfigureAwait(false);
}
}
Alternative Designs
While the abstract class design for HttpDependencyMetadataResolver
offers significant benefits through shared implementation logic, an interface-based approach could provide greater flexibility in certain scenarios.
public interface IHttpDependencyMetadataResolver
{
if NET462
/// <summary>
/// Gets request metadata for the specified HTTP web request.
/// </summary>
/// <param name="requestMessage">The HTTP web request.</param>
/// <returns>The resolved <see cref="RequestMetadata"/> if found; otherwise, <see langword="null"/>.</returns>
public virtual RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage);
#else
/// <summary>
/// Gets request metadata for the specified HTTP request message.
/// </summary>
/// <param name="requestMessage">The HTTP request message.</param>
/// <returns>The resolved <see cref="RequestMetadata"/> if found; otherwise, <see langword="null"/>.</returns>
public virtual RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage);
#endif
}
We could have exposed just an interface here, but because we must support .NET Framework (no default interface implementations) every implementation would then have to re‑implement or copy the optimized host/route/method trie logic. Using an abstract class lets us keep a single, high‑performance implementation while still allowing controlled extension points, avoiding duplicate code and performance drift across frameworks.
Risks
No response