-
Notifications
You must be signed in to change notification settings - Fork 12
LiteBus Cheat Sheet
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.
-
Messages: Simple, immutable DTOs (
- Infrastructure/Domain Layer: Contains your domain model, database access, and external service clients. Handlers use interfaces defined here.
LiteBus is modular, so you only install what you need. The layers are:
-
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. -
Core Logic (
LiteBus.Commands,.Queries,.Events): The main implementation of the mediator and pipeline logic. -
DI Extensions (
.Extensions.Microsoft.DependencyInjection,.Autofac): Glue code that integrates LiteBus with a specific dependency injection container.
- 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.
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.DependencyInjectionConfiguration (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();| 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.
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(...));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;
}
}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 resultsGuarantees 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:
SendAsyncpersists the command and returns immediately.- For
ICommand<TResult>, it returnsTask.FromResult(default(TResult)). - Ideal for returning an
HTTP 202 Acceptedresponse. - The command processor will later pick it up and execute it through the normal pipeline, adding an
IsInboxExecutionflag to the contextItemsto prevent re-inboxing.
- For
-
Configuration: Requires implementing and registering:
-
ICommandInbox(stores the command, e.g., in a database). -
ICommandInboxProcessor(fetches and processes commands). -
CommandInboxProcessorHostedService(runs the processor as a background service).
-
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);