Skip to main content

Getting Started with Excalibur

This guide gets you up and running with Excalibur.Dispatch in under 5 minutes. By the end, you'll have a working message handler processing commands through the messaging pipeline.

Before You Start

  • .NET 8.0+ (or .NET 9/10 for latest features)
  • An IDE (Visual Studio, VS Code, or Rider)
  • Install the required packages:
    dotnet add package Excalibur.Dispatch

Step 1: Define an Action

Actions are messages that trigger handlers. They can be commands (no return value) or queries (with return value).

using Excalibur.Dispatch.Abstractions;

// Action without return value
public record CreateOrderAction(string CustomerId, List<string> Items) : IDispatchAction;

// Action with return value
public record GetOrderAction(Guid OrderId) : IDispatchAction<Order>;

Step 2: Create a Handler

Handlers process actions. Use IActionHandler<TAction> for commands or IActionHandler<TAction, TResult> for queries.

using Excalibur.Dispatch.Abstractions.Delivery;

// Handler for action without return value
public class CreateOrderHandler : IActionHandler<CreateOrderAction>
{
private readonly IOrderRepository _repository;

public CreateOrderHandler(IOrderRepository repository)
{
_repository = repository;
}

public async Task HandleAsync(
CreateOrderAction action,
CancellationToken cancellationToken)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = action.CustomerId,
Items = action.Items
};

await _repository.SaveAsync(order, cancellationToken);
}
}

// Handler for action with return value
public class GetOrderHandler : IActionHandler<GetOrderAction, Order>
{
private readonly IOrderRepository _repository;

public GetOrderHandler(IOrderRepository repository)
{
_repository = repository;
}

public async Task<Order> HandleAsync(
GetOrderAction action,
CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(action.OrderId, cancellationToken);
}
}

Step 3: Register Services

Configure Excalibur.Dispatch in your Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register Dispatch — auto-discovers handlers from the entry assembly
builder.Services.AddDispatch();

// Register your dependencies
builder.Services.AddScoped<IOrderRepository, OrderRepository>();

var app = builder.Build();

Step 4: Dispatch Messages

Inject IDispatcher and send messages. No explicit context is needed — the framework manages context automatically:

using Excalibur.Dispatch.Abstractions;

public class OrderController : ControllerBase
{
private readonly IDispatcher _dispatcher;

public OrderController(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}

[HttpPost]
public async Task<IActionResult> CreateOrder(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
var action = new CreateOrderAction(request.CustomerId, request.Items);

// No context needed - Dispatch creates one automatically
var result = await _dispatcher.DispatchAsync(action, cancellationToken);

if (result.IsSuccess)
return Ok();

return BadRequest(result.ErrorMessage);
}

[HttpGet("{orderId}")]
public async Task<IActionResult> GetOrder(
Guid orderId,
CancellationToken cancellationToken)
{
var action = new GetOrderAction(orderId);

// Context-less dispatch for queries too
var result = await _dispatcher.DispatchAsync<GetOrderAction, Order>(
action, cancellationToken);

if (result.IsSuccess)
return Ok(result.ReturnValue);

return NotFound(result.ErrorMessage);
}
}

Complete Example

Here's a complete minimal example:

Program.cs
using Excalibur.Dispatch.Abstractions;
using Excalibur.Dispatch.Abstractions.Delivery;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDispatch();

var app = builder.Build();

app.MapPost("/greet", async (
GreetRequest request,
IDispatcher dispatcher,
CancellationToken ct) =>
{
var action = new GreetAction(request.Name);

// Simple dispatch - no context needed!
var result = await dispatcher.DispatchAsync<GreetAction, string>(action, ct);

return result.IsSuccess ? Results.Ok(result.ReturnValue) : Results.BadRequest();
});

app.Run();

// Action with return value
public record GreetAction(string Name) : IDispatchAction<string>;

// Handler
public class GreetHandler : IActionHandler<GreetAction, string>
{
public Task<string> HandleAsync(GreetAction action, CancellationToken ct)
{
return Task.FromResult($"Hello, {action.Name}!");
}
}

// Request DTO
public record GreetRequest(string Name);
You Have a Working App — Stop Here Unless You Need More

The example above is a complete, production-ready Dispatch application. You don't need anything else to start building.

If you need...Next step
A hands-on walkthrough?Try the Order System Tutorial — build a complete API in 15 minutes.
Event sourcing?Continue to Event-Sourced Order System — add aggregates, event store, and projections.
Security and compliance?Continue to Secure Order System — add authorization, audit logging, and structured commands.
Just dispatching?You're done. See Dispatch Only for a focused reference.
Input validation?Add FluentValidation middleware
Event sourcing?Continue to Event Sourcing
Domain aggregates?Continue to Domain Modeling
Multi-step workflows?Continue to Sagas
Message broker?Continue to Transports

Key Concepts

ConceptDescription
IDispatchActionBase interface for actions (commands without return value)
IDispatchAction<TResult>Base interface for actions with return value
IActionHandler<TAction>Handler for actions without return value
IActionHandler<TAction, TResult>Handler for actions with return value
IDispatcherCentral dispatcher for sending messages
IMessageContextContext for message metadata (correlation, tenant, etc.) - managed automatically
IMessageResultResult wrapper containing success/failure and errors

Context Management

Excalibur.Dispatch automatically manages message context for you:

  • Top-level dispatch: A new context is created with a unique CorrelationId
  • Nested dispatch: Use DispatchChildAsync to propagate context in handlers
  • Ambient context: The current context is available via IMessageContextAccessor
