LiteBus
Architecture

API Design Reference

This page is the concrete inventory for LiteBus public API shapes: named types, package placement, method signatures, and the v6 durable-messaging exemplar. The rules that govern every axis (suffix taxonomy, banned shapes, parameter budget, mapping boundary) live in AGENTS.md under API and value object design. Read that section first when adding or renaming types; use this page when you need examples and placement.

Who This Is For

  • Contributors adding public types to *.Abstractions packages
  • Reviewers checking whether a new API matches an existing pattern
  • Agents and maintainers aligning legacy surfaces during feature work

Prerequisites: Architecture (dependency role model), Dependency graph (package inventory).

Model Taxonomy (with Examples)

SuffixResponsibilityExamples
*ItemOne command unit (message payload + metadata)InboxAcceptItem, OutboxEnqueueItem
*MetadataPer-message durable annotations on an *ItemInboxAcceptMetadata, OutboxEnqueueMetadata
*RequestOperation input without a message bodyInboxLeaseRequest, TransportPublishRequest
*SettingsPer-invocation mediation tuningCommandMediationSettings, EventMediationSettings
*OptionsModule, host, processor, or adapter configurationProcessorOptions, PostgreSqlInboxStoreOptions
*HostOptionsBackground-service lifecycleInboxProcessorHostOptions
*FilterQuery or purge predicatesInboxMessageFilter
*BindingFramework adapter input at the HTTP or host edgeInboxQueryBinding, OutboxPurgeBinding
(no suffix)Small, shared value objectsSagaCorrelation, MessageIdentity

Mediation vs Durable Writers

  • Mediation keeps *Settings (CommandMediationSettings, EventMediationSettings, QueryMediationSettings) for per-call pipeline tuning. Custom mediators built on IMessageMediator use MessageMediationRequest<TMessage, TResult> for strategy and resolve overrides; CancellationToken stays on Mediate only, not inside the request bag.
  • Durable writers use *Item plus *Metadata. Scheduling and visibility belong on metadata (MessageVisibility), not separate scheduler interfaces. IInboxScheduler, IOutboxScheduler, InboxOptions, and OutboxOptions are removed in v6.
  • Framework bindings use *Binding for ASP.NET management and similar adapter input. Bindings map once at the adapter edge; they are not writer *Item types.

CLR Kind Selection

Public types use a consistent CLR kind per role. Full rename inventory: Migration guide v6.

KindUse in LiteBusDo not use for
sealed record*Item, *Metadata, *Filter, *Request, *Receipt, *Settings; immutable *Options with init only; static From / named factories on *ItemMutable host bags
record (non-sealed)Rare bases (ProcessorOptions, PostgreSqlSchemaStoreOptions)New public APIs unless inheritance is required
sealed class*HostOptions with get/set; modules, mediators, stores; optional thin construction namespaces (OutboxEnqueue); exceptionsNew immutable value objects
readonly struct / readonly record structInternal perf helpers and store keys onlyPublic contracts
interface*.Abstractions contracts:

*HostOptions stay sealed class: host and processor lifecycle bags remain mutable after instantiation (ProcessorHostOptions, InboxCleanupHostOptions, and similar).

Inbox/Outbox Naming Model

Durable writer vocabulary is message-centric on both axes. Do not use Payload as the public item body property; it collides with transport encryption wording (IPayloadEncryptor) and with metadata documented as outside the payload.

flowchart LR
  subgraph writerAPI [Writer API boundary CQRS]
    InboxAccept["IInbox.AcceptAsync"]
    OutboxEnqueue["IOutbox.EnqueueAsync"]
    TMessage["InboxAcceptItem of TMessage"]
    TEvent["OutboxEnqueueItem of TEvent"]
  end
  subgraph itemShape [Shared item shape]
    MessageProp["Message property on both"]
    MessageType["MessageType on untyped batch item"]
    Metadata["*Metadata variants"]
  end
  subgraph persistence [Persistence and query]
    Envelope["*Envelope"]
    Filter["*MessageFilter"]
    Receipt["*Receipt / Receipt of T"]
  end
  InboxAccept --> TMessage --> MessageProp
  OutboxEnqueue --> TEvent --> MessageProp
  MessageProp --> Envelope
  Envelope --> Filter
API shapeInboxOutbox
Typed itemInboxAcceptItem<TMessage> with MessageOutboxEnqueueItem<TEvent> with Message (renamed from Event)
Untyped batch itemInboxAcceptItemOutboxEnqueueItem with MessageType (renamed from EventType)
Typed accept/enqueue returnInboxReceipt<TMessage>OutboxReceipt<TEvent> (unchanged generic param name)
Batch returnInboxReceiptOutboxReceipt
Receipt outcomeInboxAcceptOutcomeOutboxEnqueueOutcome
Store append returnInboxAppendResultOutboxAppendResult
Default metadataInboxAcceptMetadata.ImmediateImmediate enqueue via metadata variants
Item order in sourceTyped first, untyped secondTyped first, untyped second (aligned in naming pass)

