Skip to content

Built-in Command Handlers ​

CommandR and Fusion include several built-in command handlers that form the execution pipeline. Understanding these handlers helps you design your own handlers and debug issues.

Handler Execution Order ​

Handlers execute in descending priority order (highest priority runs first). Filter handlers wrap lower-priority handlers, creating a middleware-like pipeline.

CommandR Handlers ​

These handlers are registered automatically when you call AddCommander().

Assembly: ActualLab.CommandR

HandlerPriorityTypeCommand Type
PreparedCommandHandler1,000,000,000FilterIPreparedCommand
CommandTracer998,000,000FilterICommand
LocalCommandRunner900,000,000FinalILocalCommand
RpcCommandHandler800,000,000FilterICommand

PreparedCommandHandler ​

Priority: 1,000,000,000 (runs first)

Handles commands implementing IPreparedCommand by calling their Prepare method before invoking remaining handlers.

cs
// Registration (automatic in AddCommander)
services.AddSingleton(_ => new PreparedCommandHandler());
commander.AddHandlers<PreparedCommandHandler>();

CommandTracer ​

Priority: 998,000,000

Provides diagnostic tracing and activity tracking for command execution. Creates OpenTelemetry activities for observability and logs errors.

cs
// Registration (automatic in AddCommander)
services.AddSingleton(_ => CommandTracer.Options.Default);
services.AddSingleton(c => new CommandTracer(c.GetRequiredService<CommandTracer.Options>(), c));
commander.AddHandlers<CommandTracer>();

LocalCommandRunner ​

Priority: 900,000,000

Executes commands implementing ILocalCommand by calling their Run method. Unlike filter handlers, LocalCommandRunner doesn't call InvokeRemainingHandlers — it fully handles ILocalCommand by executing its Run() method and returns.

cs
// Registration (automatic in AddCommander)
services.AddSingleton(_ => new LocalCommandRunner());
commander.AddHandlers<LocalCommandRunner>();

RpcCommandHandler ​

Priority: 800,000,000

Routes commands to RPC services when appropriate. Handles distributed command execution and automatic rerouting to the correct server.

cs
// Registration (automatic in AddCommander)
services.AddSingleton(c => new RpcCommandHandler(c));
commander.AddHandlers<RpcCommandHandler>();

Fusion Operations Framework Handlers ​

These handlers are registered when you call AddFusion(). They implement multi-host invalidation and operation logging.

Assembly: ActualLab.Fusion

HandlerPriorityTypeCommand Type
InvalidationGuard999,999,000FilterICommand
OperationReprocessor100,000FilterICommand
TransientOperationScopeProvider10,000FilterICommand
DeferredInvalidationScopeProvider9,000FilterICommand
FusionOperationCompletionHandler0HandlerOperationCompletion
CompletionTerminator-1,000,000,000FinalICompletion

InvalidationGuard ​

Priority: 999,999,000

Throws if a command is started while an invalidation pass is active.

Deferred invalidation re-invokes the compute methods a handler declared – it never replays the command handler – so a command running inside an invalidation pass is a bug. Left alone it would be a quiet one: every operation scope provider down the chain opts out while Invalidation.IsActive, so such a command would mutate without an operation, without events and without invalidations.

Only a nested command can reach the guard: Commander suppresses the ExecutionContext flow of an outermost command and runs its pipeline on a fresh one, so an outermost command never sees its caller's ComputeContext – and therefore runs with its Operations Framework intact.

cs
// Registration (automatic in AddFusion)
services.AddSingleton(_ => new InvalidationGuard());
commander.AddHandlers<InvalidationGuard>();

OperationReprocessor ​

Priority: 100,000

Retries failed commands with transient errors using configurable retry policies.

cs
// Registration (optional, via AddOperationReprocessor)
fusion.AddOperationReprocessor();

See Part 5: Operations Framework for details.

TransientOperationScopeProvider ​

Priority: 10,000

Provides in-memory operation scopes for commands that don't use database-backed operation scopes. Also triggers operation completion notifications.

cs
// Registration (automatic in AddFusion)
services.AddSingleton(c => new TransientOperationScopeProvider(c));
commander.AddHandlers<TransientOperationScopeProvider>();

See Part 5: Operations Framework for details.

DeferredInvalidationScopeProvider ​

Priority: 9,000

Activates the DeferredInvalidationContext that TransientOperationScopeProvider created for the command, and closes it when the handler returns. Invalidation.Defer(...) works from a handler's first statement because of this scope, and the mode of its first block becomes the operation's.

It sits below every operation scope provider, including DbOperationScopeProvider (9,000 < 9,900), so a scope is already open by the time anything can be deferred – Replicated and Distributed need one to carry their calls to other hosts. Closing the context before those providers commit is also what lets the commit read a block list that's final.

