Class TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse>
Opt-in pipeline behavior that, after a successful command, dispatches domain events from
every aggregate the unit of work tracked at commit time — regardless of the command's
response shape. Companion to DomainEventDispatchBehavior<TMessage, TResponse>,
which only fires for Result<TAggregate> responses.
public sealed class TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse> where TMessage : ICommand<TResponse> where TResponse : IResult
Type Parameters
TMessageThe command type. Must implement Mediator.ICommand<TResponse>.
TResponseThe command response. Must implement IResult.
- Inheritance
-
TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse>
- Implements
-
IPipelineBehavior<TMessage, TResponse>
- Inherited Members
- Extension Methods
Remarks
Registered via
AddTrackedAggregateDomainEventDispatch(IServiceCollection).
The opt-in extension removes any registered DomainEventDispatchBehavior<TMessage, TResponse>
to avoid double-dispatch for Result<TAggregate> handlers — auto-dispatch happens
here, sourced from CommittedAggregates.
Ordering. Runs OUTSIDE TransactionalCommandBehavior so it sees the post-commit
world. The opt-in registration handles the yank-and-reappend so this invariant holds
regardless of the consumer's call order for
AddTrellisUnitOfWork<TContext>().
Skips dispatch when:
- IsFailure is true — including
Result.FailAfterCommit. Persisted failures must dispatch events manually (typically by calling DispatchAggregateEventsAsync(IDomainEventPublisher, IAggregate, CancellationToken) before returningFailAfterCommit) or callAcceptChanges()to discard them; otherwise a later successful command in the same scope will auto-dispatch them. - The call is re-entrant — another tracked dispatch is already in progress on the same async flow. Domain-event handlers that send nested commands trigger this guard so the snapshot held by the outer behavior is not overwritten and outer events are not republished.
Re-entrant nested commands. A domain-event handler dispatched from this behavior is
free to call IMediator.Send. The nested command runs its own pipeline (including its
own TransactionalCommandBehavior) but the nested invocation of this behavior detects
re-entrancy and short-circuits. Callers that need fan-out from nested commands must dispatch
manually via DispatchAggregateEventsAsync(IDomainEventPublisher, IAggregate, CancellationToken).
Cascade detection. The behavior snapshots every tracked aggregate's pending events at
entry, dispatches only those snapshotted events, and then validates that each aggregate's
pending-event list still exactly matches its snapshot by length AND per-position reference
equality. If any handler changed the pending list (raised new events, cleared via
AcceptChanges, replaced, or reordered) on the originating aggregate or on any other
aggregate participating in the snapshot, DomainEventHandlerCascadedException
is thrown with every offending aggregate and AcceptChanges() is not called on any
aggregate.
Constructors
TrackedAggregateDomainEventDispatchBehavior(ITrackedAggregateSource, IDomainEventPublisher, ILogger<TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse>>)
Initializes a new instance of TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse>.
public TrackedAggregateDomainEventDispatchBehavior(ITrackedAggregateSource trackedAggregateSource, IDomainEventPublisher publisher, ILogger<TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse>> logger)
Parameters
trackedAggregateSourceITrackedAggregateSourceThe unit-of-work sidecar that exposes the aggregates tracked at the most recent successful commit.
publisherIDomainEventPublisherThe publisher used to fan out events to registered handlers.
loggerILogger<TrackedAggregateDomainEventDispatchBehavior<TMessage, TResponse>>The logger used to record dispatch diagnostics.
Exceptions
- ArgumentNullException
Thrown when any constructor argument is null.
Methods
Handle(TMessage, MessageHandlerDelegate<TMessage, TResponse>, CancellationToken)
public ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken)
Parameters
messageTMessagenextMessageHandlerDelegate<TMessage, TResponse>cancellationTokenCancellationToken
Returns
- ValueTask<TResponse>