-
Notifications
You must be signed in to change notification settings - Fork 0
wiki bugs
Issues found while building the recipe under strict (TreatWarningsAsErrors=true,
AnalysisMode=All, IDisposableAnalyzers) settings.
Severity: logic bug — the recipe still runs, but the parameter is dead and the shape misleads readers.
The wiki shows:
internal sealed class DispatchMiddleware(
RequestMiddleware<ProcessRequest, ProcessResponse> next)
{
public async Task InvokeAsync(
RequestContext<ProcessRequest, ProcessResponse> context,
AppDbContext db,
IHttpClientFactory httpFactory)
{
var client = httpFactory.CreateClient("downstream");
// ... do work ...
await db.SaveChangesAsync(context.CancellationToken);
context.Response = new ProcessResponse(context.Id.ToString(), "ok");
}
}The constructor takes next (it has to — Plumber's class-middleware convention requires
RequestMiddleware<TReq,TRes> next as the first constructor parameter), but InvokeAsync
never calls next(context). That's actually correct for a terminal middleware that owns
the response, but the code stores the parameter via the primary-constructor capture and the
field is never read. Under strict analyzers this surfaces as IDE0052 / IDE0060-style
warnings depending on tooling.
The recipe also implicitly teaches a non-obvious rule — that the last Use<T>() in a
chain can stop short of calling next — without saying so. A one-line callout would
help.
Fix in this sample: added a _ = next; discard inside InvokeAsync and a comment
explaining that this middleware is terminal by design. See DispatchMiddleware.cs.
Severity: strict-build only.
The wiki snippet does var client = httpFactory.CreateClient("downstream");. The returned
HttpClient is IDisposable, but the documented contract for IHttpClientFactory is
that the factory owns the lifetime and you must NOT dispose it (disposing it
returns the underlying handler to the pool early and breaks the whole point of
HttpClientFactory).
Standard analyzers can't see that contract, so IDISP004 — Don't ignore created IDisposable fires on the assignment. Documenting the suppression in the recipe (or a
parenthetical "the factory owns the client; do not using-wrap it") would save readers
the discovery.
Fix in this sample: [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP004:...")] on DispatchMiddleware.InvokeAsync with a justification.
3. services.AddSingleton(sp => RequestHandler.Create(sp)) triggers IDISP004 in the registration extension
Severity: strict-build only.
PipelineRegistration.AddProcessingPipeline is the recipe-recommended way to register
the handler. Inside the factory lambda the code creates an IDisposable
(RequestHandler<,>) and hands it to AddSingleton, which captures the lifetime in
the host's container. IDisposableAnalyzers doesn't model that ownership transfer and
flags the lambda body with IDISP004.
The recipe's "Disposal semantics" section explains why this is safe (the host DI container disposes singletons on shutdown), but the snippet itself doesn't acknowledge that strict analyzers will need a local suppression.
Fix in this sample: member-level [SuppressMessage("...", "IDISP004:...")] on
AddProcessingPipeline with a justification linking back to the disposal-semantics
guarantees.
Severity: blocking at runtime — pipeline activation throws InvalidOperationException on the first request.
The wiki shows:
internal sealed class EnrichmentMiddleware(
RequestMiddleware<ProcessRequest, ProcessResponse> next,
ILogger<EnrichmentMiddleware> logger,
TimeProvider clock)
{
...
}TimeProvider is not auto-registered by the .NET 10 host. Without an explicit
builder.Services.AddSingleton(TimeProvider.System) in Program.cs, the first request
crashes with:
System.InvalidOperationException: Unable to resolve service for type 'System.TimeProvider'
while attempting to activate 'EnrichmentMiddleware'.
The recipe's Program.cs snippet shows none of the lines that would supply it.
Fix in this sample: added builder.Services.AddSingleton(TimeProvider.System) in
Program.cs with a comment pointing at this wiki gap. Worth fixing in the recipe text —
or worth a one-line note that TimeProvider.System registration is the host's
responsibility.
Severity: non-blocking — these are illustrative types in the wiki, not promised to be defined.
The recipe references IMyService, MyService, and AppDbContext in the
builder.Services.Add* snippets and in DispatchMiddleware, but never defines them.
That's fine for prose, but a reader copy-pasting the recipe gets a compile error.
Fix in this sample: added trivial stand-in types:
-
IMyService/MyService— registered for completeness (no middleware actually injects them; the wiki doesn't either). -
AppDbContext— a no-op stub with aSaveChangesAsync(CancellationToken)method soDispatchMiddlewarecompiles. We deliberately did NOT pull inMicrosoft.EntityFrameworkCore— the sample's job is to demonstrate Plumber's host-mode wiring, not EF Core.
The stub's instance method triggers CA1822 (mark as static) — suppressed inline with
a justification that real EF Core's SaveChangesAsync is genuinely instance-bound.
Severity: strict-build only.
The wiki declares both records public sealed record. Under AnalysisMode=All,
CA1515 — Make types internal fires because nothing outside the assembly references
them. Either:
- declare them
internal(the modern guidance for app-only types), or - suppress CA1515.
Staying faithful to the recipe means keeping public. We suppressed CA1515 at the type
level on both records.
Standing project-wide suppressions inherited from Directory.Build.props:
CA1308, CA2007, CA1812, CA1848. Same rationale as the file-watcher sample.
Per-call-site / per-member suppressions added for this recipe:
-
CA1515onProcessRequest,ProcessResponse— wiki declares thempublic. -
CA1822onAppDbContext.SaveChangesAsync— stand-in shape mimics EF Core. -
IDISP004onDispatchMiddleware.InvokeAsync—IHttpClientFactoryowns the client. -
IDISP004onPipelineRegistration.AddProcessingPipeline— host DI owns the singleton handler.
dotnet format Recipe.AspNet.sln will reorder imports (system-first does not match the
project's dotnet_sort_system_directives_first = false). Always run
dotnet format Recipe.AspNet.sln (NOT bare dotnet format — both .sln and .csproj
sit in the same folder, which makes argumentless dotnet format throw
FileNotFoundException).
The recipe demonstrates the registration shape but never shows the pipeline producing real output. To make the sample runnable end-to-end:
-
IMyService.Summarize(tenant, payload)— the wiki'sIMyServiceis undefined. We gave it a real method that returns a string summary of the request, so the response reflects actual pipeline work. -
DispatchMiddleware— the wiki showshttpFactory.CreateClient("downstream")and then// ... do work ...(which would require either a real downstream or a fake handler). We swappedIHttpClientFactoryforIMyService(method-injected, scoped via Plumber's per-invocation scope). Same pedagogical point — scoped service consumption inside terminal middleware — without a network dependency. - Removed
builder.Services.AddHttpClient("downstream")fromProgram.cssince nothing consumes it after the swap above.
POST {"tenant":"acme","payload":{"orderId":"ord-1","amount":42}} to /process →
{"correlationId":"...","status":"tenant=acme; payload-kind=Object; properties=2"}.
POST {"tenant":"","payload":{...}} → {"correlationId":"...","status":"rejected"}.
- "Testing" — sample doesn't include a test project; recipe's
WebApplicationFactoryguidance is well-formed and would compile if a test project were added. - The optional controller variant (
ProcessController) — the minimal-API endpoint alone exercises the host-mode integration; adding a controller would requireAddControllers/MapControllersplumbing without showing anything new about Plumber.
- .NET 10
- ASP.NET Core 10 (
Microsoft.NET.Sdk.Web, framework reference resolves transitively) - Plumber 3.x via
ProjectReferencetoD:\plumber\Plumber\Plumber.csproj -
IDisposableAnalyzers4.0.8
Documents Plumber v4.x · Repository · MIT License · Report an issue
Getting Started
Pipeline (core)
Testing
Serilog Extensions
Diagnostics
Recipes
- AWS Lambda — API Gateway
- AWS Lambda — SQS
- Azure Functions — HTTP
- SQS polling console
- ASP.NET Core integration
- BackgroundService worker
- Webhook receiver
- Multi-command CLI
- File watcher
- Configuration reload
Repo · NuGet · NuGet — Testing · NuGet — Serilog · NuGet — Diagnostics