CQRS method names stay asymmetric (AcceptAsync vs EnqueueAsync; TMessage vs TEvent on generic items). Only the shared item shape (Message, MessageType, filters, envelopes) is symmetric.

Discriminated Value Objects (Durable Messaging)

Shared durable primitives live in LiteBus.Messaging.Abstractions.DurableMessaging:

TypeRole
MessageIdentityStable message id and correlation handles
IdempotencyIdempotency key and deduplication scope
MessageVisibilityDelayed or scheduled availability (visible_after)
MessageTraceDistributed trace context propagation
TenantScopeTenant partition for routing and lease filters
MessageContractReferenceContract name and version for persisted envelopes

Outbox adds PublicationTarget in LiteBus.Outbox.Abstractions for destination routing.

*Item types compose these value objects through *Metadata. Internal mappers (for example DurableEnvelopeMetadataMapper in LiteBus.Messaging) project metadata fields onto envelope columns. Receipt types (InboxReceipt, OutboxReceipt) expose MessageContractReference, MessageTrace, and TenantScope rather than duplicating primitive fields.

Writer Method Shape

Writer facades use a small, predictable surface. The canonical input is always *Item; optional body-only sugar overloads delegate to it.

Task<InboxReceipt<TMessage>> AcceptAsync<TMessage>(InboxAcceptItem<TMessage> item, CancellationToken cancellationToken)
    where TMessage : notnull;

Task<InboxReceipt<TMessage>> AcceptAsync<TMessage>(TMessage message, CancellationToken cancellationToken)
    where TMessage : notnull;

Task<IReadOnlyList<InboxReceipt>> AcceptBatchAsync(
    IReadOnlyList<InboxAcceptItem> items,
    CancellationToken cancellationToken);

Outbox mirrors the pattern with EnqueueAsync / EnqueueBatchAsync and OutboxEnqueueItem. Typed single-message paths return axis-specific receipts; the one canonical batch path accepts non-generic items and returns non-generic receipts. The redundant homogeneous EnqueueBatchAsync<TEvent> overload is not part of v6.

Append stores return InboxAppendResult or OutboxAppendResult, not an envelope alone. The result preserves whether the insert succeeded or resolved an existing message ID or tenant-scoped idempotency key. Writers map that store fact to InboxReceipt.Outcome or OutboxReceipt.Outcome; they never infer it by comparing envelope values.

  • The message body is the generic argument on *Item<TMessage> when typed; untyped batch paths use non-generic InboxAcceptItem.
  • CancellationToken is always the last parameter.
  • At most one sugar overload per writer operation family: message body only, no metadata scalars.
  • Batch methods accept IReadOnlyList<*Item>. Callers may use collection expressions ([item1, item2]); do not add no-op From(params *Item[]) helpers.
  • Store, lease, and processor APIs use *Request for operation inputs that are not full message accepts.
  • Custom append stores must return an axis-specific append result for every input slot. Batch results retain input order and mark later duplicates as AlreadyAccepted or AlreadyEnqueued.

Writer Construction

Build *Item values at the call site in this order of preference:

  1. Body-only default: sugar overload or *Item<T>.From(message) when metadata is *Metadata.Immediate.
  2. Named static on *Item<T>: OutboxEnqueueItem<T>.ScheduledAt(message, when) when a metadata combination is common and domain-named.
  3. with on item and metadata: full control without new API surface.
  4. Non-generic *Item: heterogeneous batch entries via OutboxEnqueueItem.From(message, messageType) (static on the untyped record).
  5. Thin namespace helper: OutboxEnqueue / InboxAccept only for glue that does not fit one record (avoid plural *Items names).

Do not use implicit conversions from the message body to *Item. Do not add writer overloads that take topic, idempotency key, visibility, or other metadata as separate parameters.

Package Placement

KindPackage
Shared durable value objectsLiteBus.Messaging.Abstractions (DurableMessaging namespace)
Axis-specific items, metadata, receiptsLiteBus.Inbox.Abstractions, LiteBus.Outbox.Abstractions
Mediation settingsLiteBus.Commands.Abstractions, LiteBus.Events.Abstractions, LiteBus.Queries.Abstractions
Module and host optionsFeature or adapter package that registers the service
Internal envelope projectionCore feature package (LiteBus.Messaging, LiteBus.Inbox, LiteBus.Outbox) as internal helpers

Abstractions packages own the public value-object types. Adapters map them; they do not define alternate option bags for the same concern.

Durable Messaging Exemplar

