Skip to main content

Excalibur LLM Quick Reference

For AI Coding Agents

This page is optimized for LLM coding agents (Cursor, Copilot, Claude Code, etc.). 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 8+ 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.Dispatch.SecurityAuthentication, encryption, signing
Excalibur.Dispatch.TestingTest utilities and fakes

Dispatch Core Types

All message types are in namespace Excalibur.Dispatch.Abstractions. Handler types are in Excalibur.Dispatch.Abstractions.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, AggregateId, Version, OccurredAt, EventType, Metadata.
DomainEventBase recordAbstract record implementing IDomainEvent with auto-generated EventId (UUID v7). Namespace: Excalibur.Dispatch.Abstractions.
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, IsSuccess, 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<IDomainEvent>), LoadFromSnapshot(ISnapshot), GetUncommittedEvents(), MarkEventsAsCommitted()
AggregateRootString-key shorthandExtends AggregateRoot<string>
EntityBase<TKey>Entity baseAbstract Key property, equality by type + key
EntityBaseString-key shorthandExtends EntityBase<string>
ValueObjectBaseValue object baseAbstract GetEqualityComponents(), component-based equality, ==/!= operators
DomainEventBaseDomain event baseAbstract record. Auto-generates EventId (GUID), OccurredAt (UTC), EventType (class name). Override AggregateId. Namespace: Excalibur.Domain.Model.
ISnapshotSnapshot interfaceSnapshotId, AggregateId, AggregateType, Version, CreatedAt, Data
Two DomainEvent base types
  • DomainEvent in Excalibur.Dispatch.Abstractions — constructor takes (aggregateId, version), uses UUID v7 EventId
  • DomainEventBase in Excalibur.Domain.Model — parameterless, override AggregateId property, uses standard GUID EventId

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, Kind, 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 persistence5 methods: LoadAsync (2 overloads), AppendAsync (optimistic concurrency via expectedVersion), GetUndispatchedEventsAsync, MarkEventAsDispatchedAsync
IEventSourcedRepository<TAggregate, TKey>Repository7 methods: GetByIdAsync, SaveAsync (2 overloads: with/without ETag), ExistsAsync, DeleteAsync, QueryAsync<TQuery>, FindAsync<TQuery>
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. Without a builder parameter, AddDispatch() scans the calling assembly.

Excalibur Event Sourcing

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

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

Excalibur Outbox

builder.Services.AddExcaliburOutbox(outbox =>
{
// Configure outbox via IOutboxBuilder
});

Excalibur Saga

builder.Services.AddExcaliburSaga(saga =>
{
// Configure saga via ISagaBuilder
});

Excalibur Data Services

builder.Services.AddExcaliburDataServices(); // Dapper + JSON config
builder.Services.AddExcaliburDataServicesWithPersistence(configuration); // + persistence providers

Handler Patterns

Dispatch Handlers (lightweight)

using Excalibur.Dispatch.Abstractions;
using Excalibur.Dispatch.Abstractions.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)
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.IsSuccess)
return Ok(result.ReturnValue);
else
return BadRequest(result.ErrorMessage);

The 2-parameter DispatchAsync(message, cancellationToken) extension methods use ambient context (MessageContextHolder.Current) or create a new one automatically. These are the recommended dispatch methods for most use cases.

Aggregate + Event Sourcing Pattern

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

// 1. Define domain events
public record OrderCreated(string OrderId, string CustomerId) : DomainEventBase
{
public override string AggregateId => OrderId;
}

public record ItemAdded(string OrderId, string ItemId) : DomainEventBase
{
public override string AggregateId => OrderId;
}

// 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 void ApplyEventInternal(IDomainEvent domainEvent)
{
switch (domainEvent)
{
case OrderCreated e:
CustomerId = e.CustomerId;
break;
case ItemAdded e:
Items.Add(e.ItemId);
break;
}
}
}

// 3. 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.Abstractions.Delivery, not Excalibur.Dispatch.Abstractions.
  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. Wrong DomainEvent constructor: DomainEvent(aggregateId, version) takes the aggregate ID and version, not event-specific data. Put event data in your record's own properties.
  7. Confusing the two DomainEvent bases: DomainEvent (Dispatch.Abstractions, constructor args) vs DomainEventBase (Domain.Model, property overrides). Both implement IDomainEvent.
  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)).