Prefer typed route constraints over raw strings. Typed parameters give you:
- Automatic validation at route matching time
- Better help text and MCP schema generation
- Correct .NET types in handler parameters
// Prefer this
app.Map("user {id:int}", (int id) => ...);
app.Map("open {path:uri}", (Uri path) => ...);
app.Map("since {date:date}", (DateOnly date) => ...);
app.Map("export {file}", (FileInfo file) => ...);
// Over this
app.Map("user {id}", (string id) => int.Parse(id));Available types: int, long, bool, email, uri, url, date, datetime, timespan, guid, and implicit FileInfo/DirectoryInfo. See Route System.
For temporal queries, use ReplDateRange for human-friendly range syntax:
app.Map("logs {range}", (ReplDateRange range) => ...);
// Accepts: "today", "last-7d", "2024-01-01..2024-03-01"Use static lambdas to avoid captures. Inject services through handler parameters instead of closures. This enables expression tree compilation and keeps handlers testable.
// Prefer this — static lambda, services injected
app.Map("list", static (IContactStore store) => store.All());
app.Map("add {name} {email:email}",
static (string name, string email, IContactStore store) =>
{
store.Add(new Contact(name, email));
return Results.Success($"Added {name}.");
});
// Avoid this — closure captures
var store = new ContactStore();
app.Map("list", () => store.All()); // captures 'store'Use ReplApp.Create(services => ...) for service registration. Prefer constructor injection in modules and implicit injection in handlers.
var app = ReplApp.Create(services =>
{
services.AddSingleton<IContactStore, InMemoryContactStore>();
services.AddSingleton(typeof(IEntityStore<>), typeof(InMemoryEntityStore<>)); // open-generic
});Use [FromServices] only when disambiguation is needed (e.g., same type available from both context and DI). Otherwise, implicit injection works:
app.Map("show", static (IContactStore store) => store.All()); // implicit
app.Map("show", static ([FromServices] IContactStore store) => store.All()); // explicit (same result)Use [FromContext] to access route values from parent contexts:
app.Context("project {id:int}", project =>
{
project.Map("status", static ([FromContext] int id, IProjectService svc) =>
svc.GetStatus(id));
});When configuration applies to all commands (tenant, environment, verbosity), register global options and access them via IGlobalOptionsAccessor:
app.Options(o =>
{
o.Parsing.AddGlobalOption<string>("tenant");
o.Parsing.AddGlobalOption<bool>("verbose");
});For many global options, prefer UseGlobalOptions<T>() with a typed class:
app.UseGlobalOptions<MyGlobalOptions>();Use IGlobalOptionsAccessor in DI factories for services that depend on global option values:
services.AddSingleton<ITenantClient>(sp =>
{
var globals = sp.GetRequiredService<IGlobalOptionsAccessor>();
return new TenantClient(globals.GetValue<string>("tenant", "default")!);
});Note: DI singleton factories are resolved lazily, so the values are available after global option parsing completes. However, singleton factories capture values once — in interactive mode, global options can change between commands. If your service needs to see updated values per command, inject IGlobalOptionsAccessor directly and read values at call time instead of capturing them in a factory. See Commands — Accessing global options.
When a command has many options, group them into a class instead of listing them all as handler parameters. This keeps handlers clean and makes option sets reusable across commands.
[ReplOptionsGroup]
public class PagingOptions
{
[ReplOption(Aliases = ["-n"])]
public int Limit { get; set; } = 20;
[ReplOption]
public int Offset { get; set; }
}
[ReplOptionsGroup]
public class FilterOptions
{
[ReplOption(Aliases = ["-q"])]
public string? Query { get; set; }
[ReplOption]
public bool IncludeArchived { get; set; }
}Inject them directly into handlers — the framework binds each property from parsed options:
app.Map("list", static (IContactStore store, PagingOptions paging, FilterOptions filter) =>
store.Query(filter.Query, filter.IncludeArchived, paging.Offset, paging.Limit));Options groups compose well — you can combine multiple groups in the same handler, and reuse them across different commands.
Use IReplModule for reusable command groups. Modules keep command definitions cohesive and composable.
public sealed class ContactModule : IReplModule
{
public void Map(IReplMap map)
{
map.Map("list", static (IContactStore store) => store.All())
.WithDescription("List all contacts")
.ReadOnly();
map.Map("add {name} {email:email}", static (string name, string email, IContactStore store) =>
{ store.Add(new(name, email)); return Results.Success($"Added {name}."); })
.WithDescription("Add a contact");
}
}Mount modules in contexts, reuse across scopes:
app.Context("contacts", contacts => contacts.MapModule<ContactModule>());Control command visibility per runtime channel:
app.MapModule(
new AdminModule(),
static context => context.Channel is ReplRuntimeChannel.Cli); // CLI-only
app.MapModule(
new DiagnosticsModule(),
static (FeatureFlags flags) => flags.DiagnosticsEnabled); // feature-gatedCall app.InvalidateRouting() if presence conditions can change at runtime.
Always validate dynamic context segments to prevent invalid scopes:
app.Context("{name}", scope =>
{
scope.Map("show", static (string name, IContactStore store) => store.Get(name));
scope.Map("remove", static (string name, IContactStore store) =>
{
store.Remove(name);
return Results.NavigateUp($"Removed '{name}'.");
});
},
validation: static (string name, IContactStore store) => store.Get(name) is not null);Validation delegates support DI injection for service-backed checks.
Behavioral annotations improve AI agent discoverability and safety:
app.Map("status", static () => GetStatus())
.WithDescription("Get system status")
.ReadOnly()
.AsResource(); // exposed as MCP resource
app.Map("deploy {env}", static (string env) => Deploy(env))
.WithDescription("Deploy to environment")
.WithDetails("Triggers a full deployment pipeline to the target environment.")
.Destructive()
.OpenWorld();
app.Map("troubleshoot {symptom}", static (string symptom) =>
$"Investigate: '{symptom}'. Use status and logs tools first.")
.WithDescription("Diagnostic guidance")
.AsPrompt(); // exposed as MCP prompt
app.Map("clear", static async (IReplInteractionChannel ch, CancellationToken ct) =>
{ await ch.ClearScreenAsync(ct); })
.AutomationHidden(); // not exposed to agentsFor MCP Apps, mark the HTML-producing command as an app resource:
app.Map("contacts dashboard", static (IContactStore contacts) => BuildHtml(contacts))
.WithDescription("Open the contacts dashboard")
.AsMcpAppResource();This lets capable hosts render the UI while keeping raw HTML out of the model-facing transcript. The handler is still a normal Repl mapping, so it can use DI, cancellation tokens, and the usual command pipeline.
Declare answer slots for interactive prompts so agents and --answer: flags can provide values:
app.Map("delete {id:int}", handler)
.Destructive()
.WithAnswer("confirm", "bool", "Confirm the deletion");Use ReplTestHost for integration tests with typed results:
await using var host = ReplTestHost.Create(() => BuildApp());
// One-shot command
var result = await host.RunCommandAsync("contacts list --json");
result.ExitCode.Should().Be(0);
var contacts = result.GetResult<Contact[]>();
// Multi-session
await using var session = await host.OpenSessionAsync();
await session.RunCommandAsync("contacts add Alice alice@test.com");
var list = await session.RunCommandAsync("contacts list");
list.OutputText.Should().Contain("Alice");Register ambient commands for common actions:
app.Options(o => o.AmbientCommands.MapAmbient(
"clear",
static async (IReplInteractionChannel ch, CancellationToken ct) =>
await ch.ClearScreenAsync(ct),
"Clear the screen"));Seed history for discoverability:
services.AddSingleton<IHistoryProvider>(new InMemoryHistoryProvider([
"contacts list", "contacts add", "status"
]));Use Spectre.Console for rich UI — prompts auto-upgrade transparently:
app.UseSpectreConsole(); // existing IReplInteractionChannel calls render as Spectre promptsSee also: Modules | Route System | MCP Overview | Testing | Configuration