diff --git a/Net.Sdk.Web.Extensions/Http/CorrelationVectorHandler.cs b/Net.Sdk.Web.Extensions/Http/CorrelationVectorHandler.cs new file mode 100644 index 0000000..27d6f97 --- /dev/null +++ b/Net.Sdk.Web.Extensions/Http/CorrelationVectorHandler.cs @@ -0,0 +1,40 @@ +using AspNetCore.Extensions; +using AspNetCore.Extensions.Options; +using Microsoft.CorrelationVector; +using Microsoft.Extensions.Options; +using System.Core.Extensions; + +namespace Net.Sdk.Web.Extensions.Http; + +public sealed class CorrelationVectorHandler : DelegatingHandler +{ + private readonly CorrelationVectorOptions options; + private readonly IHttpContextAccessor httpContextAccessor; + + public CorrelationVectorHandler( + IOptions options, + IHttpContextAccessor httpContextAccessor) + { + this.options = options.ThrowIfNull().Value.ThrowIfNull(); + this.httpContextAccessor = httpContextAccessor.ThrowIfNull(); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (this.httpContextAccessor.HttpContext?.GetCorrelationVector() is CorrelationVector cv) + { + cv.Increment(); + request.Headers.Add(this.options.Header, cv.Value); + this.httpContextAccessor.HttpContext.SetCorrelationVector(cv); + } + + var response = await base.SendAsync(request, cancellationToken); + if (response.Headers.TryGetValues(this.options.Header, out var cvHeaders) && + cvHeaders.FirstOrDefault() is string responseCv) + { + this.httpContextAccessor.HttpContext?.SetCorrelationVector(CorrelationVector.Parse(responseCv)); + } + + return response; + } +} diff --git a/Net.Sdk.Web.Extensions/Http/HttpClientBuilder.cs b/Net.Sdk.Web.Extensions/Http/HttpClientBuilder.cs new file mode 100644 index 0000000..82fa625 --- /dev/null +++ b/Net.Sdk.Web.Extensions/Http/HttpClientBuilder.cs @@ -0,0 +1,128 @@ +using System.Core.Extensions; + +namespace Net.Sdk.Web.Extensions.Http; + +public sealed class HttpClientBuilder + where T : class +{ + private readonly WebApplicationBuilder app; + private readonly List> handlers = []; + private readonly List Values)>> defaultRequestHeaders = []; + private readonly List redactedLoggedHeaderNames = []; + private readonly List> headerRedactHandlers = []; + + private bool defaultLogger = false; + private Uri? baseAddress; + private long maxResponseBufferSize = 2147483647L; //2GB default HttpClient value [https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.maxresponsecontentbuffersize?view=net-6.0] + private TimeSpan timeout = TimeSpan.FromSeconds(100); //100 seconds default HttpClient value [https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=net-6.0] + + internal HttpClientBuilder(WebApplicationBuilder app) + { + this.app = app.ThrowIfNull(); + } + + public HttpClientBuilder WithBaseAddress(Uri baseAddress) + { + baseAddress.ThrowIfNull(); + + this.baseAddress = baseAddress; + return this; + } + + public HttpClientBuilder WithDelegatingHandler(Func handlerFactory) + { + handlerFactory.ThrowIfNull(); + + this.handlers.Add(handlerFactory); + return this; + } + + public HttpClientBuilder WithDefaultRequestHeaders(Func Values)> headerFactory) + { + headerFactory.ThrowIfNull(); + + this.defaultRequestHeaders.Add(headerFactory); + return this; + } + + public HttpClientBuilder WithMaxResponseBufferSize(long responseBufferSize) + { + this.maxResponseBufferSize = responseBufferSize; + return this; + } + + public HttpClientBuilder WithRedactedLoggerHeaderName(string headerName) + { + this.redactedLoggedHeaderNames.Add(headerName); + return this; + } + + public HttpClientBuilder WithRedactedLoggerHeaderName(Func handler) + { + this.headerRedactHandlers.Add(handler); + return this; + } + + public HttpClientBuilder WithTimeout(TimeSpan timeout) + { + this.timeout = timeout; + return this; + } + + public HttpClientBuilder AddDefaultLogger() + { + this.defaultLogger = true; + return this; + } + + public WebApplicationBuilder Build() + { + this.CreateBuilder(); + return this.app; + } + + public IHttpClientBuilder CreateBuilder() + { + var builder = this.app.Services.AddHttpClient(typeof(T).Name); + foreach (var handler in this.handlers) + { + builder.AddHttpMessageHandler(handler); + } + + builder.ConfigureHttpClient((sp, client) => + { + client.BaseAddress = this.baseAddress; + client.Timeout = this.timeout; + client.MaxResponseContentBufferSize = this.maxResponseBufferSize; + foreach (var header in this.defaultRequestHeaders) + { + (var name, var values) = header(sp); + client.DefaultRequestHeaders.Add(name, values); + } + }); + + if (this.defaultLogger) + { + builder.AddDefaultLogger(); + } + + if (this.redactedLoggedHeaderNames.Count > 0) + { + builder.RedactLoggedHeaders(this.redactedLoggedHeaderNames); + } + + foreach (var handler in this.headerRedactHandlers) + { + builder.RedactLoggedHeaders(handler); + } + + this.app.Services.AddScoped>(sp => + { + var clientBuilder = sp.GetRequiredService(); + var innerClient = clientBuilder.CreateClient(typeof(T).Name); + return new HttpClient(innerClient); + }); + + return builder; + } +} \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj index 4321214..9426793 100644 --- a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj +++ b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj @@ -6,7 +6,7 @@ enable Library true - 0.1 + 0.5 Alexandru Macocian LICENSE true @@ -26,7 +26,8 @@ - + + diff --git a/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs b/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs index bf25e28..4f4915b 100644 --- a/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs +++ b/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs @@ -1,4 +1,6 @@ using AspNetCore.Extensions.Attributes; +using AspNetCore.Extensions.Middleware; +using Net.Sdk.Web.Extensions.Http; using System.Core.Extensions; using System.Extensions; @@ -20,6 +22,36 @@ public static class WebApplicationBuilderExtensions return configurationManager.GetRequiredSection(GetOptionsName()); } + public static WebApplicationBuilder WithCorrelationVector(this WebApplicationBuilder builder) + { + builder.ThrowIfNull(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + return builder; + } + + public static Net.Sdk.Web.Extensions.Http.HttpClientBuilder RegisterHttpClient(this WebApplicationBuilder builder) + where T : class + { + _ = builder.ThrowIfNull(); + return new Net.Sdk.Web.Extensions.Http.HttpClientBuilder(builder); + } + + public static Net.Sdk.Web.Extensions.Http.HttpClientBuilder WithCorrelationVector(this Net.Sdk.Web.Extensions.Http.HttpClientBuilder httpClientBuilder) + where T : class + { + return httpClientBuilder.ThrowIfNull() + .WithDelegatingHandler(sp => sp.GetRequiredService()); + } + + public static IHttpClientBuilder WithCorrelationVector(this IHttpClientBuilder httpClientBuilder) + where T : class + { + return httpClientBuilder.ThrowIfNull() + .AddHttpMessageHandler(); + } + private static string GetOptionsName() { var maybeAttribute = typeof(TOptions).GetCustomAttributes(false).OfType().FirstOrDefault(); diff --git a/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs b/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs index 0cfab57..c72a5cc 100644 --- a/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs +++ b/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using AspNetCore.Extensions.Middleware; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using System.Core.Extensions; @@ -10,6 +11,14 @@ namespace AspNetCore.Extensions.Websockets.Extensions; public static class WebApplicationExtensions { + public static WebApplication UseCorrelationVector(this WebApplication webApplication) + { + webApplication.ThrowIfNull() + .UseMiddleware(); + + return webApplication; + } + public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route) where TWebSocketRoute : WebSocketRouteBase {