Skip to content

Modules

Modules are the primary composition unit in Repl Toolkit. A module is a self-contained set of commands that can be registered at any point in the command graph — once, or under multiple contexts.

Any method or class that calls Map(...) and Context(...) on an IReplMap is a module. There’s no base class or interface to implement — it’s a convention, not a framework type.

// A simple module as a static method
static void RegisterCrudCommands<T>(IReplMap map, IStore<T> store) where T : IEntity
{
map.Map("list", () => store.All());
map.Map("show {id:int}", (int id) => store.Get(id));
map.Map("delete {id:int}", (int id) =>
{
store.Delete(id);
return Results.Success($"Deleted {id}.");
});
}

Register it anywhere in the graph:

app.Context("contacts", contacts => RegisterCrudCommands(contacts, contactStore));
app.Context("products", products => RegisterCrudCommands(products, productStore));

Both contacts and products now expose list, show, and delete — without copy-pasting routes.

MapModule — registering a class-based module

Section titled “MapModule — registering a class-based module”

MapModule is the primary registration mechanism for class-based modules. There are three forms.

The recommended form. Repl resolves the constructor from the DI container — every service registered in builder.Services is available to the module:

builder.Services.AddScoped<IAuditLog, AuditLog>();
// …later, when building the command graph:
app.MapModule<AuditModule>(); // DI resolves IAuditLog automatically

The module declares its dependencies via a primary constructor (or any constructor):

public class AuditModule(IAuditLog log) : IReplModule
{
public void Map(IReplMap map)
{
map.Map("audit list", static (IAuditLog l) => l.All());
map.Map("audit clear", static (IAuditLog l) =>
{
l.Clear();
return Results.Success("Audit log cleared.");
});
}
}

Pass a pre-constructed instance when you wire dependencies manually or the module has no DI dependencies:

app.MapModule(new AuditModule(myAuditLog));

This also works inside a Context(...) callback, where local variables are already available:

app.Context("admin", admin =>
{
admin.MapModule(new AuditModule(myAuditLog));
admin.MapModule(new MaintenanceModule());
});

Conditional — MapModule(module, predicate)

Section titled “Conditional — MapModule(module, predicate)”

Mount a module only when a runtime condition is met. The predicate is evaluated each time the command graph is traversed:

app.MapModule(
new AdminModule(),
ctx => ctx.Channel != ReplRuntimeChannel.Session
&& ctx.SessionState.TryGet<bool>("auth.admin", out var isAdmin)
&& isAdmin);

For predicates that need DI services, pass a delegate — Repl resolves the parameters from the container at evaluation time:

app.MapModule(
new AdminModule(),
(IAuthService auth, ReplRuntimeChannel channel) =>
channel == ReplRuntimeChannel.Interactive && auth.IsAdmin());

ModulePresenceContext provides:

  • Channel: Cli, Interactive, or Session
  • SessionState: current mutable session state
  • SessionInfo: current read-only session metadata

Dynamic context routes are still configured up front. Use the optional validation delegate to guard entering the scope, then bind the captured route value into each handler:

app.Context(
"project {id:int}",
project =>
{
project.Map("show", static ([FromContext] int id, IProjectStore store) =>
store.Get(id) ?? throw new KeyNotFoundException($"Project {id} not found."));
project.Map("archive", static ([FromContext] int id, IProjectStore store) =>
{
var project = store.Get(id) ?? throw new KeyNotFoundException($"Project {id} not found.");
project.Archive();
store.Save(project);
return Results.Success($"Archived '{project.Name}'.");
});
},
validation: static (int id, IProjectStore store) =>
store.Get(id) is null ? $"Project {id} not found." : string.Empty);

The validator runs when the context is matched. It can return bool, string, IReplResult, or null; a non-empty string is rendered as a validation failure. The command handlers remain regular mapped commands and can use [FromContext] to make context binding explicit.

Use the same validation delegate for access control checks that should block an entire dynamic scope.

Use .AutomationHidden() to hide a command from MCP and other programmatic surfaces while keeping it in the interactive REPL. For MCP-only commands, register them with .AsMcpAppResource() or .AsResource() — these are only surfaced on the MCP channel:

app.Map("debug reset", () => store.Clear())
.AutomationHidden(); // visible in REPL, hidden from MCP agents
app.Map("contacts dashboard", BuildDashboard)
.AsMcpAppResource(); // surfaced as an MCP ui:// resource

Modules can register other modules — compose freely:

public class ContactsModule : IReplModule
{
public void Map(IReplMap map)
{
map.Context("contacts", contacts =>
{
RegisterCrud(contacts);
RegisterAudit(contacts, "contact");
RegisterImportExport(contacts);
});
}
}

There’s no depth limit and no performance penalty — routes are compiled at startup.