fix: auto-register correlated logging for minimal API hosts (#5503)#5511
fix: auto-register correlated logging for minimal API hosts (#5503)#5511
Conversation
TestWebApplicationFactory<T> registered AddCorrelatedTUnitLogging() inside CreateHostBuilder(), which returns null for minimal API apps using WebApplication.CreateBuilder — silently skipping registration via the null-propagating ConfigureServices call. Move the registration to a ConfigureWebHost(IWebHostBuilder) override, which is invoked by WebApplicationFactory<T> for both IHostBuilder-based and minimal API hosts. Add TUnit.AspNetCore.Tests.MinimalApi as a clean regression target with zero-override factory and test that asserts correlated logging is wired up by the base class alone. Verified by reverting the fix and confirming the new test fails. Removes a workaround in the existing TestWebAppFactory that registered logging via ConfigureWebHost manually.
There was a problem hiding this comment.
Code Review
The fix is correct and well-motivated. Moving from to is exactly the right layer — guarantees is called for all host models, so this closes the silent-skip path for minimal API apps.
The regression test design is particularly clean: having zero overrides is self-documenting evidence that the base class is doing the work. Well done there.
Issue 1: Potential double-registration for callers who applied the workaround themselves
Any user who read the old XML doc and applied the workaround (calling AddCorrelatedTUnitLogging() in their own ConfigureWebHost override without calling base) will now get the service registered twice. Both:
IStartupFilter → TUnitTestContextStartupFilter(causesTUnitTestContextMiddlewareto run twice in the pipeline)ILoggerProvider → CorrelatedTUnitLoggerProvider(duplicates every log write to stdout)
ASP.NET Core allows multiple registrations of interface-typed singletons, so neither will be rejected — they'll just silently double-up. The first is worse because middleware duplication can cause subtle context-overwrite bugs under concurrency.
Suggested improvement: Guard against double-registration in AddCorrelatedTUnitLogging:
This makes the extension idempotent (like AddLogging() itself) and removes the migration burden from users.
Issue 2: Silent behavioral change for subclasses overriding ConfigureWebHost without calling base
Before this PR, for IHostBuilder-based apps, logging was registered in CreateHostBuilder() — independent of whether the subclass overrode ConfigureWebHost. Now the registration depends entirely on base.ConfigureWebHost(builder) being called.
Any existing subclass like:
…will silently lose correlated logging on all host types (not just minimal API). The XML doc warns about it, but this is a semantic change to a protected override that previously had no base to call.
Suggested improvement: Since this affects the public API surface, consider adding a note in release notes / changelog and/or bumping the minor version if SemVer is observed. Alternatively, extract the registration into a separate non-overridable helper and call it from both ConfigureWebHost and ConfigureHostBuilder to provide belt-and-suspenders coverage — though that adds complexity.
Minor observation: ConfigureStartupConfiguration is still skipped for minimal API hosts in the shared (non-isolated) path
In CreateHostBuilder():
This is a pre-existing gap (not introduced here) — ConfigureStartupConfiguration is not wired into the minimal API host path outside of GetIsolatedFactory. Worth a follow-up issue if users rely on it.
Summary
| Verdict | |
|---|---|
| Fix correctness | ✅ Correct approach |
| Regression test | ✅ Solid zero-override pattern |
| Workaround removal | ✅ Appropriate cleanup |
| Double-registration risk | |
| Subclass breaking change |
The core fix is solid. The double-registration guard (Issue 1) is the highest-priority follow-up as it can cause subtle runtime bugs for users who migrated the workaround.
There was a problem hiding this comment.
Code Review
The fix is correct and well-motivated. Moving AddCorrelatedTUnitLogging() from CreateHostBuilder() to ConfigureWebHost(IWebHostBuilder) is exactly the right layer — WebApplicationFactory<T> guarantees ConfigureWebHost is called for all host models, so this closes the silent-skip path for minimal API apps.
The regression test design is particularly clean: MinimalApiTestFactory having zero overrides is self-documenting evidence that the base class is doing the work. Well done there.
Issue 1: Potential double-registration for callers who applied the workaround themselves
Any user who read the old XML doc and applied the workaround (calling AddCorrelatedTUnitLogging() in their own ConfigureWebHost override without calling base) will now get the service registered twice. Both:
IStartupFilter -> TUnitTestContextStartupFilter(causesTUnitTestContextMiddlewareto run twice in the pipeline)ILoggerProvider -> CorrelatedTUnitLoggerProvider(duplicates every log write to stdout)
ASP.NET Core allows multiple registrations of interface-typed singletons, so neither will be rejected — they will just silently double-up. The first is worse because middleware duplication can cause subtle context-overwrite bugs under concurrency.
Suggested improvement: Guard against double-registration in AddCorrelatedTUnitLogging:
public static IServiceCollection AddCorrelatedTUnitLogging(
this IServiceCollection services,
LogLevel minLogLevel = LogLevel.Information)
{
if (services.Any(d => d.ImplementationType == typeof(TUnitTestContextStartupFilter)
|| d.ImplementationInstance is TUnitTestContextStartupFilter))
return services;
services.AddSingleton<IStartupFilter>(new TUnitTestContextStartupFilter());
services.AddSingleton<ILoggerProvider>(new CorrelatedTUnitLoggerProvider(minLogLevel));
return services;
}This makes the extension idempotent (like AddLogging() itself) and removes the migration burden from users.
Issue 2: Silent behavioral change for subclasses overriding ConfigureWebHost without calling base
Before this PR, for IHostBuilder-based apps, logging was registered in CreateHostBuilder() — independent of whether the subclass overrode ConfigureWebHost. Now the registration depends entirely on base.ConfigureWebHost(builder) being called.
Any existing subclass like:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// forgot to call base — they did not need to before this PR
builder.ConfigureTestServices(services => services.AddSingleton<IMyService, MyFakeService>());
}...will silently lose correlated logging on all host types (not just minimal API). The XML doc warns about it, but this is a semantic change to a protected override that previously had no base to call.
Suggested improvement: If the idempotency guard from Issue 1 is applied, you could keep a belt-and-suspenders registration in CreateHostBuilder as well. The double call becomes harmless, and existing subclasses that override ConfigureWebHost without calling base will still get logging via the CreateHostBuilder path for IHostBuilder-based apps. A changelog note for this behavioral change would also help.
Minor observation: ConfigureStartupConfiguration is still skipped for minimal API hosts in the shared (non-isolated) path
In CreateHostBuilder(), hostBuilder?.ConfigureHostConfiguration(ConfigureStartupConfiguration) is a null-conditional call, so for minimal API hosts where CreateHostBuilder returns null, ConfigureStartupConfiguration is never invoked on the shared factory path. This is a pre-existing gap (not introduced by this PR), but worth a follow-up issue if users rely on it.
Summary
| Verdict | |
|---|---|
| Fix correctness | Correct approach |
| Regression test | Solid zero-override pattern |
| Workaround removal | Appropriate cleanup |
| Double-registration risk | Recommend making extension idempotent |
| Subclass breaking change | Consider changelog note or belt-and-suspenders approach |
The core fix is solid. The double-registration guard (Issue 1) is the highest-priority follow-up as it can cause subtle runtime bugs for users who previously applied the workaround themselves.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 1 medium |
🟢 Metrics 0 complexity
Metric Results Complexity 0
TIP This summary will be updated as you push new changes. Give us feedback
Summary
TestWebApplicationFactory<T>was registeringAddCorrelatedTUnitLogging()insideCreateHostBuilder(), which returnsnullfor minimal API hosts (WebApplication.CreateBuilder/ top-level statements). The null-propagatinghostBuilder?.ConfigureServices(...)silently skipped registration, leaving server-sideILoggeroutput uncorrelated to the owningTestContext.ConfigureWebHost(IWebHostBuilder)override.WebApplicationFactory<T>invokes this hook for bothIHostBuilder-based and minimal API hosts, so correlated logging is now wired up for both hosting models with no subclass cooperation required.TUnit.AspNetCore.Tests/TestWebAppFactory.csthat manually registered logging viaConfigureWebHost.TUnit.AspNetCore.Tests.MinimalApi(minimal API webapp with namespacedProgramso it can coexist with the existingTUnit.AspNetCore.Tests.WebApp's globalProgramin the same test assembly) plusMinimalApiAutoRegistrationTestswith a zero-overrideMinimalApiTestFactory— the existence of the no-override factory is the regression test.Test plan
dotnet buildclean acrossTUnit.AspNetCore.Core,TUnit.AspNetCore.Tests.MinimalApi, andTUnit.AspNetCore.TestsTUnit.AspNetCore.Tests6/6 passing onnet10.0(5 existingCorrelatedLoggingResolverTests+ 1 newServerLog_AutoCorrelated_OnMinimalApiHost_WithoutSubclassConfig)git stash), reran the new test, confirmed it fails with the marker missing fromTestContext.Current.GetStandardOutput(). Restored the fix and the test passes again — proves the test catches the exact bugnet8.0,net9.0,net10.0Notes for reviewers
ConfigureWebHostnow must callbase.ConfigureWebHost(builder)to inherit logging registration. This is documented in the XML doc on the override. The new no-override regression test catches accidental drift in the base behavior.CorrelatedLoggingResolverTests(which useTestWebAppFactoryagainst the existing minimal APITUnit.AspNetCore.Tests.WebApp) would have started failing once the workaround inTestWebAppFactorywas removed if the base class fix were absent, so they also serve as additional regression coverage.