Skip to main content

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
  1. Action - A message representing something you want to do
  2. Dispatcher - Routes actions to the right handler
  3. Pipeline - Optional behaviors that run before/after handling
  4. Handler - Code that processes the action
  5. 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.

  1. Actions and Handlers -- Start here. Learn how messages flow and how handlers process them.
  2. Results and Errors -- How to handle success and failure without exceptions.
  3. Message Context -- Passing metadata (correlation, tenant, user) through the pipeline.
  4. Dependency Injection -- Registration patterns and service lifetimes.
  5. 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