بنقرة واحدة
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