Skip to main content

Excalibur LLM Quick Reference

For AI Coding Agents

This page is optimized for LLM coding agents and AI pair-programming assistants. It provides the essential information needed to help developers use this framework without reading all documentation pages. For the full docs, see the sidebar navigation.

Excalibur is a .NET 10 NuGet package framework (75+ packages) with focused package families:

  • Excalibur.Dispatch.* — Messaging (MediatR alternative): dispatching, pipelines, middleware, transports
  • Excalibur.Domain — Domain modeling (DDD): aggregates, entities, value objects
  • Excalibur.EventSourcing.* — Persistence: event stores, snapshots, repositories
  • Excalibur.Saga.* — Workflows: sagas, process managers, outbox

Excalibur.Dispatch is the foundation. All other families depend on it, but it depends on none of them.

Package Map​

Install ThisWhen You Need
Excalibur.Dispatch + Excalibur.Dispatch.AbstractionsIn-process messaging (MediatR replacement)
Excalibur.Dispatch.Transport.RabbitMQRabbitMQ transport
Excalibur.Dispatch.Transport.KafkaKafka transport
Excalibur.Dispatch.Transport.AzureServiceBusAzure Service Bus transport
Excalibur.Dispatch.Transport.AwsSqsAWS SQS transport
Excalibur.Dispatch.Transport.GooglePubSubGoogle Pub/Sub transport
Excalibur.DomainAggregates, entities, value objects (DDD building blocks)
Excalibur.ApplicationCQRS base classes: CommandBase, QueryBase, ICommand, IQuery<T>
Excalibur.EventSourcing + Excalibur.EventSourcing.AbstractionsEvent store, repository, snapshots, upcasting
Excalibur.EventSourcing.SqlServerSQL Server event store + snapshot store
Excalibur.OutboxTransactional outbox pattern
Excalibur.SagaSaga / process manager abstractions
Excalibur.Data + Excalibur.Data.AbstractionsData access: IDb, IDataRequest, IUnitOfWork
Excalibur.Dispatch.ObservabilityOpenTelemetry metrics and tracing
Excalibur.Dispatch.Resilience.PollyCircuit breakers, retries (Polly v8)
Excalibur.SecurityAuthentication, encryption, signing
Excalibur.Dispatch.TestingTest utilities and fakes

Dispatch Core Types​

All message types are in namespace Excalibur.Dispatch. Handler types are in Excalibur.Dispatch.Delivery.

TypeKindKey Members
IDispatchMessageMarker interfaceBase for all messages. No members.
IDispatchActionCommand (no return)Extends IDispatchMessage
IDispatchAction<TResponse>Query (returns T)Extends IDispatchAction
IDispatchEventEvent (pub/sub)Extends IDispatchMessage. Multiple handlers allowed.
IDispatchDocumentDocument/batchExtends IDispatchMessage. For ETL, bulk processing.
IDomainEventDomain eventExtends IDispatchEvent. Has EventId, OccurredAt, Metadata, CorrelationId, CausationId. Carries NO aggregate id or stream version — those are supplied to / assigned by the event store.
DomainEventBase recordAbstract record implementing IDomainEvent with auto-generated EventId (UUID v7). Namespace: Excalibur.Dispatch.
MessageNameAttributeStable name[MessageName("Contoso.Orders.OrderCreated")]. Required on every registered event type; it is the name used by the event store, the outbox MessageType, and the CloudEvents type. Namespace: Excalibur.Dispatch.
MessageNameAliasAttributeRetired name[MessageNameAlias("Contoso.Crm.CustomerCreated")]. Repeatable, read-side only — lets data written under an old name stay readable.
IActionHandler<TAction>Command handler1 method: Task HandleAsync(TAction action, CancellationToken cancellationToken)
IActionHandler<TAction, TResult>Query handler1 method: Task<TResult> HandleAsync(TAction action, CancellationToken cancellationToken)
IEventHandler<TEvent>Event handler1 method: Task HandleAsync(TEvent eventMessage, CancellationToken cancellationToken)
IDispatcherCentral dispatcher6 dispatch methods + ServiceProvider property
IMessageResultResult wrapperSucceeded, ErrorMessage, ProblemDetails
IMessageResult<T>Typed resultAdds ReturnValue property

Excalibur Domain Types​

Domain building blocks are in namespace Excalibur.Domain.Model.

