fix(aspire): route CreateHttpClient through IHttpClientFactory#5957
Conversation
AspireFixture.CreateHttpClient constructed HttpClient directly from a cached SocketsHttpHandler when EnableTelemetryCollection was true, bypassing the AppHost's IHttpClientFactory. DelegatingHandlers registered via ConfigureHttpClientDefaults / AddHttpClient in ConfigureBuilder were silently dropped. The runtime's built-in DiagnosticsHandler propagates traceparent/baggage on every HttpClient regardless of construction path, so the telemetry branch wasn't actually doing anything telemetry-specific. Aspire's App.CreateHttpClient already trusts dev certs via the factory's primary handler. Collapse to always delegate. Regression test in OtlpCorrelationIntegrationTests verifies a handler registered via ConfigureHttpClientDefaults fires; the shared fixture gains a singleton invocation counter exposed via HttpHandlerInvocationCount.
There was a problem hiding this comment.
Code Review
This is a clean, well-scoped fix. The root cause is correctly identified and the solution is appropriately minimal.
AspireFixture.cs
The collapse to a single delegation line is the right call. The previous EnableTelemetryCollection branch was creating a raw SocketsHttpHandler — no ActivityPropagationHandler, no DiagnosticsHandler — so the doc comment claiming it "automatically propagates W3C traceparent/baggage" was never true. The fix removes both the lie and the bug simultaneously.
Disposal cleanup is correct: removing _httpHandler?.Dispose() / _httpHandler = null from DisposeAsync follows from the field deletion.
IntegrationTestFixture.cs
Counter design is solid. Volatile.Read for the read + Interlocked.Increment for the write is the correct minimal pattern for a shared counter under concurrent access. Sealing both helper types is good practice.
Handler registration is idiomatic. AddSingleton<HttpHandlerInvocationCounter> + AddTransient<CountingDelegatingHandler> + AddHttpMessageHandler<CountingDelegatingHandler>() is the standard Typed Handler pattern — the transient handler gets a new instance per HttpClient pipeline but they all share the singleton counter through DI, which is what you want.
One minor note: HttpHandlerInvocationCount calls GetRequiredService<HttpHandlerInvocationCounter>() on every access. Since this is a test-only property called rarely, it's not a problem in practice — but if this property ever grows hot-path usage, caching the resolved singleton at initialization time would be cleaner.
OtlpCorrelationIntegrationTests.cs
The before-delta pattern (var before = ...; /* act */; Assert > before) is the right way to handle the shared fixture here: it doesn't assume the counter starts at zero (other tests may have already fired it), and it still catches a regression where the handler is never invoked. The test is as simple as it needs to be.
Overall: correct fix, clean simplification, good regression coverage. No blocking concerns.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.44.0 to 1.45.8. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.45.8 <!-- Release notes generated using configuration in .github/release.yml at v1.45.8 --> ## What's Changed ### Other Changes * fix(aspire): route CreateHttpClient through IHttpClientFactory by @thomhurst in thomhurst/TUnit#5957 ### Dependencies * chore(deps): update tunit to 1.45.0 by @thomhurst in thomhurst/TUnit#5949 * chore(deps): update dependency dompurify to v3.4.5 by @thomhurst in thomhurst/TUnit#5951 * chore(deps): update dependency microsoft.testing.extensions.codecoverage to 18.7.0 by @thomhurst in thomhurst/TUnit#5953 * chore(deps): update dependency coverlet.collector to 10.0.1 by @thomhurst in thomhurst/TUnit#5952 * chore(deps): update dependency polyfill to 10.6.0 by @thomhurst in thomhurst/TUnit#5955 * chore(deps): update dependency polyfill to 10.6.0 by @thomhurst in thomhurst/TUnit#5954 **Full Changelog**: thomhurst/TUnit@v1.45.0...v1.45.8 ## 1.45.0 <!-- Release notes generated using configuration in .github/release.yml at v1.45.0 --> ## What's Changed ### Other Changes * fix(generator): place CallerArgumentExpression before params in [GenerateAssertion] emit by @JohnVerheij in thomhurst/TUnit#5940 * fix(sourcegen): drop covariant TActual when [GenerateAssertion] method has its own type parameters by @JohnVerheij in thomhurst/TUnit#5935 * feat(assertions): add CancellationToken overload to WaitsFor and Eventually by @JohnVerheij in thomhurst/TUnit#5938 * fix(html-report): extract categories using MTP Key=name convention by @thomhurst in thomhurst/TUnit#5946 * feat(html-report): rewrite as split-pane design template by @thomhurst in thomhurst/TUnit#5947 ### Dependencies * chore(deps): update microsoft.testing to 2.2.3 by @thomhurst in thomhurst/TUnit#5927 * chore(deps): update mstest to 4.2.3 by @thomhurst in thomhurst/TUnit#5928 * chore(deps): update tunit to 1.44.39 by @thomhurst in thomhurst/TUnit#5929 * chore(deps): update aspire to 13.3.3 by @thomhurst in thomhurst/TUnit#5933 * chore(deps): update dependency dompurify to v3.4.4 by @thomhurst in thomhurst/TUnit#5944 * chore(deps): update dependency qs to v6.15.2 by @thomhurst in thomhurst/TUnit#5941 **Full Changelog**: thomhurst/TUnit@v1.44.39...v1.45.0 ## 1.44.39 <!-- Release notes generated using configuration in .github/release.yml at v1.44.39 --> ## What's Changed ### Other Changes * fix(tests): retry trx read to dodge MTP post-exit flush race on Windows by @thomhurst in thomhurst/TUnit#5888 * fix(pipeline): timeout + retry InstallPlaywrightModule so a hung download fails fast by @thomhurst in thomhurst/TUnit#5889 * fix(otel): require two consecutive idle windows in DrainAsync to catch in-transit POSTs by @thomhurst in thomhurst/TUnit#5890 * test(assertions): drop flaky wall-clock upper bound on WaitsFor timeout test by @thomhurst in thomhurst/TUnit#5886 * fix(sourcegen): drop spurious ')' in MethodAssertionGenerator Task<bool> emit by @JohnVerheij in thomhurst/TUnit#5920 * fix(sourcegen): merge generic parameter lists in [AssertionExtension] emit by @JohnVerheij in thomhurst/TUnit#5921 * fix(aspnetcore): scope correlation processor per-factory to stop cross-factory tag leak by @thomhurst in thomhurst/TUnit#5891 * Changed FSharp.Core version to 10.1.300 by @licon4812 in thomhurst/TUnit#5909 * feat(mocks): add Mock.HttpClientFactory() helper by @thomhurst in thomhurst/TUnit#5894 * Harden WaitsFor timeout test by @thomhurst in thomhurst/TUnit#5926 * fix(sourcegen): emit `default` literal for value-type assertion parameters by @JohnVerheij in thomhurst/TUnit#5919 ### Dependencies * chore(deps): update dependency nunit to 4.6.0 by @thomhurst in thomhurst/TUnit#5826 * chore(deps): update tunit to 1.44.0 by @thomhurst in thomhurst/TUnit#5882 * chore(deps): update dependency mockolate to 3.2.0 by @thomhurst in thomhurst/TUnit#5892 * chore(deps): update dependency yaml to v2.9.0 by @thomhurst in thomhurst/TUnit#5887 * chore(deps): update dependency nuget.protocol to 7.6.0 by @thomhurst in thomhurst/TUnit#5897 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.8 by @thomhurst in thomhurst/TUnit#5898 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.300 by @thomhurst in thomhurst/TUnit#5899 * chore(deps): update microsoft.extensions by @thomhurst in thomhurst/TUnit#5905 * chore(deps): update microsoft.aspnetcore to 10.0.8 by @thomhurst in thomhurst/TUnit#5904 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.300 by @thomhurst in thomhurst/TUnit#5902 * chore(deps): update aspire to 13.3.1 by @thomhurst in thomhurst/TUnit#5900 * chore(deps): update dependency system.commandline to 2.0.8 by @thomhurst in thomhurst/TUnit#5903 * chore(deps): update dependency azure.storage.blobs to 12.28.0 by @thomhurst in thomhurst/TUnit#5910 * chore(deps): update dependency dotnet-sdk to v10.0.300 by @thomhurst in thomhurst/TUnit#5901 * chore(deps): update dependency stackexchange.redis to 2.13.1 by @thomhurst in thomhurst/TUnit#5906 * chore(deps): update aspire to 13.3.2 by @thomhurst in thomhurst/TUnit#5924 * chore(deps): bump mermaid from 11.12.2 to 11.15.0 in /docs by @dependabot[bot] in thomhurst/TUnit#5893 * chore(deps): update dependency streamjsonrpc to 2.24.92 by @thomhurst in thomhurst/TUnit#5915 * chore(deps): update dependency dompurify to v3.4.3 by @thomhurst in thomhurst/TUnit#5913 * chore(deps): update microsoft.build to 18.6.3 by @thomhurst in thomhurst/TUnit#5914 **Full Changelog**: thomhurst/TUnit@v1.44.0...v1.44.39 Commits viewable in [compare view](thomhurst/TUnit@v1.44.0...v1.45.8). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
AspireFixture<TAppHost>.CreateHttpClientconstructedHttpClientdirectly from a cachedSocketsHttpHandlerwhenEnableTelemetryCollectionwas true, bypassing the AppHost'sIHttpClientFactory. AnyDelegatingHandlerregistered viaConfigureHttpClientDefaultsorAddHttpClientinConfigureBuilderwas silently dropped.DiagnosticsHandlerpropagatestraceparent/baggageon everyHttpClientregardless of construction path, and Aspire's ownApp.CreateHttpClientalready trusts dev certs via the factory's primary handler — so the telemetry branch wasn't doing anything telemetry-specific. Collapse to always delegate toApp.CreateHttpClient. Dropped the unused_httpHandlerfield and its disposal.OtlpCorrelationIntegrationTests. The sharedIntegrationTestFixturenow registers aCountingDelegatingHandlerviaConfigureHttpClientDefaultsand exposesHttpHandlerInvocationCount(backed by a DI singleton — no static mutable state).Test plan
dotnet build TUnit.Aspire.Tests/TUnit.Aspire.Tests.csprojpasses locallyCreateHttpClient_Invokes_Registered_DelegatingHandlerand passesOtlpCorrelationIntegrationTestsstill pass (handler is a pass-through, shouldn't affect OTLP correlation)