Class TransactionalCommandBehavior<TMessage, TResponse>
Pipeline behavior that automatically commits staged changes after a successful command handler.
The behavior wraps each command invocation in an Trellis.IUnitOfWork.BeginScope() scope, runs the handler (which stages changes via repositories), then calls CommitAsync(CancellationToken) if the handler returned success. If the handler returned a failure Result<TValue>, no commit occurs and the staged changes are discarded when the underlying store/context is disposed.
Persist-on-failure outcomes. If the response implements IPersistOnFailure
and the per-instance flag is true (the canonical producer is
FailAfterCommit<TValue>(Error)), the commit step also runs even though the
result is a failure. This enables the worker-handler pattern of persisting a
permanently_failed state row alongside the failure outcome. On commit failure for a
persist-on-failure outcome, the commit error replaces the handler error in the returned result.
This behavior is constrained to Mediator.ICommand<TResponse> messages — queries are not wrapped and incur no overhead.
Atomicity: the unit of work persists all staged changes within a single handler atomically
(the shipped EF Core adapter does so through the DbContext's implicit transaction). Cross-aggregate
operations that share the same unit of work are automatically transactional.
Nested-command semantics. When a command handler dispatches another command via
IMediator, the inner command flows through the same pipeline and the inner
TransactionalCommandBehavior<TMessage, TResponse> opens a nested
Trellis.IUnitOfWork.BeginScope(). The inner CommitAsync(CancellationToken) defers
(returns success without touching the store) until the outermost scope unwinds. This
prevents a successful inner command from committing a partially-completed outer command's
staged changes. Caveat: if the inner command returns a failure but the outer handler
ignores it and returns success, the outer's commit will persist any changes the inner staged
before failing — handlers that need to discard inner failures' staged work must detach the
affected entities themselves. Every Trellis.IUnitOfWork implementation must implement
Trellis.IUnitOfWork.BeginScope() with depth-aware semantics; the shipped EF Core unit of
work does this with an internal depth counter.
public sealed class TransactionalCommandBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse> where TMessage : ICommand<TResponse> where TResponse : IResult, IFailureFactory<TResponse>
Type Parameters
TMessageThe command type.
TResponseThe result type, constrained to IResult and IFailureFactory<TSelf>.
- Inheritance
-
TransactionalCommandBehavior<TMessage, TResponse>
- Implements
-
IPipelineBehavior<TMessage, TResponse>
- Inherited Members
- Extension Methods
Constructors
TransactionalCommandBehavior(IUnitOfWork)
Initializes a new instance of TransactionalCommandBehavior<TMessage, TResponse>.
public TransactionalCommandBehavior(IUnitOfWork unitOfWork)
Parameters
unitOfWorkIUnitOfWorkThe scoped unit of work used to commit staged changes.
Exceptions
- ArgumentNullException
Thrown when
unitOfWorkis 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>
Remarks
The handler is committed when the result is successful, or when the result opts in to post-handler persistence via PersistOnFailure = true (typically a FailAfterCommit<TValue>(Error) outcome from a worker handler that stages a "permanently_failed" row). On commit failure for a persist-on-failure outcome, the commit error replaces the handler error — the caller must learn that the persist-on-failure guarantee was not honored.