Skip to content

Give each domain event type one dispatcher that owns run-all, aggregation and cancellation, so a cancelled publish stops and propagates unwrapped - #434

Merged
Vulthil merged 1 commit into
mainfrom
fix/domain-event-dispatch-cancellation
Oct 2, 2026
Merged

Vulthil merged 1 commit into
mainfrom
fix/domain-event-dispatch-cancellation

Conversation

@Vulthil

@Vulthil Vulthil commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Summary

Each domain event type now has one internal dispatcher that owns the whole dispatch contract, and a cancelled publish stops and propagates unwrapped.

Before, the contract was split: DomainEventPublisher ran the handler loop and aggregated failures, and NotificationHandlerWrapper<T> resolved the handlers and nested the pipeline handlers, joined by a Func parameter that only ever received one value. The loop's catch (Exception) also caught OperationCanceledException, so after the caller's token fired the remaining handlers kept running, and the caller got an AggregateException instead of a cancellation. In the outbox relay that AggregateException passed the shutdown filter: every message dispatched during a graceful stop was logged as an error, counted in the failed metric, and the relay kept dispatching the rest of the batch, so handlers that ignore the token ran their side effects during shutdown (and again later, because the cancelled record step rolls the batch back).

  • DomainEventDispatcher<TEvent> (internal; replaces NotificationHandlerWrapper<T>) resolves the event's handlers and pipeline handlers, runs the pipeline handlers in registration order around the whole set of handlers, and runs the handlers in registration order.
  • Cancellation rule: when the caller's token is cancelled, no further handler starts and the OperationCanceledException propagates unwrapped, even after an earlier handler failed, because the caller abandons the whole publish. A handler's own OperationCanceledException while the token is still live is an ordinary failure. This matches the delivery dispatcher's rule.
  • Failure contract (unchanged): a failed handler does not stop the others; once every handler has run, the failures are thrown together in one AggregateException, even when only one handler failed.
  • DomainEventPublisher keeps only the untyped and typed entry points and the per-type cache. The one-value Func seam and the NotificationHandlerExecutor record are deleted.
  • Docs: the IDomainEventPublisher XML docs state the contract (with the exceptions it throws), and docs/articles/domain-modeling.md gets a short paragraph.

No public API change.

Verification

  • dotnet build Vulthil.SharedKernel.slnx: 0 warnings, 0 errors.
  • All 18 unit test projects pass on net10.0 and net9.0. New tests:
    • 9 in DomainEventPublisherTests, through the publisher's methods with real DI: a later handler still runs after a failure; several failures come in one AggregateException in handler order; a cancelled token stops the handlers and comes through unwrapped; a cancellation in the last handler comes through unwrapped; a cancellation after a failed handler still wins; a handler's own cancellation while the token is live is a failure; an already cancelled token runs no handler; pipeline handlers wrap the whole set of handlers; the event's runtime type picks the handlers.
    • 1 in OutboxRelayCycleTests, with the real publisher and DomainEventOutboxDispatcher: a cancelled domain-event dispatch stops the relay cycle with nothing recorded, no second message dispatched and nothing logged.
  • Vulthil.IntegrationTests (Docker): 70/70 on net10.0 and net9.0.
  • Aspire MessagingIntegrationTests: 13/13 on net10.0.
  • dotnet pack package validation against 1.2.0 passes for Vulthil.SharedKernel.Application.
  • Break checks (each made tests fail, then reverted):
    • the old catch-all: 1 publisher test and the relay test fail;
    • no stop check before each handler: 2 tests fail;
    • a single failure thrown unwrapped: 2 tests fail;
    • stopping at the first failure: 4 tests fail.

Backport to v1.0: no

…tion and cancellation, so a cancelled publish stops and propagates unwrapped
@Vulthil
Vulthil merged commit d5f5997 into main Oct 2, 2026
7 checks passed
@Vulthil
Vulthil deleted the fix/domain-event-dispatch-cancellation branch October 2, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant