-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
Short answers to the questions that come up most often.
Same shape, different host.
RequestContext<TRequest, TResponse> is the typed analogue of HttpContext. The Use overloads, the onion execution model, and the per-request DI scope all behave the same way.
The mental model transfers wholesale — the typed (TRequest, TResponse) pair is the only real difference.
Yes. Use RequestHandler.Create<TReq, TRes>(serviceProvider) to build a handler that reuses the host's IServiceProvider instead of building its own.
This is useful when a non-HTTP pipeline (a background worker, a queue handler, a webhook dispatcher) should share the host's services. See Advanced and the ASP.NET Core integration recipe.
MediatR is a command bus: many handlers, routed by request type, dispatched by the framework.
Plumber is a pipeline: one chain per (TRequest, TResponse) pair, traversed in registration order, with each step able to short-circuit.
Use MediatR when the dispatcher is the point. Use Plumber when the pipeline is the point — a Lambda function, a queue consumer, a CLI command. See Concepts for the long-form comparison.
No. The smallest pipeline registers no services and uses no scope-aware features:
using var handler = RequestHandlerBuilder
.Create<string, string>()
.Build();
handler.Use((context, next) =>
{
context.Response = $"Hello, {context.Request}!";
return next(context);
});DI lights up when you have services to share — call ConfigureServices on the builder, then resolve via constructor or method injection on a class middleware.
Heuristic:
-
Delegate for one-off transformations that have no dependencies — a small lambda passed to
handler.Use((context, next) => ...). - Class for anything with dependencies, anything you want to test in isolation, or anything that needs method-injected scoped services.
Class middleware also gives you a stable place to hang structured logging and a unit-test target. Delegates are best when the body is small enough to read at the registration site. See Middleware for the full convention.
Common causes:
-
An earlier middleware short-circuited. Look for a step that returns without calling
await next(context). Validation, caching, and authorization commonly short-circuit on purpose. - An earlier middleware threw. Wrap the outer edge with an error-boundary middleware to log and rethrow — exceptions otherwise propagate silently past later steps.
-
The class signature does not match the convention. Plumber expects
RequestMiddleware<TReq, TRes> nextas the first constructor parameter andRequestContext<TReq, TRes>as the firstInvokeAsyncparameter.InvokeAsyncmust bepublicand returnTask.
Call AddJsonFile("appsettings.json", optional: true) on the builder explicitly.
v3 configuration is opt-in — nothing is loaded automatically except the command-line args appended at Build() time.
For the conventional set (appsettings.json, appsettings.{ENV}.json, DOTNET_* env vars, all env vars), call AddDefaultConfigurationSources(). User secrets remain opt-in via AddUserSecrets<T>().
Configure all middleware before your first invocation. The first call to InvokeAsync builds the pipeline; later Use calls throw InvalidOperationException.
The recommended pattern is a single Pipeline.Configure method that takes a RequestHandler<TReq, TRes> and returns it after registering every middleware — see the convention used in Building a pipeline.
Pick Unit as TResponse whenever the pipeline produces no meaningful return value: event handlers (SQS, SNS, EventBridge), queue consumers, notification dispatchers, fire-and-forget workers.
Unit is more expressive than object? and keeps every handler typed uniformly as RequestHandler<TRequest, TResponse> — there is no separate void-returning shape.
using var handler = RequestHandlerBuilder
.Create<MessageBatch, Unit>()
.Build()
.Use<ProcessMiddleware>();
await handler.InvokeAsync(batch);Cooperative cancellation. Inside the middleware, check context.CancellationToken periodically — context.ThrowIfCanceled() is the one-line shorthand.
If a step does long-running work that already accepts a CancellationToken (HTTP calls, database queries, file IO), pass context.CancellationToken directly so the underlying operation observes the same signal:
public async Task InvokeAsync(RequestContext<MyReq, MyRes> context, HttpClient http)
{
var response = await http.GetAsync(context.Request.Url, context.CancellationToken);
// ...
await next(context);
}The terminal middleware at the end of the pipeline already throws OperationCanceledException if the token is cancelled before it runs — so explicit ThrowIfCanceled calls inside short middleware are defence-in-depth, not strictly required. They become important in long-running middleware that does work before deferring to next.
InvokeAsync throws different exceptions for the two cases:
-
Handler timeout (configured via
Build(TimeSpan)) →TimeoutException. -
Caller cancellation (the
CancellationTokenpassed toInvokeAsync) →OperationCanceledException.
If both fire, caller cancellation wins — an OperationCanceledException propagates rather than a TimeoutException. Catch them separately to log or convert each case independently.
try
{
await handler.InvokeAsync(request, cts.Token);
}
catch (TimeoutException) { /* handler exceeded its timeout */ }
catch (OperationCanceledException) { /* caller cancelled */ }They propagate out of InvokeAsync by default.
Wrap an error-boundary middleware around the chain if you want to log, convert, or swallow them — see the example in Request lifecycle.
Documents Plumber v3.x · Repository · MIT License · Report an issue
Getting Started
Reference
- Building a pipeline
- Middleware
- Request lifecycle
- Testing
- PlumberApplicationFactory
- Advanced
- FAQ
- Migration
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