-
Notifications
You must be signed in to change notification settings - Fork 16
Command Module
The Command Module provides the infrastructure for implementing the "Command" side of Command Query Separation (CQS). Commands are messages that represent an intention to change the system's state.
- Intent to Change State: Commands encapsulate all the information needed to perform an action, such as creating, updating, or deleting data.
- Single Handler: Each command must be handled by exactly one handler. LiteBus will throw an exception if zero or multiple handlers are found for a command.
-
Naming Convention: Commands should be named with imperative verbs in the present tense (e.g.,
CreateProductCommand,UpdateUserAddress).
LiteBus provides two interfaces for defining commands.
Use ICommand for operations that do not need to return a value to the caller. These are "fire-and-forget" style commands where the caller only needs to know if the operation succeeded or failed (via an exception).
/// <summary>
/// A command to update the stock level of a specific product.
/// </summary>
public sealed class UpdateStockLevelCommand : ICommand
{
public required Guid ProductId { get; init; }
public required int NewQuantity { get; init; }
}Use ICommand<TCommandResult> for commands that must return a value after execution, such as the ID of a newly created entity.
/// <summary>
/// A command to create a new product that returns the new product's DTO.
/// </summary>
public sealed class CreateProductCommand : ICommand<ProductDto>
{
public required string Name { get; init; }
public required decimal Price { get; init; }
}Command handlers contain the business logic to process a command.
-
ICommandHandler<TCommand>: For commands implementingICommand. -
ICommandHandler<TCommand, TCommandResult>: For commands implementingICommand<TCommandResult>.
// Handler for a command without a result
public sealed class UpdateStockLevelCommandHandler : ICommandHandler<UpdateStockLevelCommand>
{
public async Task HandleAsync(UpdateStockLevelCommand command, CancellationToken cancellationToken = default)
{
// Business logic to update stock level...
}
}
// Handler for a command with a result
public sealed class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, ProductDto>
{
public async Task<ProductDto> HandleAsync(CreateProductCommand command, CancellationToken cancellationToken = default)
{
// Business logic to create the product...
var newProduct = new ProductDto { Id = Guid.NewGuid(), Name = command.Name };
return newProduct;
}
}The ICommandMediator is used to send commands into the pipeline for processing.
public interface ICommandMediator
{
Task SendAsync(ICommand command, CommandMediationSettings? settings = null, CancellationToken cancellationToken = default);
Task<TCommandResult> SendAsync<TCommandResult>(ICommand<TCommandResult> command, CommandMediationSettings? settings = null, CancellationToken cancellationToken = default);
}// In a controller or service
public class ProductsController : ControllerBase
{
private readonly ICommandMediator _commandMediator;
public ProductsController(ICommandMediator commandMediator)
{
_commandMediator = commandMediator;
}
[HttpPost]
public async Task<ActionResult<ProductDto>> Create(CreateProductCommand command)
{
// Send a command that returns a result
var result = await _commandMediator.SendAsync(command);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id}/stock")]
public async Task<IActionResult> UpdateStock(Guid id, UpdateStockLevelCommand command)
{
// Send a command that does not return a result
await _commandMediator.SendAsync(command);
return NoContent();
}
}The command inbox stores ICommand instances for deferred, at-least-once execution. It is an explicit API, not a command mediator mode.
-
Immediate execution:
ICommandMediator.SendAsyncalways executes the command in the current process. -
Deferred execution:
ICommandScheduler.ScheduleAsyncstores the command and returns aCommandReceipt<TCommand>. -
Processing:
ICommandInboxProcessor.ProcessPendingAsyncleases due commands and executes them through the normal command pipeline.
Define a command without a handler result and register it as an inbox contract.
public sealed record ProcessPaymentCommand(Guid OrderId, decimal Amount) : ICommand;builder.Services.AddLiteBus(liteBus =>
{
liteBus.AddCommandModule(commands =>
{
commands.Register<ProcessPaymentCommand>();
commands.Register<ProcessPaymentCommandHandler>();
});
liteBus.AddCommandInboxModule(inbox =>
{
inbox.Contracts.Register<ProcessPaymentCommand>(
"payments.commands.process-payment",
version: 1);
});
});Schedule the command when the caller should receive acceptance rather than wait for execution.
var receipt = await commandScheduler.ScheduleAsync(
new ProcessPaymentCommand(orderId, amount),
new CommandScheduleOptions
{
IdempotencyKey = $"payment:{orderId}"
},
cancellationToken);-
ScheduleAsyncstores the command envelope throughICommandInboxWriter. -
ProcessPendingAsyncleases throughICommandInboxLeaseStoreand records results throughICommandInboxStateStore. -
ICommand<TResult>is not accepted by the scheduler. Return acceptance data from the API layer, then query later state. - Closed generic commands are supported when each closed type is registered with a stable contract. Open generic contracts are rejected.
To use the inbox, you must:
- Register command handlers in the command module.
- Register command contracts in the inbox module.
- Register store roles directly or through a store package such as
LiteBus.Inbox.PostgreSql. - Run
ICommandInboxProcessor.ProcessPendingAsyncfrom a worker, timer, or hosting adapter.
The Command Module uses several advanced features that are shared across all LiteBus modules. For detailed explanations, see the dedicated pages:
-
Handler Priority: Control the execution order of pre-handlers and post-handlers using the
[HandlerPriority]attribute. - Handler Filtering: Selectively execute handlers based on context using tags or predicates.
- Execution Context: Share data between handlers and control the execution flow within a single command pipeline.
- Polymorphic Dispatch: Create handlers for base command types that can process derived commands.
- Generic Messages & Handlers: Build reusable, generic commands and handlers.
- Open Generic Handlers: Write a single pre/post/error handler that automatically applies to all commands matching its constraints, useful for cross-cutting concerns like logging, validation, and metrics.
- Domain Events and Unit of Work: Collect domain events from aggregates and dispatch them near the transaction boundary.
-
Immutability: Design commands as immutable records or classes with
init-only properties. -
Validation: Use pre-handlers or the specialized
ICommandValidatorinterface to validate commands before execution. - Idempotency: For critical operations, design handlers to be idempotent so they can be safely retried.
- Focus: A command should represent a single, atomic business operation.