Domain event dispatch using MediatR notifications. Multiple handlers per event, dispatch after persistence, and idempotent handling. Trigger: domain event, notification, INotification, event handler, publish.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
OrderCreated, OrderShippedpublic interface IDomainEvent : INotification
{
DateTimeOffset OccurredAt { get; }
}
public abstract record DomainEvent : IDomainEvent
{
public DateTimeOffset OccurredAt { get; } = DateTimeOffset.UtcNow;
}
public sealed record OrderCreatedEvent(
Guid OrderId,
string CustomerName) : DomainEvent;
public sealed record OrderSubmittedEvent(
Guid OrderId,
decimal Total) : DomainEvent;
public sealed record OrderCancelledEvent(
Guid OrderId,
string Reason) : DomainEvent;
public sealed record OrderItemAddedEvent(
Guid OrderId,
Guid ProductId,
int Quantity) : DomainEvent;
public abstract class AggregateRoot
{
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyList<IDomainEvent> DomainEvents =>
_domainEvents.AsReadOnly();
protected void RaiseDomainEvent(IDomainEvent domainEvent)
=> _domainEvents.Add(domainEvent);
public void ClearDomainEvents() => _domainEvents.Clear();
}
public sealed class Order : AggregateRoot
{
public static Order Create(string customerName)
{
var order = new Order { CustomerName = customerName };
order.RaiseDomainEvent(
new OrderCreatedEvent(order.Id, customerName));
return order;
}
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can submit");
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
}
}
// Handler 1: Send confirmation email
internal sealed class SendOrderConfirmationOnCreated(
IEmailService emailService,
ILogger<SendOrderConfirmationOnCreated> logger)
: INotificationHandler<OrderCreatedEvent>
{
public async Task Handle(
OrderCreatedEvent notification, CancellationToken ct)
{
logger.LogInformation(
"Sending confirmation for order {OrderId}",
notification.OrderId);
await emailService.SendOrderConfirmationAsync(
notification.OrderId, notification.CustomerName, ct);
}
}
// Handler 2: Update dashboard stats
internal sealed class UpdateDashboardOnOrderCreated(AppDbContext db)
: INotificationHandler<OrderCreatedEvent>
{
public async Task Handle(
OrderCreatedEvent notification, CancellationToken ct)
{
var stats = await db.DashboardStats.SingleAsync(ct);
stats.IncrementOrderCount();
await db.SaveChangesAsync(ct);
}
}
// Handler 3: Reserve inventory
internal sealed class ReserveInventoryOnOrderSubmitted(
IInventoryService inventoryService)
: INotificationHandler<OrderSubmittedEvent>
{
public async Task Handle(
OrderSubmittedEvent notification, CancellationToken ct)
{
await inventoryService.ReserveForOrderAsync(
notification.OrderId, ct);
}
}
public sealed class DomainEventDispatcher(IPublisher publisher)
: SaveChangesInterceptor
{
public override async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken ct = default)
{
var context = eventData.Context!;
// Collect events from all aggregates
var aggregates = context.ChangeTracker
.Entries<AggregateRoot>()
.Select(e => e.Entity)
.Where(e => e.DomainEvents.Count > 0)
.ToList();
var events = aggregates
.SelectMany(a => a.DomainEvents)
.ToList();
// Clear events before dispatch to prevent re-dispatch
aggregates.ForEach(a => a.ClearDomainEvents());
// Dispatch each event
foreach (var domainEvent in events)
await publisher.Publish(domainEvent, ct);
return result;
}
}
// Registration
builder.Services.AddSingleton<DomainEventDispatcher>();
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.AddInterceptors(
sp.GetRequiredService<DomainEventDispatcher>());
});
internal sealed class ProcessOrderPaymentOnSubmitted(
AppDbContext db,
IPaymentService paymentService)
: INotificationHandler<OrderSubmittedEvent>
{
public async Task Handle(
OrderSubmittedEvent notification, CancellationToken ct)
{
// Idempotency check — skip if already processed
var alreadyProcessed = await db.PaymentRecords
.AnyAsync(p => p.OrderId == notification.OrderId, ct);
if (alreadyProcessed)
return;
await paymentService.ChargeAsync(
notification.OrderId, notification.Total, ct);
db.PaymentRecords.Add(new PaymentRecord
{
OrderId = notification.OrderId,
Amount = notification.Total,
ProcessedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync(ct);
}
}
// Alternative: dispatch events in the handler directly
internal sealed class SubmitOrderHandler(
IOrderRepository repository,
IUnitOfWork unitOfWork,
IPublisher publisher)
: IRequestHandler<SubmitOrderCommand, Result>
{
public async Task<Result> Handle(
SubmitOrderCommand request, CancellationToken ct)
{
var order = await repository.FindAsync(request.OrderId, ct);
if (order is null)
return Result.Failure(
Error.NotFound("Order.NotFound", "Order not found"));
order.Submit();
await unitOfWork.SaveChangesAsync(ct);
// Dispatch events after successful save
foreach (var domainEvent in order.DomainEvents)
await publisher.Publish(domainEvent, ct);
order.ClearDomainEvents();
return Result.Success();
}
}
INotification and INotificationHandler< implementationsIDomainEvent or DomainEvent base typesRaiseDomainEvent or AddDomainEvent in entitiesSaveChangesInterceptor that dispatches eventsIPublisher.Publish callsIDomainEvent interface extending INotification