TypeKindKey Members
AggregateRoot<TKey>Aggregate baseId, Version, ETag, RaiseEvent(IDomainEvent), abstract ApplyEventInternal(IDomainEvent), LoadFromHistory(IEnumerable<HistoricEvent>), LoadFromSnapshot(ISnapshot), GetUncommittedEvents(), MarkEventsAsCommitted()
AggregateRootString-key shorthandExtends AggregateRoot<string>
HistoricEventReplay envelopereadonly record struct HistoricEvent(IDomainEvent Event, long Version). Pairs a persisted event with the store-assigned stream version used during replay. FromEvents/LoadFromHistory consume IEnumerable<HistoricEvent> — the version comes from this envelope, never from the event payload.
EntityBase<TKey>Entity baseAbstract Key property, equality by type + key
EntityBaseString-key shorthandExtends EntityBase<string>
ValueObjectBaseValue object baseAbstract GetEqualityComponents(), component-based equality, ==/!= operators
DomainEventDomain event baseAbstract record. Auto-generates EventId (UUID v7) and OccurredAt (UTC via TimeProvider). Its stored name comes from the required [MessageName] attribute. Put event data in the record's own properties. Fluent API: WithMetadata(), WithCorrelationId(), WithCausationId(). Namespace: Excalibur.Dispatch.
ISnapshotSnapshot interfaceSnapshotId, AggregateId, AggregateType, Version, CreatedAt, Data
Two DomainEvent base types
  • DomainEvent in Excalibur.Dispatch.Abstractions — parameterless construction, event data in the record's own properties, uses UUID v7 EventId, fluent metadata API

Both implement IDomainEvent. Use whichever fits your project.

Excalibur CQRS Types​

CQRS types are in Excalibur.Application. They extend Dispatch's IDispatchAction with richer metadata.

TypeKindKey Members
ICommandCommand interfaceExtends IDispatchAction. Has Id, MessageId, CorrelationId, TenantId, TransactionBehavior.
ICommand<TResult>Command with resultExtends ICommand + IDispatchAction<TResult>
CommandBaseCommand base classConstructor: CommandBase(Guid correlationId, string? tenantId = null). Provides Id, MessageId, Headers, transaction control.
CommandBase<TResponse>Command with resultExtends CommandBase, implements ICommand<TResponse>
IQuery<TResult>Query interfaceExtends IDispatchAction<TResult>. Has same metadata as ICommand.
QueryBase<TResult>Query base classConstructor: QueryBase(Guid correlationId, string? tenantId = null)
ICommandHandler<TCommand>Command handlerExtends IActionHandler<TCommand> where TCommand : ICommand
ICommandHandler<TCommand, TResult>Command+result handlerExtends IActionHandler<TCommand, TResult>
IQueryHandler<TQuery, TResult>Query handlerExtends IActionHandler<TQuery, TResult> where TQuery : IQuery<TResult>

Excalibur Event Sourcing Types​

Event sourcing types are in Excalibur.EventSourcing.Abstractions.

TypeKindKey Members
IEventStoreEvent persistence3 methods: LoadAsync (2 overloads — full stream + from-version), AppendAsync (optimistic concurrency via expectedVersion). Takes aggregateId + aggregateType as explicit parameters.
IEventSourcedRepository<TAggregate, TKey>Repository5 methods: GetByIdAsync, SaveAsync (2 overloads: with/without ETag), ExistsAsync, DeleteAsync
IEventSourcedRepository<TAggregate>String-key shorthandExtends IEventSourcedRepository<TAggregate, string>
ISnapshotStoreSnapshot persistenceGetLatestAsync, SaveAsync
ISnapshotStrategyWhen to snapshotShouldCreateSnapshot(aggregate)

Message Type Decision Tree​

Need to DO something (no result)? → IDispatchAction (or ICommand for CQRS)
Need to DO + GET a result? → IDispatchAction<T> (or IQuery<T> / ICommand<T>)
Something HAPPENED (pub/sub)? → IDispatchEvent
Domain event with sourcing metadata? → IDomainEvent (extends IDispatchEvent)
Processing documents/batches? → IDispatchDocument

DI Registration​

Dispatch (messaging only)​

// Minimal: register pipeline + handlers
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});

The AddDispatch(Action<IDispatchBuilder>?) overload creates the pipeline, discovers handlers, and builds the configuration. When no handler assembly is named, both overloads discover handlers from the entry assembly — the application's own assembly, not the assembly that made the call. Handlers that live in a separate class library are not found this way; name that assembly explicitly with AddDispatch(typeof(SomeHandler).Assembly) or dispatch.AddHandlersFromAssembly(...).

Excalibur Event Sourcing​

