From fd186cf6a44c5ca41ae74255ba6732ab6dd10274 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Thu, 7 Mar 2024 13:18:09 +0300 Subject: [PATCH] Import UserAgentVersionHandler and its extensions (#213) `UserAgentVersionHandler` adds `User-Agen`t header from provided value and optionally clears existing ones (e.g. the framework). This is useful for outgoing requests that should not know about the framework. The extensions included also help pick the version from an assembly's version. --- .../IHttpClientBuilderExtensions.cs | 110 ++++++++++++++++++ .../UserAgentVersionHandler.cs | 38 ++++++ 2 files changed, 148 insertions(+) create mode 100644 src/Tingle.Extensions.Http/IHttpClientBuilderExtensions.cs create mode 100644 src/Tingle.Extensions.Http/UserAgentVersionHandler.cs diff --git a/src/Tingle.Extensions.Http/IHttpClientBuilderExtensions.cs b/src/Tingle.Extensions.Http/IHttpClientBuilderExtensions.cs new file mode 100644 index 00000000..3b850496 --- /dev/null +++ b/src/Tingle.Extensions.Http/IHttpClientBuilderExtensions.cs @@ -0,0 +1,110 @@ +using System.Reflection; +using Tingle.Extensions.Http; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for . +/// +public static class IHttpClientBuilderExtensions +{ + /// + /// Adds a that adds a User-Agent header to each outgoing request. + /// The version is pulled from the assembly containing + /// whereas the name is pulled from the entry assembly or the one containing . + /// + /// The type from which to pull the assembly version. + /// The to use. + /// Whether to clear User-Agent headers. + /// The . + public static IHttpClientBuilder AddUserAgentVersionHandler(this IHttpClientBuilder builder, bool clear = false) + { + var name = Assembly.GetEntryAssembly()?.GetName().Name + ?? typeof(T).Assembly.GetName().Name + ?? throw new InvalidOperationException("Unable to get the name from the entry assembly or the one containing the type argument."); + return builder.AddUserAgentVersionHandler(name, clear); + } + + /// + /// Adds a that adds a User-Agent header to each outgoing request. + /// The version is pulled from the assembly containing . + /// + /// The type from which to pull the assembly version. + /// The to use. + /// The product name to use. + /// Whether to clear User-Agent headers. + /// The . + public static IHttpClientBuilder AddUserAgentVersionHandler(this IHttpClientBuilder builder, string name, bool clear = false) + { + return builder.AddUserAgentVersionHandler(typeof(T), name, clear); + } + + /// + /// Adds a that adds a User-Agent header to each outgoing request. + /// The version is pulled from the assembly containing . + /// + /// The to use. + /// The type from which to pull the assembly version + /// The product name to use. + /// Whether to clear User-Agent headers. + /// The . + public static IHttpClientBuilder AddUserAgentVersionHandler(this IHttpClientBuilder builder, Type type, string name, bool clear = false) + { + return builder.AddUserAgentVersionHandler(type.Assembly, name, clear); + } + + /// + /// Adds a that adds a User-Agent header to each outgoing request. + /// The version is pulled from the . + /// + /// The to use. + /// The from which to pull the version. + /// The product name to use. + /// Whether to clear User-Agent headers. + /// The . + public static IHttpClientBuilder AddUserAgentVersionHandler(this IHttpClientBuilder builder, Assembly assembly, string name, bool clear = false) + { + /* + * Use the informational version if available because it has the git commit sha. + * Using the git commit sha allows for maximum reproduction. + * + * Examples: + * 1) 1.7.1-ci.131+Branch.main.Sha.752f6cdfabb76e65d2b2cd18b3b284ef65713213 + * 2) 1.7.1-PullRequest10247.146+Branch.pull-10247-merge.Sha.bf46008b75eacacad3b7654959d38f8df4c7fcdb + * 3) 1.7.1-fixes-2021-10-12-2.164+Branch.fixes-2021-10-12-2.Sha.bf46008b75eacacad3b7654959d38f8df4c7fcdb + * 4) 1.9.3+Branch.migration-to-hc.Sha.ed9934bab03eaca1dfcef2c212372f1e6820418e + * + * When not available, use the usual assembly version + */ + string? version = null; + var attr = assembly.GetCustomAttribute(); + if (attr is not null && !string.IsNullOrWhiteSpace(attr.InformationalVersion)) + { + version = attr.InformationalVersion; + } + else + { + version ??= assembly.GetName().Version!.ToString(3); + } + + return builder.AddUserAgentVersionHandler(name, version, clear); + } + + /// + /// Adds a that adds a User-Agent header to each outgoing request. + /// + /// The to use. + /// The product name to use. + /// The version to use. + /// Whether to clear User-Agent headers. + /// The . + public static IHttpClientBuilder AddUserAgentVersionHandler(this IHttpClientBuilder builder, string name, string version, bool clear = false) + { + if (builder is null) throw new ArgumentNullException(nameof(builder)); + if (string.IsNullOrEmpty(name)) throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); + if (string.IsNullOrEmpty(version)) throw new ArgumentException($"'{nameof(version)}' cannot be null or empty.", nameof(version)); + + // a message handler is used because, some scenarios do not support configuring HttpClient (e.g. gRPC clients via DI) + return builder.AddHttpMessageHandler(() => new UserAgentVersionHandler(name, version, clear)); + } +} diff --git a/src/Tingle.Extensions.Http/UserAgentVersionHandler.cs b/src/Tingle.Extensions.Http/UserAgentVersionHandler.cs new file mode 100644 index 00000000..98cd0ad8 --- /dev/null +++ b/src/Tingle.Extensions.Http/UserAgentVersionHandler.cs @@ -0,0 +1,38 @@ +using System.Net.Http.Headers; + +namespace Tingle.Extensions.Http; + +internal class UserAgentVersionHandler : DelegatingHandler +{ + private readonly string name; + private readonly string version; + private readonly bool clear; + + public UserAgentVersionHandler(string name, string version, bool clear) + { + if (string.IsNullOrWhiteSpace(this.name = name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + + if (string.IsNullOrWhiteSpace(this.version = version)) + { + throw new ArgumentException($"'{nameof(version)}' cannot be null or whitespace.", nameof(version)); + } + + this.clear = clear; + } + + /// + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (clear) request.Headers.UserAgent.Clear(); + + // populate the User-Agent header + var userAgent = new ProductInfoHeaderValue(name, version); + request.Headers.UserAgent.Add(userAgent); + + // execute the request + return base.SendAsync(request, cancellationToken); + } +}