Core Concepts
This section covers the foundational concepts you need to understand to use Dispatch effectively.
Before You Start
- .NET 8.0+ (or .NET 9/10 for latest features)
- Install the required packages:
dotnet add package Excalibur.Dispatch - Complete the Getting Started guide
Overview
Dispatch is built around a simple flow:
flowchart LR
A[Action] --> B[Dispatcher]
B --> C[Pipeline]
C --> D[Handler]
D --> E[Result]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#e8f5e9
style E fill:#c8e6c9
- Action - A message representing something you want to do
- Dispatcher - Routes actions to the right handler
- Pipeline - Optional behaviors that run before/after handling
- Handler - Code that processes the action
- Result - Success or failure with optional data
Key Concepts
Actions
Actions are messages that represent work to be done. They're simple data classes:
// Action without return value (command)
public record CreateOrderAction(string CustomerId, List<string> Items) : IDispatchAction;
// Action with return value (query)
public record GetOrderAction(Guid OrderId) : IDispatchAction<Order>;
Learn more: Actions and Handlers
Handlers
Handlers contain the business logic that processes actions:
public class CreateOrderHandler : IActionHandler<CreateOrderAction>
{
public async Task HandleAsync(CreateOrderAction action, CancellationToken ct)
{
// Process the action
}
}
Learn more: Actions and Handlers
Results
Results indicate success or failure without exceptions:
var result = await dispatcher.DispatchAsync(action, cancellationToken);
if (result.IsSuccess)
{
// Handle success
}
else
{
// Handle failure
var error = result.ErrorMessage;
}
Learn more: Results and Errors
Message Context
Context carries metadata through the pipeline:
public class MyHandler : IActionHandler<MyAction>
{
private readonly IMessageContextAccessor _contextAccessor;
public async Task HandleAsync(MyAction action, CancellationToken ct)
{
var context = _contextAccessor.MessageContext;
var correlationId = context.CorrelationId;
var tenantId = context.GetItem<string>("TenantId");
}
}
Learn more: Message Context
Dependency Injection
Dispatch integrates seamlessly with Microsoft.Extensions.DependencyInjection:
// Recommended pattern with fluent configuration
services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
dispatch.ConfigurePipeline("Default", pipeline =>
{
pipeline.Use<ValidationMiddleware>();
});
});
Learn more: Dependency Injection
Gotchas and Common Mistakes
Handlers run sequentially by default
Dispatch executes action handlers one at a time through the pipeline. If you register multiple event handlers for the same event, they also run in sequence. This is intentional -- it guarantees predictable ordering and avoids concurrency issues within a single dispatch call.
The mistake: Assuming handlers run in parallel and designing for concurrent execution.
Why it matters: If Handler A takes 500ms and Handler B takes 300ms, the total dispatch time is ~800ms, not ~500ms. If you need true parallelism, dispatch separate messages or use background processing.
Context is per-request scoped
IMessageContext is created fresh for each top-level DispatchAsync call. Items set on the context are visible to all middleware and the handler within that pipeline execution, but are not shared across separate dispatches.
The mistake: Setting a value on IMessageContext in one handler and expecting to read it in an unrelated dispatch:
// In Handler A:
context.SetItem("TenantId", "acme");
// In a separate dispatch -- this is a NEW context, "TenantId" is NOT set
await dispatcher.DispatchAsync(new OtherAction(), ct);
Correct approach: Use DispatchChildAsync for related dispatches (copies correlation/tenant metadata), or pass data through the message itself.
Use DispatchChildAsync for nested dispatch
When dispatching a message from within an existing handler, always use DispatchChildAsync instead of DispatchAsync:
// Wrong: loses causation chain
await _dispatcher.DispatchAsync(new NotifyCustomerAction(orderId), ct);
// Correct: propagates CorrelationId, TenantId, UserId, sets CausationId
await _dispatcher.DispatchChildAsync(new NotifyCustomerAction(orderId), ct);
DispatchChildAsync creates a child context that maintains the full message lineage -- essential for distributed tracing and debugging.
Recommended Reading Order
- Actions and Handlers -- Start here. Learn how messages flow and how handlers process them.
- Results and Errors -- How to handle success and failure without exceptions.
- Message Context -- Passing metadata (correlation, tenant, user) through the pipeline.
- Dependency Injection -- Registration patterns and service lifetimes.
- Configuration -- Builder API, options, and environment-specific settings.
What's Next
Once you understand these core concepts, move on to building with Dispatch:
- Handlers - Deep dive into action and event handler patterns
- Pipeline - Add middleware behaviors to your message processing
- Transports - Configure message transports for production
See Also
- Getting Started - Quick start tutorial
- Configuration - Detailed configuration reference
- Middleware - Cross-cutting concerns and pipeline behaviors