Skip to content

Command Module

A. Shafie edited this page Sep 26, 2025 · 5 revisions

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.

Core Concepts

  • 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).

Command Contracts

LiteBus provides two interfaces for defining commands.

ICommand

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 │...
}

ICommand<TCommandResult>

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

Command handlers contain the business logic to process a command.

  • ICommandHandler<TCommand>: For commands implementing ICommand.
  • ICommandHandler<TCommand, TCommandResult>: For commands implementing ICommand<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 Command Mediator

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);
}

Usage

// 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();
    }
}

Durable Command Inbox (v4.0)

A major feature introduced in v4.0 is the durable command inbox, which provides guaranteed, fault-tolerant, and deferred command execution.

  • Purpose: To ensure critical commands are eventually processed, even if the application restarts or fails.
  • Mechanism: Commands are first persisted to a durable store (e.g., a database table) and then processed asynchronously by a background service.

How to Use

Mark any command with the [StoreInInbox] attribute to enable this behavior.

using LiteBus.Commands.Abstractions;

/// <summary>
/// This command is critical and must be processed. It will be stored
/// in the inbox for guaranteed execution.
/// </summary>
[StoreInInbox]
public sealed class ProcessPaymentCommand : ICommand
{
    public required Guid OrderId { get; init; }
    public required decimal Amount { get; init; }
}

Behavior

  • When SendAsync is called for an inbox-enabled command, LiteBus stores it via the registered ICommandInbox implementation and returns immediately.
  • If the command has a result type (e.g., ICommand<TResult>), SendAsync will return Task.FromResult(default(TResult)). The caller should not expect an immediate result. This pattern is suitable for API endpoints that return an HTTP 202 Accepted response.

Configuration

To use the inbox, you must:

  1. Implement ICommandInbox and ICommandInboxProcessor.
  2. Register your implementations in the DI container.
  3. Register the CommandInboxProcessorHostedService to run the background processor.

Shared Advanced Features

The Command Module utilizes 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.

Best Practices

  1. Immutability: Design commands as immutable records or classes with init-only properties.
  2. Validation: Use pre-handlers or the specialized ICommandValidator interface to validate commands before execution.
  3. Idempotency: For critical operations, design handlers to be idempotent so they can be safely retried.
  4. Focus: A command should represent a single, atomic business operation.
Clone this wiki locally