Skip to content
Mark Lauter edited this page May 10, 2026 · 2 revisions

FAQ

Short answers to the questions that come up most often.

Comparisons

How does Plumber compare to ASP.NET Core middleware?

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.

Can I use Plumber alongside ASP.NET Core?

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.

How is this different from MediatR?

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.

Do I need DI to use Plumber?

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.

Authoring middleware

When should I use a class middleware vs a delegate?

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.

What should I check if a class middleware isn't running?

Common causes:

  1. 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.
  2. An earlier middleware threw. Wrap the outer edge with an error-boundary middleware to log and rethrow — exceptions otherwise propagate silently past later steps.
  3. The class signature does not match the convention. Plumber expects RequestMiddleware<TReq, TRes> next as the first constructor parameter and RequestContext<TReq, TRes> as the first InvokeAsync parameter. InvokeAsync must be public and return Task.

What do I need to do to load appsettings.json?

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>().

Can I add middleware after the pipeline has been invoked?

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.

When should I use Unit?

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);

Cancellation, timeouts, and errors

How do I cancel a long-running middleware?

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.

How do I distinguish a handler timeout from caller cancellation?

InvokeAsync throws different exceptions for the two cases:

  • Handler timeout (configured via Build(TimeSpan)) → TimeoutException.
  • Caller cancellation (the CancellationToken passed to InvokeAsync) → 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 */ }

What happens to exceptions thrown inside a middleware?

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.

See also

Clone this wiki locally