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