See Part 5: Operations Framework for details.

FusionOperationCompletionHandler ​

Priority: 0 (handler for OperationCompletion)

Applies the invalidation calls a handler recorded – locally right after the commit, and on every other host once the operation log delivers the operation. It also handles OperationCompletion, which is how a Distributed invalidation is recovered from its event row.

cs
// Registration (automatic in AddFusion) - replaces CommandR's OperationCompletionHandler
commander.AddOperationCompletionHandler(c => new FusionOperationCompletionHandler(c));

See Invalidation Modes for details.

CompletionTerminator ​

Priority: -1,000,000,000 (runs last)

Terminal handler for completion commands. Ensures the completion pipeline has a final handler.

cs
// Registration (automatic in AddFusion)
services.AddSingleton(_ => new CompletionTerminator());
commander.AddHandlers<CompletionTerminator>();

Entity Framework Handlers ​

These handlers are registered when you call AddOperations() on a DbContextBuilder.

Assembly: ActualLab.Fusion.EntityFramework

HandlerPriorityTypeCommand Type
DbOperationScopeProvider9,900FilterICommand

DbOperationScopeProvider ​

Priority: 9,900

Provides database operation scopes for database-backed operations. Manages transactions and ensures operations are logged to the database.

cs
// Registration (via AddOperations on DbContextBuilder)
services.AddDbContextServices<AppDbContext>(db => {
    db.AddOperations(operations => {
        // DbOperationScopeProvider is registered here
    });
});

See Part 5: Operations Framework for details.

Complete Pipeline Visualization ​

When all handlers are registered, the pipeline looks like this (in execution order):

Command Received
    │
    ▼
┌──────────────────────────────────────────┐
│ PreparedCommandHandler (1,000,000,000)   │ ← Calls IPreparedCommand.Prepare()
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ InvalidationGuard (999,999,000)          │ ← Rejects commands during invalidation
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ CommandTracer (998,000,000)              │ ← Creates activity, logs errors
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ LocalCommandRunner (900,000,000)         │ ← Runs ILocalCommand.Run() if applicable
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ RpcCommandHandler (800,000,000)          │ ← Routes to RPC if needed
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ OperationReprocessor (100,000)           │ ← Retries on transient errors
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ TransientOperationScopeProvider (10,000) │ ← Provides operation scope
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ DbOperationScopeProvider (9,900)         │ ← Provides DB operation scope
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ DeferredInvalidationScopeProvider (9,000)│ ← Opens the invalidation scope
└──────────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────────┐
│ Your Handlers (default priority: 0)      │ ← Your command handlers
└──────────────────────────────────────────┘

For completion commands (ICompletion<T>):

Completion Command
    │
    ▼
┌──────────────────────────────────────────┐
│ CompletionTerminator (-1,000,000,000)    │ ← Terminal handler
└──────────────────────────────────────────┘

CompletionTerminator is the terminal handler of every completion, so a handler of your own for ICompletion<T> has to be a filter – a second non-filter handler for the same completion is an error.

Adding Custom Handlers ​

Filter Handler ​

A filter handler wraps subsequent handlers (like middleware):

cs
public class LoggingHandler : ICommandHandler<ICommand>
{
    [CommandHandler(Priority = 500_000, IsFilter = true)]
    public async Task OnCommand(ICommand command, CommandContext context, CancellationToken ct)
    {
        Console.WriteLine($"Before: {command.GetType().Name}");
        try {
            await context.InvokeRemainingHandlers(ct);
        }
        finally {
            Console.WriteLine($"After: {command.GetType().Name}");
        }
    }
}

// Registration:
// services.AddSingleton<LoggingHandler>();
// commander.AddHandlers<LoggingHandler>();

Final Handler ​

A final handler doesn't call InvokeRemainingHandlers:

cs
public class MyCommandHandler : ICommandHandler<MyCommand>
{
    public async Task OnCommand(MyCommand command, CommandContext context, CancellationToken ct)
    {
        // Handle the command - don't call InvokeRemainingHandlers
        await DoWork(command, ct);
    }

    private Task DoWork(MyCommand command, CancellationToken ct) => Task.CompletedTask;
}

Priority Guidelines ​

When choosing priorities for custom handlers:

RangePurpose
> 100,000Infrastructure handlers (validation, tracing, RPC routing)
10,000 - 100,000Cross-cutting concerns (logging, caching, retry logic)
1,000 - 10,000Database/transaction management
0 - 1,000Business logic handlers (default: 0)
< 0Post-processing, cleanup handlers

Your custom handlers typically use the default priority (0) unless they need to run before/after specific built-in handlers.