builder.Services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.AddRepository<OrderAggregate, Guid>(key => new OrderAggregate(key))
.UseIntervalSnapshots(100)
.UseEventStore<SqlServerEventStore>();
}));

The IEventSourcingBuilder provides fluent configuration: AddRepository, UseEventStore, UseIntervalSnapshots, UseTimeBasedSnapshots, UseNoSnapshots, UseEventSerializer, UseTransactionalOutboxWriter, AddUpcastingPipeline, AddSnapshotUpgrading.

Excalibur Outbox​

builder.Services.AddExcalibur(excalibur => excalibur.AddOutbox(outbox =>
{
// Configure outbox via IOutboxBuilder
}));

Excalibur Saga​

builder.Services.AddExcalibur(excalibur => excalibur.AddSaga(saga =>
{
// Configure saga via ISagaBuilder
}));

Excalibur Data Services​

Data-access primitives (IDataRequest, IDb) are registered as part of the unified builder. Use services.AddExcalibur(...) with the relevant persistence providers (e.g. SQL Server via UseSqlServer(...) on the event sourcing builder) — the pre-unification AddExcaliburDataServices(...) / AddExcaliburDataServicesWithPersistence(...) aggregators have been removed.

Handler Patterns​

Dispatch Handlers (lightweight)​

using Excalibur.Dispatch;
using Excalibur.Dispatch.Delivery;

// 1. Command (no return value)
public record CreateOrder(string CustomerId, List<string> Items) : IDispatchAction;

public class CreateOrderHandler : IActionHandler<CreateOrder>
{
public async Task HandleAsync(CreateOrder action, CancellationToken cancellationToken)
{
// Process command...
}
}

// 2. Query (with return value)
public record GetOrder(Guid OrderId) : IDispatchAction<Order>;

public class GetOrderHandler : IActionHandler<GetOrder, Order>
{
public async Task<Order> HandleAsync(GetOrder action, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(action.OrderId, cancellationToken);
}
}

// 3. Event (multiple handlers, pub/sub)
[MessageName("Contoso.Orders.OrderPlaced")]
public record OrderPlaced(string OrderId) : IDispatchEvent;

public class OrderPlacedHandler : IEventHandler<OrderPlaced>
{
public Task HandleAsync(OrderPlaced eventMessage, CancellationToken cancellationToken)
{
// React to event...
return Task.CompletedTask;
}
}

CQRS Handlers (rich metadata)​

using Excalibur.Application.Requests.Commands;
using Excalibur.Application.Requests.Queries;

// Command with CQRS base class (adds CorrelationId, TenantId, transaction control)
public class PlaceOrder : CommandBase
{
public PlaceOrder(string customerId, Guid correlationId)
: base(correlationId) => CustomerId = customerId;

public string CustomerId { get; }
}

public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
public async Task HandleAsync(PlaceOrder action, CancellationToken cancellationToken)
{
// ICommandHandler<T> extends IActionHandler<T> — same signature
}
}

// Query with CQRS base class
public class GetOrderById : QueryBase<Order>
{
public GetOrderById(Guid orderId, Guid correlationId)
: base(correlationId) => OrderId = orderId;

public Guid OrderId { get; }
}

public class GetOrderByIdHandler : IQueryHandler<GetOrderById, Order>
{
public async Task<Order> HandleAsync(GetOrderById action, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(action.OrderId, cancellationToken);
}
}

Dispatching Messages​

// Inject IDispatcher via DI
private readonly IDispatcher _dispatcher;

// Context-less dispatch (context created automatically)
// Extension method from DispatcherContextExtensions (requires Excalibur.Dispatch package)
var result = await _dispatcher.DispatchAsync(new CreateOrder("cust-1", items), cancellationToken);

// Query dispatch
var result = await _dispatcher.DispatchAsync<GetOrder, Order>(
new GetOrder(orderId), cancellationToken);

// Check result
if (result.Succeeded)
return Ok(result.ReturnValue);
else
return Problem(result.ErrorMessage, statusCode: result.ProblemDetails?.Status);
Do not map a failed result to 400

A failed IMessageResult means your handler ran and reported a failure — not, by default, that the caller sent a bad request. Hard-coding BadRequest(...) reports a server-side fault to the caller as their own mistake, so they retry a request that can never succeed, or abandon one that would have.

result.ProblemDetails.Status carries the status the framework determined for that failure. With the pipeline's exception mapping configured (UseExceptionMapping()), a validation failure arrives as 400 and an authorization failure as 403; a handler that threw with nothing mapping it arrives as 500. When no status was determined, ProblemDetails is null and Problem(...) falls back to 500 — the safe direction, and never the caller's fault by accident.

