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); + } +}