Skip to content

CommandR: Cheat Sheet

Quick reference for commands, handlers, and the CommandR pipeline.

Defining Commands

Basic command with result:

cs
public record CreateOrderCommand(long UserId, List<OrderItem> Items) : ICommand<Order>
{
    public CreateOrderCommand() : this(0, new()) { } // Parameterless constructor for serialization
}

Command without result:

cs
public record DeleteOrderCommand(long OrderId) : ICommand<Unit>
{
    public DeleteOrderCommand() : this(0) { }
}

Command Interfaces

InterfacePurpose
ICommandBase marker interface
ICommand<TResult>Command with typed result
IBackendCommandServer-only execution
IOutermostCommandForces top-level execution
IDelegatingCommandOrchestrates other commands
IPreparedCommandRequires Prepare() before execution
ISystemCommandFramework-triggered commands
ILocalCommandSelf-executing commands
IEventCommandMulti-handler events
ISessionCommandFusion session-bound commands

Registering CommandR

cs
var services = new ServiceCollection();

// Add CommandR
var commander = services.AddCommander();

// Add handler classes
commander.AddHandlers<OrderHandlers>();

// Add command services (creates proxy)
commander.AddService<OrderService>();

Handler Patterns

Interface-Based Handler

cs
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand>
{
    public async Task OnCommand(
        CreateOrderCommand command,
        CommandContext context,
        CancellationToken ct)
    {
        // Handle command
    }
}

// Registration
services.AddScoped<CreateOrderHandler>();
commander.AddHandlers<CreateOrderHandler>();

Convention-Based Handler

cs
public class OrderHandlers
{
    [CommandHandler]
    public async Task<Order> CreateOrder(
        CreateOrderCommand command,
        IOrderRepository repo, // Resolved from DI
        CancellationToken ct)
    {
        return await repo.Create(command, ct);
    }
}

// Registration
services.AddScoped<OrderHandlers>();
commander.AddHandlers<OrderHandlers>();

Command Service

cs
public class OrderService : ICommandService
{
    [CommandHandler]
    public virtual async Task<Order> CreateOrder( // Must be virtual
        CreateOrderCommand command,
        CancellationToken ct)
    {
        // ...
    }
}

// Registration (creates proxy automatically)
commander.AddService<OrderService>();

// Always use Commander - direct calls throw!
await commander.Call(new CreateOrderCommand(...), ct);
// await orderService.CreateOrder(...); // Throws NotSupportedException!

Filter Handler

cs
[CommandHandler(Priority = 100, IsFilter = true)]
public async Task LoggingFilter(ICommand command, CancellationToken ct)
{
    var context = CommandContext.GetCurrent();
    Console.WriteLine($"Before: {command.GetType().Name}");
    try {
        await context.InvokeRemainingHandlers(ct);
    }
    finally {
        Console.WriteLine($"After: {command.GetType().Name}");
    }
}

Executing Commands

cs
var commander = services.Commander();

// Call - returns result, throws on error
var order = await commander.Call(new CreateOrderCommand(...), ct);

// Run - returns context, never throws
var context = await commander.Run(new CreateOrderCommand(...), ct);
if (context.UntypedResult is { Error: { } error })
    Console.WriteLine($"Error: {error}");

// Start - fire and forget
var context = commander.Start(new CreateOrderCommand(...));

CommandContext

cs
[CommandHandler]
public async Task<Order> CreateOrder(CreateOrderCommand command, CancellationToken ct)
{
    var context = CommandContext.GetCurrent();

    // Access services
    var db = context.Services.GetRequiredService<AppDbContext>();

    // Store data for other handlers
    context.Items["Key"] = value;

    // Access outer context (for nested commands)
    var root = context.OutermostContext;

    // Share data across nested calls
    root.Items["SharedKey"] = sharedValue;

    // Get commander
    var commander = context.Commander;
}

Handler Priority

Higher priority = runs first. Default is 0.

RangePurpose
> 100,000Infrastructure (validation, tracing)
10,000 - 100,000Cross-cutting concerns
1,000 - 10,000Database/transaction
0 - 1,000Business logic (default: 0)
< 0Post-processing

Built-in Handlers (Execution Order)

HandlerPriorityAssembly
PreparedCommandHandler1,000,000,000CommandR
CommandTracer998,000,000CommandR
LocalCommandRunner900,000,000CommandR
RpcCommandHandler800,000,000CommandR
OperationReprocessor100,000Fusion
NestedOperationLogger11,000Fusion
InMemoryOperationScopeProvider10,000Fusion
DbOperationScopeProvider1,000Fusion.EF
Your handlers0-
InvalidatingCommandCompletionHandler100Fusion
CompletionTerminator-1,000,000,000Fusion

Local Commands

cs
// Using LocalCommand factory
var cmd = LocalCommand.New(() => Console.WriteLine("Hello"));
var cmd = LocalCommand.New(async ct => await DoWorkAsync(ct));
var cmd = LocalCommand.New<int>(() => 42);

await commander.Call(cmd);

Prepared Commands

cs
public record CreateOrderCommand(...) : IPreparedCommand, ICommand<Order>
{
    public Task Prepare(CommandContext context, CancellationToken ct)
    {
        if (Items.Count == 0)
            throw new ArgumentException("Order must have items");
        return Task.CompletedTask;
    }
}

With Operations Framework

cs
[CommandHandler]
public virtual async Task<Order> CreateOrder(
    CreateOrderCommand command, CancellationToken ct)
{
    // Invalidation block (runs on all hosts)
    if (Invalidation.IsActive) {
        _ = GetOrders(command.UserId, default);
        return default!;
    }

    // Main logic (runs on originating host only)
    await using var db = await DbHub.CreateOperationDbContext(ct);
    var order = new Order { ... };
    db.Orders.Add(order);
    await db.SaveChangesAsync(ct);
    return order;
}

See Part 5: Operations Framework for details.