The 2-parameter DispatchAsync(message, cancellationToken) extension methods reuse the current message context when one is available, and otherwise create a fresh root context. These are the recommended dispatch methods for most use cases.

A context is available inside a handler only when that handler declared it wants one — by taking IMessageContext as a settable property, or by injecting IMessageContextAccessor. A handler that declares neither runs on a faster path with no context established, so a nested DispatchAsync(message, cancellationToken) from inside it starts a new root rather than a child. When generating a handler that dispatches follow-up messages and needs the causal chain, inject IMessageContextAccessor — or pass the parent explicitly with DispatchAsync(message, context, cancellationToken).

Aggregate + Event Sourcing Pattern​

using Excalibur.Domain.Model;
using Excalibur.Dispatch;

// 1. Define domain events (every event declares its stable name)
[MessageName("Contoso.Orders.OrderCreated")]
public record OrderCreated(string OrderId, string CustomerId) : DomainEvent;

[MessageName("Contoso.Orders.ItemAdded")]
public record ItemAdded(string OrderId, string ItemId) : DomainEvent;

// 2. Define the aggregate
public class OrderAggregate : AggregateRoot
{
public string CustomerId { get; private set; } = string.Empty;
public List<string> Items { get; } = [];

public OrderAggregate(string id) => Id = id;

// Commands raise events
public void Create(string customerId)
{
RaiseEvent(new OrderCreated(Id, customerId));
}

public void AddItem(string itemId)
{
RaiseEvent(new ItemAdded(Id, itemId));
}

// Pattern-matching event application (no reflection, O(1))
protected override bool ApplyEventInternal(IDomainEvent domainEvent)
{
switch (domainEvent)
{
case OrderCreated e:
{
CustomerId = e.CustomerId;
return true;
}
case ItemAdded e:
{
Items.Add(e.ItemId);
return true;
}
default:
return false;
}
}
}

// 3. Define the command the handler accepts
public record PlaceOrder(string CustomerId) : IDispatchAction;

// 4. Use in a handler
public class PlaceOrderHandler : IActionHandler<PlaceOrder>
{
private readonly IEventSourcedRepository<OrderAggregate> _repository;

public PlaceOrderHandler(IEventSourcedRepository<OrderAggregate> repository)
=> _repository = repository;

public async Task HandleAsync(PlaceOrder action, CancellationToken cancellationToken)
{
var order = new OrderAggregate(Guid.NewGuid().ToString());
order.Create(action.CustomerId);

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

Key points:

  • RaiseEvent() records events AND applies them via ApplyEventInternal
  • LoadFromHistory() replays events to rebuild state (called by repository)
  • LoadFromSnapshot() restores from a snapshot, then replays subsequent events
  • GetUncommittedEvents() returns events raised since last save
  • MarkEventsAsCommitted() clears the uncommitted list after persistence

Common LLM Mistakes​

  1. Wrong handler namespace: Handlers are in Excalibur.Dispatch.Delivery, not Excalibur.Dispatch.
  2. Optional CancellationToken: Framework requires CancellationToken cancellationToken as required parameter (no = default).
  3. Missing ConfigureAwait: Library code must use await task.ConfigureAwait(false).
  4. Confusing Action vs Event: IDispatchAction routes to exactly 1 handler. IDispatchEvent supports multiple handlers (pub/sub).
  5. Explicit context creation: Most code should use the 2-parameter DispatchAsync(message, ct) extensions. Only create IMessageContext explicitly when you need to set correlation IDs or tenant context.
  6. DomainEvent is parameterless: DomainEvent uses virtual init properties with sensible defaults. Put event data in your record's own properties. Events carry no aggregate id or stream version — the aggregate id is passed to the event store and the version is store-assigned.
  7. Only ONE DomainEvent base: DomainEvent in Excalibur.Dispatch. The old DomainEventBase from Excalibur.Domain.Model was removed.
  8. No EntityFramework: This framework uses Dapper for SQL, not EF Core. Never suggest EF migrations or DbContext.
  9. Blocking async in Dispose: Use IAsyncDisposable with DisposeAsync(), never task.GetAwaiter().GetResult().
  10. Missing aggregate factory: AddRepository requires a factory function: es.AddRepository<MyAggregate>(key => new MyAggregate(key)).
  11. Missing [MessageName]: every registered event type must declare one — registration throws without it. Never write EventType; that member no longer exists. See stable message names.