Domain events
A notification is a message with zero, one, or many handlers and no return value. IDomainEvent is a
notification that also records when it happened — the DDD shape.
public record OrderCreatedEvent(Guid OrderId, string UserId) : IDomainEvent{ public DateTimeOffset OccurredOn { get; } = DateTimeOffset.UtcNow;}Handlers
Any number, each independent:
public class SendConfirmationEmail : INotificationHandler<OrderCreatedEvent>{ public ValueTask Handle(OrderCreatedEvent e, CancellationToken ct) { /* ... */ }}
public class UpdateInventory : INotificationHandler<OrderCreatedEvent>{ public ValueTask Handle(OrderCreatedEvent e, CancellationToken ct) { /* ... */ }}Publishing
await publisher.Publish(new OrderCreatedEvent(orderId, userId), ct);Publish is Mediant’s fastest path relative to MediatR — handlers are invoked directly, with no
per-handler wrapper object or closure. See Benchmarks.
Strategy
cfg.NotificationPublishStrategy = NotificationPublishStrategy.Sequential; // defaultcfg.NotificationPublishStrategy = NotificationPublishStrategy.Parallel;Sequential runs handlers in order and stops at the first exception. Parallel starts them all and
aggregates the failures. Pick Parallel only when handlers are genuinely independent — no shared
DbContext, no ordering assumptions.
Polymorphic notifications
cfg.EnablePolymorphicNotifications = true;Publishing OrderCreatedEvent now also invokes handlers registered for its base types and implemented
interfaces — for example an INotificationHandler<IDomainEvent> that writes every domain event to an
audit log. Off by default, because it changes which handlers run.
Reliable delivery: the transactional outbox
Publish is in-process and in-memory. If the process dies between committing the transaction and running
the handlers, the event is gone. For at-least-once delivery that survives a crash, enqueue the event into
the outbox inside your business transaction. A background processor publishes it after the commit and
retries failures.
builder.Services.AddMediantOutbox();public class CreateOrderHandler(AppDbContext db, IOutbox outbox) : ICommandHandler<CreateOrder, Result<Guid>>{ public async ValueTask<Result<Guid>> Handle(CreateOrder cmd, CancellationToken ct) { var order = Order.Create(cmd.UserId, cmd.Items); db.Orders.Add(order);
// Enqueued in the same transaction as the row. Both commit, or neither does. await outbox.EnqueueAsync(new OrderCreatedEvent(order.Id, cmd.UserId), ct);
return Result<Guid>.Success(order.Id); }}Post-commit work
Some work must happen after the transaction commits, but doesn’t need durability — sending an email,
warming a cache. Use IPostCommitTaskQueue rather than the outbox:
postCommit.Enqueue(ct => emailService.SendConfirmationAsync(cmd.Email, ct));The [Transactional] behavior drains the queue once the commit succeeds, and discards it if the
transaction rolls back.