Skip to content

LiteBus Cheat Sheet

A. Shafie edited this page Nov 11, 2025 · 2 revisions

Architecture & Layers

LiteBus is designed to fit cleanly into modern 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 → Main Handler → 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 → Specific Pre-Handlers → Main Handler → Specific Post-Handlers → Global Post-Handlers.

Handler Type Interface / Purpose Scope
Pre-Handler ICommandPreHandler<T>. For validation, permissions, caching checks. Specific (<MyCommand>) or Global (<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>) or Global (<ICommand>)
Error Handler ICommandErrorHandler<T>. Centralized exception logic. Specific (<MyCommand>) or Global (<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: Dynamic logic (most powerful 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 boilerplate 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<>));

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

Durable Command Inbox

Guarantees at-least-once execution for critical commands, even if the application restarts.

  • Usage: Add [StoreInInbox] attribute to a command.
    [StoreInInbox]
    public sealed record ProcessPaymentCommand(...) : ICommand;
  • Behavior: SendAsync persists the command and returns immediately.
    • For ICommand<TResult>, it returns Task.FromResult(default(TResult)).
    • Ideal for returning an HTTP 202 Accepted response.
    • The command processor will later pick it up and execute it through the normal pipeline, adding an IsInboxExecution flag to the context Items to prevent re-inboxing.
  • Configuration: Requires implementing and registering:
    1. ICommandInbox (stores the command, e.g., in a database).
    2. ICommandInboxProcessor (fetches and processes commands).
    3. CommandInboxProcessorHostedService (runs the processor as a background service).

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

Clone this wiki locally