-
Notifications
You must be signed in to change notification settings - Fork 0
Command Runners
github-actions[bot] edited this page Feb 17, 2026
·
1 revision
Command runners are an optional layer that dispatches commands using DI. They are useful when you want to keep the state machine pure but still have a structured way to execute commands.
- Declare either
ICommandRunner<TCommand>orIAsyncCommandRunner<TCommand>for each command type. - Call
serviceCollection.AddCommandRunners<TCommand>()to automatically discover and register them. - Resolve
ICommandDispatcher<TCommand>orIAsyncCommandDispatcher<TCommand>to dispatch commands.
- Keeps command handling modular and testable.
- Uses DI for dependency management.
- Supports sync-only or async-capable command execution.
public abstract record UserCommand
{
public sealed record SendWelcomeEmail(Guid UserId) : UserCommand;
}
public sealed class SendWelcomeEmailRunner : ICommandRunner<UserCommand.SendWelcomeEmail>
{
public void Run(UserCommand.SendWelcomeEmail command)
{
// send email
}
}
services.AddCommandRunners<UserCommand>();
var provider = services.BuildServiceProvider()
.GetRequiredService<ICommandDispatcher<UserCommand>>();
provider.Run(new UserCommand.SendWelcomeEmail(Guid.NewGuid()));If any runner implements IAsyncCommandRunner<TCommand>, resolve IAsyncCommandDispatcher<TCommand> instead.
public abstract record BillingCommand
{
public sealed record ChargeCard(decimal Amount) : BillingCommand;
}
public sealed class ChargeCardRunner : IAsyncCommandRunner<BillingCommand.ChargeCard>
{
public Task RunAsync(BillingCommand.ChargeCard command)
{
return Task.CompletedTask;
}
}
services.AddCommandRunners<BillingCommand>();
var provider = services.BuildServiceProvider()
.GetRequiredService<IAsyncCommandDispatcher<BillingCommand>>();
await provider.RunAsync(new BillingCommand.ChargeCard(42m));services.AddCommandRunners<UserCommand>(new CommandRunnerOptions
{
MissingBehavior = CommandRunnerMissingBehavior.NoOp,
Lifetime = ServiceLifetime.Scoped,
AutoRegisterRunners = false
});
services.AddScoped<SendWelcomeEmailRunner>();-
MissingBehavior: defaults to throw if a command has no runner. -
Lifetime: controls runner and provider lifetime. -
AutoRegisterRunners: set tofalseto register runners manually.
This example shows a state machine producing commands and a command runner provider executing them.
public abstract record OrderTrigger
{
public sealed record Submit : OrderTrigger;
}
public enum OrderState
{
Draft,
Submitted
}
public sealed record OrderData(Guid OrderId, decimal Total);
public abstract record OrderCommand
{
public sealed record Charge(decimal Amount) : OrderCommand;
public sealed record SendReceipt(Guid OrderId) : OrderCommand;
}
public sealed class ChargeRunner : IAsyncCommandRunner<OrderCommand.Charge>
{
public Task RunAsync(OrderCommand.Charge command)
{
return Task.CompletedTask;
}
}
public sealed class SendReceiptRunner : ICommandRunner<OrderCommand.SendReceipt>
{
public void Run(OrderCommand.SendReceipt command)
{
}
}
var machine = StateMachine<OrderState, OrderTrigger, OrderData, OrderCommand>.Create()
.StartWith(OrderState.Draft)
.For(OrderState.Draft)
.On<OrderTrigger.Submit>()
.Execute(data => new OrderCommand.Charge(data.Total))
.Execute(data => new OrderCommand.SendReceipt(data.OrderId))
.TransitionTo(OrderState.Submitted)
.Build();
var services = new ServiceCollection()
.AddCommandRunners<OrderCommand>()
.AddTransient<ChargeRunner>()
.AddTransient<SendReceiptRunner>();
var provider = services.BuildServiceProvider()
.GetRequiredService<IAsyncCommandDispatcher<OrderCommand>>();
var (nextState, nextData, commands) = machine.Fire(
new OrderTrigger.Submit(),
OrderState.Draft,
new OrderData(Guid.NewGuid(), 99m));
await provider.RunAsync(commands);