-
Notifications
You must be signed in to change notification settings - Fork 0
Add CorrelationIdDelegatingHandler and fix DI registration in pipeline integration tests #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,13 +18,17 @@ The format is inspired by Keep a Changelog, and this project follows Semantic Ve | |||||
| - **Duplicate-Heavy Locations** panel redesigned from a plain list to a ranked visual impact view with rank badges, share percentages, and proportional impact bars. | ||||||
|
|
||||||
| ### Added | ||||||
| - **`StorageLens.Shared.Infrastructure.Tests` project** — new xUnit test project covering the HTTP client policy pipeline behaviour: | ||||||
| - `HttpClientPolicyPipelineIntegrationTests` with three integration tests verifying that `CorrelationIdDelegatingHandler` is correctly resolved from DI and properly adds the `X-Correlation-Id` header to outgoing requests, does not overwrite an already-present header, and generates a unique ID per request. | ||||||
| - **`CorrelationIdDelegatingHandler`** class added to `StorageLens.Shared.Infrastructure` — a `DelegatingHandler` that automatically attaches a `X-Correlation-Id` header to every outgoing HTTP request when the header is not already present, enabling end-to-end correlation tracing across services. | ||||||
| - **Chart details modal patterns** on Dashboard and Reports pages for chart click-drill interactions. | ||||||
| - **Interaction hints** on chart headers (e.g., "Click bars for details", "Click points for details") to make interactivity discoverable. | ||||||
| - **Alerts page** (`/Alerts`) with adjustable min/max threshold sliders, test recipient support, and automatic threshold-check email dispatch when usage reaches the max threshold. | ||||||
| - **SMTP alert service** (`SmtpAlertEmailService`) with cooldown protection to avoid repeated email sends on page refresh. | ||||||
| - **`AlertsModelTests`** unit test validating that threshold crossing triggers an email dispatch call. | ||||||
|
|
||||||
| ### Fixed | ||||||
| - **`CorrelationIdDelegatingHandler` DI registration** — added `services.AddSingleton<CorrelationIdDelegatingHandler>()` in `HttpClientPolicyPipelineIntegrationTests` so the handler is resolvable when `AddHttpMessageHandler<T>()` activates it through the DI container. Previously tests threw `System.InvalidOperationException: No service for type 'StorageLens.Shared.Infrastructure.CorrelationIdDelegatingHandler' has been registered.` | ||||||
|
||||||
| - **`CorrelationIdDelegatingHandler` DI registration** — added `services.AddSingleton<CorrelationIdDelegatingHandler>()` in `HttpClientPolicyPipelineIntegrationTests` so the handler is resolvable when `AddHttpMessageHandler<T>()` activates it through the DI container. Previously tests threw `System.InvalidOperationException: No service for type 'StorageLens.Shared.Infrastructure.CorrelationIdDelegatingHandler' has been registered.` | |
| - **`CorrelationIdDelegatingHandler` DI registration** — added `services.AddTransient<CorrelationIdDelegatingHandler>()` in `HttpClientPolicyPipelineIntegrationTests` so the handler is resolvable when `AddHttpMessageHandler<T>()` activates it through the DI container. Previously tests threw `System.InvalidOperationException: No service for type 'StorageLens.Shared.Infrastructure.CorrelationIdDelegatingHandler' has been registered.` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| namespace StorageLens.Shared.Infrastructure; | ||
|
|
||
| public class CorrelationIdDelegatingHandler : DelegatingHandler | ||
| { | ||
| public const string HeaderName = "X-Correlation-Id"; | ||
|
|
||
| protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
| { | ||
| if (!request.Headers.Contains(HeaderName)) | ||
| { | ||
| request.Headers.TryAddWithoutValidation(HeaderName, Guid.NewGuid().ToString("N")); | ||
| } | ||
|
|
||
| return base.SendAsync(request, cancellationToken); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| using System.Net; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using StorageLens.Shared.Infrastructure; | ||
| using Xunit; | ||
|
|
||
| namespace StorageLens.Shared.Infrastructure.Tests; | ||
|
|
||
| public class HttpClientPolicyPipelineIntegrationTests | ||
| { | ||
| [Fact] | ||
| public async Task HttpClient_WithCorrelationIdHandler_AddsCorrelationIdHeader() | ||
| { | ||
| HttpRequestMessage? capturedRequest = null; | ||
|
|
||
| var services = new ServiceCollection(); | ||
| services.AddSingleton<CorrelationIdDelegatingHandler>(); | ||
| services.AddHttpClient("test") | ||
| .AddHttpMessageHandler<CorrelationIdDelegatingHandler>() | ||
|
Comment on lines
+15
to
+18
|
||
| .ConfigurePrimaryHttpMessageHandler(() => new StubHttpMessageHandler(req => | ||
| { | ||
| capturedRequest = req; | ||
| return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); | ||
| })); | ||
|
|
||
| var provider = services.BuildServiceProvider(); | ||
| var factory = provider.GetRequiredService<IHttpClientFactory>(); | ||
| var client = factory.CreateClient("test"); | ||
|
|
||
| await client.GetAsync("http://localhost/test"); | ||
|
|
||
| Assert.NotNull(capturedRequest); | ||
| Assert.True(capturedRequest.Headers.Contains(CorrelationIdDelegatingHandler.HeaderName)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task HttpClient_WithCorrelationIdHandler_DoesNotOverwriteExistingHeader() | ||
| { | ||
| HttpRequestMessage? capturedRequest = null; | ||
| const string existingCorrelationId = "existing-correlation-id"; | ||
|
|
||
| var services = new ServiceCollection(); | ||
| services.AddSingleton<CorrelationIdDelegatingHandler>(); | ||
| services.AddHttpClient("test") | ||
| .AddHttpMessageHandler<CorrelationIdDelegatingHandler>() | ||
| .ConfigurePrimaryHttpMessageHandler(() => new StubHttpMessageHandler(req => | ||
|
Comment on lines
+41
to
+45
|
||
| { | ||
| capturedRequest = req; | ||
| return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); | ||
| })); | ||
|
|
||
| var provider = services.BuildServiceProvider(); | ||
| var factory = provider.GetRequiredService<IHttpClientFactory>(); | ||
| var client = factory.CreateClient("test"); | ||
|
|
||
| var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/test"); | ||
| request.Headers.TryAddWithoutValidation(CorrelationIdDelegatingHandler.HeaderName, existingCorrelationId); | ||
| await client.SendAsync(request); | ||
|
|
||
| Assert.NotNull(capturedRequest); | ||
| var headerValues = capturedRequest.Headers.GetValues(CorrelationIdDelegatingHandler.HeaderName); | ||
| Assert.Equal(existingCorrelationId, headerValues.Single()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task HttpClient_WithCorrelationIdHandler_GeneratesUniqueIdsPerRequest() | ||
| { | ||
| var correlationIds = new List<string>(); | ||
|
|
||
| var services = new ServiceCollection(); | ||
| services.AddSingleton<CorrelationIdDelegatingHandler>(); | ||
| services.AddHttpClient("test") | ||
| .AddHttpMessageHandler<CorrelationIdDelegatingHandler>() | ||
| .ConfigurePrimaryHttpMessageHandler(() => new StubHttpMessageHandler(req => | ||
|
Comment on lines
+69
to
+73
|
||
| { | ||
| var id = req.Headers.GetValues(CorrelationIdDelegatingHandler.HeaderName).First(); | ||
| correlationIds.Add(id); | ||
| return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); | ||
| })); | ||
|
|
||
| var provider = services.BuildServiceProvider(); | ||
| var factory = provider.GetRequiredService<IHttpClientFactory>(); | ||
| var client = factory.CreateClient("test"); | ||
|
|
||
| await client.GetAsync("http://localhost/test"); | ||
| await client.GetAsync("http://localhost/test"); | ||
|
|
||
| Assert.Equal(2, correlationIds.Count); | ||
| Assert.NotEqual(correlationIds[0], correlationIds[1]); | ||
| } | ||
|
|
||
| private sealed class StubHttpMessageHandler( | ||
| Func<HttpRequestMessage, Task<HttpResponseMessage>> handler) : HttpMessageHandler | ||
| { | ||
| protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
| => handler(request); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|
|
||||||
| <PropertyGroup> | ||||||
| <TargetFramework>net8.0</TargetFramework> | ||||||
| <ImplicitUsings>enable</ImplicitUsings> | ||||||
| <Nullable>enable</Nullable> | ||||||
| <IsPackable>false</IsPackable> | ||||||
| <IsTestProject>true</IsTestProject> | ||||||
| </PropertyGroup> | ||||||
|
|
||||||
| <ItemGroup> | ||||||
| <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" /> | ||||||
|
||||||
| <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" /> | |
| <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.12" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changelog bullets document registering
CorrelationIdDelegatingHandleras a singleton. ForIHttpClientFactory+AddHttpMessageHandler<T>(), singleton lifetime is unsafe; update the changelog to reflect the correct transient (or factory-based) registration so docs match the intended DI configuration.