Skip to content

LiteBus Cheat Sheet

A. Shafie edited this page May 29, 2026 · 8 revisions

Architecture & Layers

LiteBus is designed to fit cleanly into .NET application architectures. It acts as the "Application Layer" orchestrator, decoupling your API/UI from your business logic.

  • Presentation Layer (API/UI): Injects and uses mediator interfaces (ICommandMediator, IQueryMediator, IEventPublisher) to send messages. It is completely unaware of the business logic implementation.
  • Application Layer (Your Code + LiteBus):
    • Messages: Simple, immutable DTOs (ICommand, IQuery, IEvent) that represent user intent or system facts.
    • Mediators: The entry point into LiteBus. They find the correct handlers and manage the execution pipeline.
    • Pipeline: For each message, LiteBus invokes a sequence of handlers: Pre-Handlers, then Main Handler, then Post-Handlers. If an error occurs, Error Handlers are invoked.
    • Handlers: The core of your business logic. They depend on infrastructure contracts (IRepository, etc.) but not on the presentation layer.
  • Infrastructure/Domain Layer: Contains your domain model, database access, and external service clients. Handlers use interfaces defined here.

NuGet Package Layers

LiteBus is modular, so you only install what you need. The layers are:

  1. Abstractions (.Abstractions): Contains only the interfaces (ICommand, IQuery, IEvent, ICommandHandler, etc.). Your domain and application layers can depend on these without pulling in the full library.
  2. Core Logic (LiteBus.Commands, .Queries, .Events): The main implementation of the mediator and pipeline logic.
  3. DI Extensions (.Extensions.Microsoft.DependencyInjection, .Autofac): Glue code that integrates LiteBus with a specific dependency injection container.

Core Concepts

  • Command: An intent to change system state. Handled by exactly one handler.
  • Query: A request for data that does not change state. Handled by exactly one handler.
  • Event: A notification that something has occurred. Handled by zero or more handlers.

Installation & Configuration

NuGet Packages (for Microsoft DI):

dotnet add package LiteBus.Commands.Extensions.Microsoft.DependencyInjection
dotnet add package LiteBus.Queries.Extensions.Microsoft.DependencyInjection
dotnet add package LiteBus.Events.Extensions.Microsoft.DependencyInjection

Configuration (Program.cs):

var builder = WebApplication.CreateBuilder(args);

// LiteBus is DI-agnostic. This example uses the Microsoft DI extensions.
builder.Services.AddLiteBus(liteBus =>
{
    // Scan an assembly for all commands, queries, events, and their handlers
    var appAssembly = typeof(Program).Assembly;
    liteBus.AddCommandModule(module => module.RegisterFromAssembly(appAssembly));
    liteBus.AddQueryModule(module => module.RegisterFromAssembly(appAssembly));
    liteBus.AddEventModule(module => module.RegisterFromAssembly(appAssembly));
});

var app = builder.Build();

Message & Handler Definitions

Type Message Definition Handler Definition
Command (void) public sealed record ShipOrderCommand(...) : ICommand; public sealed class ShipOrderHandler : ICommandHandler<ShipOrderCommand>
Command (result) public sealed record CreateProductCommand(...) : ICommand<Guid>; public sealed class CreateProductHandler : ICommandHandler<CreateProductCommand, Guid>
Query public sealed record GetProductQuery(...) : IQuery<ProductDto>; public sealed class GetProductHandler : IQueryHandler<GetProductQuery, ProductDto>
Stream Query public sealed record StreamProductsQuery(...) : IStreamQuery<ProductDto>; public sealed class StreamProductsHandler : IStreamQueryHandler<StreamProductsQuery, ProductDto>
Event public sealed record OrderShippedEvent(...) : IEvent; public sealed class OrderShippedHandler : IEventHandler<OrderShippedEvent>
POCO Event public sealed record OrderShipped { ... }; public sealed class OrderShippedHandler : IEventHandler<OrderShipped>

