Pipelines & Integration
The execution pipeline
Section titled “The execution pipeline”Every command invocation goes through a 12-stage pipeline. Understanding it lets you hook in at the right stage and reason about where errors come from.
1. Global Option Parsing — --help, --output:format, --answer:*, custom globals 2. Prefix Resolution — abbreviated command names 3. Pre-Execution Handling — shell completion, help rendering, ambient commands 4. Route Resolution — matches tokens to registered routes 5. Banner Rendering — suppressed in CI/non-interactive 6. Command Option Parsing — --name value, @responsefile, --no-* 7. Argument Binding — CancellationToken → attributes → groups → route → DI → positional → defaults 8. Context Validation — optional validation delegates on contexts 9. Middleware & Handler — your Use() chain + handler invocation10. Result Processing — unwrap IExitResult, EnterInteractiveResult11. Output Transformation — format selection and rendering12. Exit Code — 0 success, 1 all other results, n via Results.Exit(n)Middleware
Section titled “Middleware”Register middleware with app.Use(...). The delegate receives a ReplExecutionContext (with Services, Items, and CancellationToken) and a ReplNext delegate to invoke the next stage:
app.Use(async (context, next) =>{ var logger = context.Services.GetRequiredService<ILogger<Program>>(); var sw = Stopwatch.StartNew(); await next(); logger.LogInformation("Command completed in {Elapsed}ms", sw.ElapsedMilliseconds);});Middleware runs between argument binding (step 7) and your handler (step 9). Chain multiple Use(...) calls — they execute in registration order.
Authentication middleware example
Section titled “Authentication middleware example”Resolve your auth service from context.Services. If the check fails, short-circuit by not calling next():
app.Use(async (context, next) =>{ var auth = context.Services.GetRequiredService<IAuthService>(); if (!auth.IsAuthenticated) throw new UnauthorizedAccessException("Not authenticated. Run 'auth login' first.");
await next();});Logging middleware
Section titled “Logging middleware”app.Use(async (context, next) =>{ var logger = context.Services.GetRequiredService<ILogger<Program>>(); logger.LogDebug("Executing command"); try { await next(); } catch (Exception ex) { logger.LogError(ex, "Command threw"); throw; }});Scripting and automation
Section titled “Scripting and automation”One-shot CLI invocation
Section titled “One-shot CLI invocation”Every command works as a one-liner. Combine with shell pipes for scripting:
# Export contacts and pipe to jqmyapp contacts list --json | jq '.[] | select(.email | endswith("@example.com"))'
# Import from a filemyapp contacts import contacts.csv
# Chain with standard Unix toolsmyapp orders list --json | jq '.[].id' | xargs -I{} myapp orders {} archiveResponse files
Section titled “Response files”Group complex invocations in a file:
myapp @deploy.rspdeploy.rsp:
deploy--environmentproduction--regionus-east-1--confirmResponse files are space-transparent (one token per line) and non-recursive.
Pre-filled prompts for CI
Section titled “Pre-filled prompts for CI”Use --answer:* to supply interactive prompts from environment variables or scripts:
# In CI — no user presentmyapp release publish \ --answer:confirm=yes \ --answer:version=$VERSIONMachine-readable help in scripts
Section titled “Machine-readable help in scripts”# Discover available commandsmyapp --help --json | jq '.commands[].name'
# Get parameter schema for a commandmyapp contacts add --help --json | jq '.parameters'Integrating with ASP.NET Core
Section titled “Integrating with ASP.NET Core”Repl integrates natively with WebApplicationBuilder. The command graph and HTTP endpoints share the same DI container:
var builder = WebApplication.CreateBuilder(args);builder.Services.AddSingleton<IContactStore, ContactStore>();
var webApp = builder.Build();webApp.UseWebSockets();webApp.MapReplWebSocket("/repl"); // REPL sessionswebApp.MapGet("/api/contacts", (IContactStore store) => store.All()); // REST API
// Repl command graph — same services; register all routes before RunAsyncvar repl = webApp.Services.GetRequiredService<IReplApp>();repl.Map("contacts list", static (IContactStore store) => store.All());
await webApp.RunAsync();The REPL command contacts list and the REST endpoint /api/contacts share the same IContactStore instance.
Hosting background services
Section titled “Hosting background services”A Repl app can host IHostedService / BackgroundService implementations alongside its command surface. Register them as you would in any .NET host:
var app = ReplApp.Create(services =>{ services.AddSingleton<IContactStore, ContactStore>(); services.AddHostedService<CacheWarmupService>(); // runs on startup services.AddHostedService<SessionCleanupService>(); // runs in background});Enable hosted service lifecycle so Repl starts and stops them:
return await app.RunAsync(args, options: new ReplRunOptions{ HostedServiceLifecycle = HostedServiceLifecycleMode.Head,});This is most useful for REPL and MCP server scenarios where the process runs indefinitely — background services have a long lifetime alongside the interactive session. In CLI mode the process exits after the command completes, so hosted services run only for the duration of that invocation.
public class SessionCleanupService(IContactStore store) : BackgroundService{ protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { await Task.Delay(TimeSpan.FromHours(1), ct); store.PurgeExpiredSessions(); } }}Signal handling and graceful shutdown
Section titled “Signal handling and graceful shutdown”Repl handles SIGINT (Ctrl+C) and propagates a CancellationToken to all handlers. Long-running commands should honor it:
app.Map("process {file}", async (string file, CancellationToken ct) =>{ await foreach (var record in ReadRecords(file, ct)) { ct.ThrowIfCancellationRequested(); await ProcessRecord(record, ct); } return Results.Success("Done.");});In hosted sessions, graceful shutdown sends a disconnect signal to all active sessions before the server exits.
Error handling
Section titled “Error handling”Errors flow through the pipeline as typed Results:
app.Map("pay {amount:double}", async (double amount, IPaymentService payments) =>{ if (amount <= 0) return Results.Validation("Amount must be positive.");
var result = await payments.ChargeAsync(amount);
return result.IsSuccess ? Results.Success($"Paid {amount:C}.") : Results.Error("payment_failed", $"Payment failed: {result.Message}");});Unhandled exceptions are caught at stage 9, rendered to stderr with a stack trace in debug mode, and produce exit code 5.
Custom exit codes
Section titled “Custom exit codes”return Results.Exit(42); // explicit exit code