Accept (Inbox)

// Default metadata
await inbox.AcceptAsync(command, cancellationToken);

// Custom metadata
await inbox.AcceptAsync(
    InboxAcceptItem<PlaceOrderCommand>.From(
        command,
        InboxAcceptMetadata.Immediate with
        {
            Idempotency = Idempotency.Keyed("order-123"),
            Visibility = MessageVisibility.After(TimeSpan.FromMinutes(5)),
            Trace = MessageTrace.Correlated(correlationId),
            Tenant = TenantScope.Isolated(tenantId),
        }),
    cancellationToken);

Enqueue (Outbox)

// Default metadata
await outbox.EnqueueAsync(integrationEvent, cancellationToken);

// Topic and idempotency
await outbox.EnqueueAsync(
    OutboxEnqueueItem<OrderSubmittedIntegrationEvent>.WithTopic(integrationEvent, "orders.events") with
    {
        Metadata = OutboxEnqueueMetadata.Immediate with
        {
            Idempotency = Idempotency.Keyed(eventId),
        }
    },
    cancellationToken);

Feature guides (Inbox, Outbox, Transactional messaging writes) show end-to-end usage. Migration guide v6 documents the break from InboxOptions / OutboxOptions, scheduler interfaces, and the v6.0 rename tables (outbox Message property, package renames, and related tables).

Package Rename Reference (v6.0)

Legacy to v6.0 mapping for application upgrades. Shipping libraries use v6 names exclusively; see Migration guide v6 for the full inventory.

AreaLegacyTarget
Durable outbox writerIOutboxWriter.AddAsyncIOutbox.EnqueueAsync with OutboxEnqueueItem
In-process dispatchUseInProcessDispatcher()UseInProcessDispatch()
EF Core storageUseEfCoreStorage(...), EfCore*StoreOptionsUseEntityFrameworkCoreStorage(...), EntityFrameworkCore*StoreOptions
AWS SQS packages*.AwsSqsSqs NuGet IDs*.AwsSqs (for example LiteBus.Transport.AwsSqs, LiteBus.Inbox.Dispatch.AwsSqs, LiteBus.Inbox.Ingress.AwsSqs)
ASP.NET management*QueryParameters, *PurgeParameters*QueryBinding, *PurgeBinding
Mediation pipeline bagMessageMediationRequest with embedded CancellationTokenMessageMediationRequest<TMessage, TResult>; pass CancellationToken only on Mediate
Handler exceptionsMultipleCommandHandlerFoundException, MultipleMessageHandlerFoundException, MultipleQueryHandlerFoundExceptionMultipleHandlerFoundException
Outbox item bodyOutboxEnqueueItem.Event, untyped EventTypeMessage, MessageType
Batch construction helpersInboxAcceptItems, OutboxEnqueueItems(removed): static factories on InboxAcceptItem / OutboxEnqueueItem
Saga module extensionsInboxModuleBuilderExtensions (saga packages)InboxModuleBuilderSagaExtensions, InboxModuleBuilderPostgreSqlSagaExtensions
Event mediation settingsNested bags on EventMediationSettingsEventRoutingSettings, EventExecutionSettings, EventHandlerFilter
Testing doublesFake* in the former aggregate testing packageTest* in the matching concern package (for example TestCommandMediator in LiteBus.Testing.Mediation, TestInboxStore in LiteBus.Testing.DurableMessaging)

Ergonomics

  • Put From and domain-named factories on *Item records. Writer batch signatures stay on IReadOnlyList<*Item>; use collection expressions for batches.
  • One body-only sugar overload per writer operation family is allowed; see Writer construction.
  • Keep module-builder configuration on *Options types registered at compose time. Do not thread those options through accept or enqueue calls.
  • When a feature needs both mediation and durable persistence, call ICommandMediator.SendAsync with *Settings separately from ITransactionalOutbox.EnqueueAsync with OutboxEnqueueItem; do not merge the two concerns into one type.

Error Handler Context

Error handlers use HandleErrorAsync(MessageErrorContext<TMessage, TResult>, CancellationToken). The typed context shares Outcome and HandledResult with the mediation pipeline, so observing a failure leaves it unhandled while recovery is an explicit state transition. Cancellation is always the token supplied to the mediation call; error handlers do not depend on ambient token lookup.

Other aligned v6 surfaces include ILeaseRenewable.RenewLeaseAsync(LeaseRenewalRequest, ...) on inbox and outbox lease stores; ISagaStore.SaveAsync(SagaSaveItem<TState>, ...), SagaCompleteItem, SagaSaveItem.From, SagaCorrelation.SagaDefinitionId, and tenant-scoped saga primary keys. See Saga and Changelog.

Track broader roadmap items in Roadmap.

What This Does Not Cover

Next

On this page