Note: POCO events are published via the generic _eventPublisher.PublishAsync<TEvent>(myPocoEvent) method.


Mediator Usage

Inject ICommandMediator, IQueryMediator, and IEventPublisher into your services.

Commands:

// Fire-and-forget
await _commandMediator.SendAsync(new ShipOrderCommand(...));

// With result
Guid productId = await _commandMediator.SendAsync(new CreateProductCommand(...));

Queries:

// Single result
ProductDto dto = await _queryMediator.QueryAsync(new GetProductQuery(id));

// Stream result
await foreach (var item in _queryMediator.StreamAsync(new StreamProductsQuery()))
{
    // ... process each item
}

Events:

await _eventPublisher.PublishAsync(new OrderShippedEvent(...));

Handler Pipeline

Handlers execute in a specific order: Global Pre-Handlers, then Specific Pre-Handlers, then Main Handler, then Specific Post-Handlers, then Global Post-Handlers.

Handler Type Interface / Purpose Scope
Pre-Handler ICommandPreHandler<T>. For validation, permissions, caching checks. Specific (<MyCommand>), Global (<ICommand>), or Open Generic (<T> where T : ICommand)
Validator ICommandValidator<T>. Semantic sugar for pre-handlers. Specific
Main Handler ICommandHandler<T, TResult>. Core business logic. Specific
Post-Handler ICommandPostHandler<T, TResult>. For side effects like notifications, cache invalidation. Specific (<MyCommand, MyResult>), Global (<ICommand>), or Open Generic (<T> where T : ICommand)
Error Handler ICommandErrorHandler<T>. Centralized exception logic. Specific (<MyCommand>), Global (<ICommand>), or Open Generic (<T> where T : ICommand)

(The same patterns apply to Queries and Events)

Example Validator:

public sealed class CreateProductValidator : ICommandValidator<CreateProductCommand>
{
    public Task ValidateAsync(CreateProductCommand command, CancellationToken ct)
    {
        if (command.Price <= 0)
        {
            throw new ValidationException("Price must be positive.");
        }
        return Task.CompletedTask;
    }
}

Advanced Features

Handler Priority: Control execution order for pre/post/event handlers. Lower numbers run first (default: 0).

[HandlerPriority(10)]
public class ValidationHandler : ICommandPreHandler<MyCommand> { /*...*/ }

[HandlerPriority(20)]
public class EnrichmentHandler : ICommandPreHandler<MyCommand> { /*...*/ }

Handler Filtering: Select handlers at runtime.

  • Tags: Static labels. Untagged handlers always run.
    [HandlerTag("PublicAPI")]
    public class StrictValidator : ICommandValidator<MyCommand> { /*...*/ }
    
    // Mediate with tag
    var settings = new CommandMediationSettings { Filters = { Tags = new[] { "PublicAPI" } } };
    await _commandMediator.SendAsync(command, settings);
  • Predicates: Runtime logic, most useful with events.
    var settings = new EventMediationSettings
    {
        Routing = { HandlerPredicate = d => d.HandlerType.Namespace.StartsWith("MyProject.Core") }
    };
    await _eventPublisher.PublishAsync(e, settings);

Polymorphic Dispatch: A handler for a base type or interface can process any derived message. This is a core mechanism but is most useful for creating cross-cutting handlers (pre, post, error).

// 1. Define base interface
public interface IAuditableEvent : IEvent { }

// 2. Implement on events
public record ProductCreatedEvent(...) : IAuditableEvent;
public record UserLoggedInEvent(...) : IAuditableEvent;

// 3. Handle the base interface (this will run for both events)
public class AuditingHandler : IEventPostHandler<IAuditableEvent> { /*...*/ }

Generic Messages & Handlers: Create reusable components to reduce repeated code for common operations (e.g., CRUD).

// Generic Message
public record CreateEntityCommand<T>(T Entity) : ICommand<Guid>;

// Generic Handler
public class CreateEntityHandler<T> : ICommandHandler<CreateEntityCommand<T>, Guid> { /*...*/ }

