-
Notifications
You must be signed in to change notification settings - Fork 0
Migration
This page documents major-version migrations for Plumber. Sections are appended as new majors ship; older sections stay for historical reference. The current section covers the v2.x to v3.x jump; future migrations will be appended below it.
v3 reshapes the public API around concrete types and explicit configuration, modernizes the dispatch path (expression-tree compiled), and makes timeout exceptions distinguishable from caller cancellation. The migrations below cover the common cases.
Both IRequestHandlerBuilder<TRequest, TResponse> and IRequestHandler<TRequest, TResponse> are gone in v3. Type your variables and parameters with the concrete classes instead.
// v2
IRequestHandlerBuilder<MyReq, MyRes> builder = RequestHandlerBuilder.Create<MyReq, MyRes>();
IRequestHandler<MyReq, MyRes> handler = builder.Build();// v3
RequestHandlerBuilder<MyReq, MyRes> builder = RequestHandlerBuilder.Create<MyReq, MyRes>();
RequestHandler<MyReq, MyRes> handler = builder.Build();The no-response type was renamed. Unit is a readonly record struct borrowed from F# and Haskell; it lets pipelines with no meaningful response stay typed uniformly as RequestHandler<TRequest, TResponse>.
// v2
RequestHandlerBuilder.Create<SqsEvent, Void>();// v3
RequestHandlerBuilder.Create<SqsEvent, Unit>();v2 implicitly added appsettings.json, environment variables, and user secrets. v3 makes configuration opt-in — the only thing appended automatically is args (via AddCommandLine, last so it always wins).
// v2 — implicit
var builder = RequestHandlerBuilder.Create<TReq, TRes>(args);// v3 — explicit; pick sources individually
var builder = RequestHandlerBuilder.Create<TReq, TRes>(args)
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables();
// or opt back into the conventional set
var builder = RequestHandlerBuilder.Create<TReq, TRes>(args)
.AddDefaultConfigurationSources();AddDefaultConfigurationSources() excludes user secrets — call AddUserSecrets<T>() explicitly with a type from your assembly when you want them.
The builder no longer exposes mutable Services and Configuration properties. Use the Configure* callbacks; they run at Build() time, with the built IConfiguration available where appropriate.
// v2
var builder = RequestHandlerBuilder.Create<TReq, TRes>();
builder.Services.AddSingleton<IMyService, MyService>();
builder.Configuration.AddInMemoryCollection(...);// v3
var builder = RequestHandlerBuilder.Create<TReq, TRes>()
.AddInMemoryCollection(...)
.ConfigureServices((services, configuration) =>
{
services.AddSingleton<IMyService, MyService>();
});v2 let you inject anything into a middleware constructor. v3 still does, but the constructor parameters are resolved from the root provider, and the middleware itself is constructed once at registration time. For scoped or transient services (such as DbContext or HttpClient), use method injection on InvokeAsync — those parameters are resolved from the per-request scope on every invocation.
// v2 — DbContext was captured in the singleton middleware
internal sealed class SaveMiddleware(
RequestMiddleware<TReq, TRes> next,
AppDbContext db)
{
public async Task InvokeAsync(RequestContext<TReq, TRes> context)
{
await db.SaveAsync(context.Request);
await next(context);
}
}// v3 — DbContext is resolved fresh from the per-request scope
internal sealed class SaveMiddleware(RequestMiddleware<TReq, TRes> next)
{
public async Task InvokeAsync(
RequestContext<TReq, TRes> context,
AppDbContext db)
{
await db.SaveAsync(context.Request);
await next(context);
}
}Constructor injection remains the right choice for genuinely-singleton dependencies — ILogger<T>, TimeProvider, an options instance bound from configuration. See Middleware for the full lifetime story.
v2 surfaced both handler timeouts and caller cancellation as OperationCanceledException. v3 throws TimeoutException for handler timeouts and OperationCanceledException for caller cancellation. If both fire, caller cancellation wins. Update any catch clauses that distinguished them by message.
// v2
catch (OperationCanceledException ex)
{
if (ex.Message.Contains("timeout")) { /* ... */ }
}// v3
catch (TimeoutException) { /* handler timeout */ }
catch (OperationCanceledException) { /* caller cancellation */ }Wrap the handler in using. The handler owns the service provider it built — leaking it leaks the provider, any file watchers the configuration registered (for example, AddJsonFile(..., reloadOnChange: true)), and any IDisposable services.
// v2
var handler = builder.Build();
var response = await handler.InvokeAsync(request);// v3
using var handler = builder.Build();
var response = await handler.InvokeAsync(request);The exception is host-mode handlers built via RequestHandler.Create(IServiceProvider) — those reuse an externally-owned provider and leave it untouched on Dispose. The wrapping using only marks the handler itself disposed.
Future major-version migrations will be appended below as new sections; v2 to v3 stays at the top for now since it is the only one.
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