원클릭으로
notification-handlers
Use when dispatching domain events via MediatR notifications with multiple handlers.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when dispatching domain events via MediatR notifications with multiple handlers.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when adding caching to .NET APIs or optimizing response times with distributed cache, output cache, or ETags.
Use when configuring API response formats, custom formatters, or Accept header handling.
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when building minimal API endpoints with route groups, filters, or TypedResults.
| name | notification-handlers |
| description | Use when dispatching domain events via MediatR notifications with multiple handlers. |
| metadata | {"category":"cqrs","agent":"ef-specialist","when-to-use":"When implementing domain event dispatch using MediatR notification handlers"} |
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