Middleware
Cross-cutting logic - auth checks, logging, timing - lives in middleware and stays out of handler methods.
Implement ICommandMiddleware:
public class TimingMiddleware : ICommandMiddleware
{
public async ValueTask InvokeAsync(CommandContext ctx, CommandMiddlewareDelegate next)
{
var sw = Stopwatch.StartNew();
await next(ctx);
Console.Error.WriteLine($"{ctx.CommandName}: {sw.ElapsedMilliseconds}ms");
}
}
Runs for every command:
app.UseMiddleware<TimingMiddleware>();
Apply via attribute on the method:
[MiddlewareAttribute<TimingMiddleware>]
public static Task<int> Deploy(string environment) { … }
ICommandMiddleware receives CommandContext with:
CommandPath- the matched command pathArgs- the raw argumentsExitCode- settable exit codeCancellationToken- the cancellation token for the invocationArguments- the bound option objects the handler receives ([AsParameters]parameters, global and namespace options), each aCommandArgumentwith itsValueand aMemberFlagsmap from member name to flag. Lets middleware validate what was parsed instead of re-parsingArgsReportError(message, flag)- writesError: --flag: messageto stderr and setsExitCodeto 2, like the generated parser. Do not callnextafterwards
Note
Middleware does not run for --help, --version, __completion, __complete, or __schema.
The pipeline is wired in generated code, not a runtime delegate chain. Each middleware call is emitted as a direct invocation in the generated dispatch method. There is no runtime list to build or iterate.
When using Nullean.Argh.Hosting, middleware types are resolved from the DI container. Control lifetimes with the overload:
builder.Services.AddArgh(args, b =>
{
b.UseMiddleware<AuditMiddleware>(ServiceLifetime.Singleton);
});
Tip
Without a host, middleware falls back to new T() when no IServiceProvider is available.