// Register the open generic handler type in your module configuration
module.Register(typeof(CreateEntityHandler<>));

Open Generic Handlers: Write a single handler that automatically applies to all message types satisfying its constraints. Ideal for cross-cutting concerns like logging, validation, and authorization.

// Open generic pre-handler, applies to every ICommand
public sealed class CommandLogger<T> : ICommandPreHandler<T> where T : ICommand
{
    public Task PreHandleAsync(T message, CancellationToken ct)
    {
        Console.WriteLine($"Executing: {typeof(T).Name}");
        return Task.CompletedTask;
    }
}

// Open generic post-handler, applies to every ICommand
public sealed class CommandMetrics<T> : ICommandPostHandler<T> where T : ICommand
{
    public Task PostHandleAsync(T message, object? result, CancellationToken ct)
    {
        // Record metrics...
        return Task.CompletedTask;
    }
}

// RegisterFromAssembly discovers open generics automatically
module.RegisterFromAssembly(typeof(Program).Assembly);

// Or register explicitly if the handler is in a different assembly
module.Register(typeof(CommandLogger<>));
module.Register(typeof(CommandMetrics<>));

LiteBus automatically closes the generic for each concrete message type at startup. Generic constraints (interface, class, struct, new()) are fully respected.

Execution Context: Share data and control flow within a single mediation pipeline.

// Access anywhere in the pipeline
IExecutionContext context = AmbientExecutionContext.Current;

// Share data between handlers
context.Items["UserId"] = "user-123";
var userId = context.Items["UserId"];

// Abort pipeline (e.g., from a pre-handler)
context.Abort(); // For void commands/events
context.Abort(cachedResult); // For queries or commands with results

Command Inbox

Stores ICommand instances for deferred, at-least-once execution.

  • Usage: Schedule a command without a result through ICommandScheduler.
    public sealed record ProcessPaymentCommand(Guid OrderId, decimal Amount) : ICommand;
    
    var receipt = await commandScheduler.ScheduleAsync(
        new ProcessPaymentCommand(orderId, amount),
        new CommandScheduleOptions
        {
            IdempotencyKey = $"payment:{orderId}"
        },
        cancellationToken);
  • Behavior: SendAsync executes now. ScheduleAsync stores for later execution and returns a receipt.
    • ICommand<TResult> is not scheduled because future execution cannot return a result to the original caller.
    • The command processor later executes the command through the normal pipeline and adds CommandInboxExecutionContextKeys.IsInboxExecution to context Items.
    • Closed generic commands are supported when each closed type is registered with an inbox contract.
  • Configuration: Requires implementing and registering:
    1. ICommandInboxWriter for accepted commands.
    2. ICommandInboxLeaseStore for worker leases.
    3. ICommandInboxStateStore for completion, retry, and dead-letter state.
    4. ICommandInboxProcessor from a worker, timer, or hosting adapter.

Advanced Event Mediation

Fine-grained control over event handler execution via EventMediationSettings.

var settings = new EventMediationSettings
{
    // Pass contextual data to all handlers in the pipeline
    Items = { ["CorrelationId"] = "some-id" },

    // How handlers are selected
    Routing = new EventMediationRoutingSettings
    {
        Tags = new[] { "Notifications" },
        HandlerPredicate = d => d.Priority < 100
    },
    // How selected handlers are executed
    Execution = new EventMediationExecutionSettings
    {
        // How priority groups run relative to each other (e.g., group 1 vs group 2)
        PriorityGroupsConcurrencyMode = ConcurrencyMode.Sequential, // or Parallel

        // How handlers within the same priority group run
        HandlersWithinSamePriorityConcurrencyMode = ConcurrencyMode.Parallel // or Sequential
    }
};
await _eventPublisher.PublishAsync(myEvent, settings);

When priority groups run sequentially, lower priority groups finish before later groups start. When priority groups run in parallel, priority is grouping metadata; later groups can finish first.

Clone this wiki locally