// From a controller (top-level) - context created automatically
await _dispatcher.DispatchAsync(action, cancellationToken);

// From within a handler (nested) - use child context for proper tracing
await _dispatcher.DispatchChildAsync(action, cancellationToken);

See Handlers for more details on nested dispatch patterns.

Excalibur.Dispatch vs MediatR

If you're coming from MediatR, here's how concepts map:

MediatRExcalibur.Dispatch
IRequestIDispatchAction
IRequest<TResponse>IDispatchAction<TResult>
IRequestHandler<TRequest>IActionHandler<TAction>
IRequestHandler<TRequest, TResponse>IActionHandler<TAction, TResult>
INotificationIDispatchEvent
INotificationHandler<T>IEventHandler<TEvent>
IMediatorIDispatcher

Adding More Packages

Excalibur.Dispatch is the messaging core. Add more Excalibur packages as your architecture grows:

NeedPackage
Unified Excalibur builder (AddExcalibur)Excalibur.Hosting
Domain modeling (aggregates, entities)Excalibur.Domain
Event sourcing with persistenceExcalibur.EventSourcing
SQL Server event storeExcalibur.EventSourcing.SqlServer
Outbox patternExcalibur.Outbox
Change data captureExcalibur.Cdc
Sagas/process managersExcalibur.Saga
Leader electionExcalibur.LeaderElection
ASP.NET Core hosting integrationExcalibur.Hosting
# Add hosting (includes unified entry point for all subsystems)
dotnet add package Excalibur.Hosting

# Add domain modeling
dotnet add package Excalibur.Domain

# Add event sourcing
dotnet add package Excalibur.EventSourcing
dotnet add package Excalibur.EventSourcing.SqlServer

# Optional subsystems used by AddExcalibur(...).AddXxx(...)
dotnet add package Excalibur.Outbox
dotnet add package Excalibur.Cdc
dotnet add package Excalibur.Saga
dotnet add package Excalibur.LeaderElection

Unified Registration

Use AddExcalibur() as the single entry point for domain, event sourcing, and saga subsystems. It registers messaging primitives with sensible defaults:

Feature methods are provided by their feature packages:

  • .AddEventSourcing(...) => Excalibur.EventSourcing
  • .AddOutbox(...) => Excalibur.Outbox
  • .AddCdc(...) => Excalibur.Cdc
  • .AddSagas(...) => Excalibur.Saga
  • .AddLeaderElection(...) => Excalibur.LeaderElection
builder.Services.AddExcalibur(excalibur =>
{
excalibur
.AddEventSourcing(es => es.UseEventStore<SqlServerEventStore>())
.AddOutbox(outbox => outbox.UseSqlServer(opts => opts.ConnectionString = connectionString))
.AddCdc(cdc => cdc.TrackTable<Order>())
.AddSagas(opts => opts.EnableTimeouts = true)
.AddLeaderElection(opts => opts.LeaseDuration = TimeSpan.FromSeconds(30));
});

Need custom messaging configuration (transports, pipelines, middleware)? Call AddDispatch with a builder action:

// Configure Dispatch with transports and middleware (handlers auto-discovered)
builder.Services.AddDispatch(dispatch =>
{
dispatch.UseRabbitMQ(rmq => rmq.HostName("localhost"));
dispatch.UseObservability();
});

// Configure Excalibur subsystems
builder.Services.AddExcalibur(excalibur =>
{
excalibur
.AddEventSourcing(es => es.UseEventStore<SqlServerEventStore>())
.AddOutbox(outbox => outbox.UseSqlServer(opts => opts.ConnectionString = connectionString));
});

You can also bind Excalibur options from appsettings.json:

appsettings.json
{
"Excalibur": {
"EventSourcing": { "Enabled": true, "SnapshotFrequency": 100 },
"Outbox": { "Enabled": true, "PollingInterval": "00:00:05" },
"Saga": { "Enabled": false },
"LeaderElection": { "Enabled": false },
"Cdc": { "Enabled": false }
}
}
builder.Services.Configure<ExcaliburOptions>(
builder.Configuration.GetSection("Excalibur"));

See the Package Guide for the complete package selection framework with migration paths and code examples.

Step 5: Add a Transport (Optional)

The examples above dispatch messages in-process (no broker needed). When you're ready to send messages to a real broker, add a transport package and configure routing:

dotnet add package Excalibur.Dispatch.Transport.RabbitMQ
// Register the transport with destination mapping
services.AddRabbitMQTransport("rabbitmq", rmq =>
{
rmq.ConnectionString("amqp://guest:guest@localhost:5672/")
.MapQueue<CreateOrderAction>("orders-queue");
});

// Configure routing rules
services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
dispatch.UseRouting(routing =>
{
routing.Transport
.Route<CreateOrderAction>().To("rabbitmq")
.Default("local"); // Keep unmatched messages in-process
});
});

Your handlers don't change — only the registration code changes. See Choosing a Transport to pick the right broker and Message Routing for the full routing API.

StepPageWhat You'll Learn
1You are hereInstall Dispatch and create your first handler
2Your First EventEvents with multiple handlers
3SamplesBrowse working examples for your use case
4Core ConceptsPipelines, middleware, and context

Looking for a focused reference? See Dispatch Only for Dispatch without event sourcing or sagas.

What's Next

Coming from Another Framework?

If you're migrating from an existing messaging framework, start here:

See Also