.claude/skills/domain-event-dispatch/SKILL.md
Use when raising and handling domain events within a bounded context, decoupling domain logic from side-effect infrastructure (notifications, audit, read-model projections), or when multiple independent reactions to the same domain change are directly calling services from the entity or use case handler. Covers in-process dispatch via MediatR INotification, a manual IDomainEventDispatcher with IServiceProvider, and EF Core SaveChanges integration to dispatch events after the transaction commits. Domain: Domain-Driven Design, Architecture. Level: Intermediate. Tags: domain-events, ddd, mediatr, event-dispatch, decoupling.
npx skillsauth add klod68/littlerae domain-event-dispatchInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
When a domain operation occurs (order placed, user registered, inventory depleted), other parts of the system need to react: send a notification, update a read model, trigger a workflow. Directly calling those side-effect services from the domain entity or use case handler violates SRP and creates tight coupling.
Domain entities raise events. A dispatcher (MediatR or custom) publishes them to handlers. All handlers execute within the same transaction boundary.
/// <summary>Marker interface for domain events.</summary>
public interface IDomainEvent
{
DateTime OccurredAtUtc { get; }
}
public abstract record DomainEventBase : IDomainEvent
{
public DateTime OccurredAtUtc { get; init; } = DateTime.UtcNow;
}
public abstract class Entity
{
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void Raise(IDomainEvent domainEvent)
{
ArgumentNullException.ThrowIfNull(domainEvent);
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents() => _domainEvents.Clear();
}
public sealed record OrderPlaced(Guid OrderId, decimal Total) : DomainEventBase;
public class Order : Entity
{
public void Place()
{
Status = OrderStatus.Placed;
Raise(new OrderPlaced(Id, Total));
}
}
// Event inherits INotification:
public sealed record OrderPlaced(Guid OrderId, decimal Total)
: DomainEventBase, INotification;
// Handler:
internal sealed class SendOrderConfirmation : INotificationHandler<OrderPlaced>
{
public async Task Handle(OrderPlaced notification, CancellationToken ct)
{
// Send email, push notification, etc.
}
}
public interface IDomainEventDispatcher
{
Task DispatchAsync(IEnumerable<IDomainEvent> events, CancellationToken ct);
}
internal sealed class DomainEventDispatcher : IDomainEventDispatcher
{
private readonly IServiceProvider _serviceProvider;
public DomainEventDispatcher(IServiceProvider serviceProvider)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
_serviceProvider = serviceProvider;
}
public async Task DispatchAsync(IEnumerable<IDomainEvent> events, CancellationToken ct)
{
foreach (var domainEvent in events)
{
var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
var handlers = _serviceProvider.GetServices(handlerType);
foreach (var handler in handlers)
{
await ((dynamic)handler!).HandleAsync((dynamic)domainEvent, ct);
}
}
}
}
public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
Task HandleAsync(TEvent domainEvent, CancellationToken ct);
}
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
var entities = ChangeTracker.Entries<Entity>()
.Where(e => e.Entity.DomainEvents.Count > 0)
.Select(e => e.Entity)
.ToList();
var events = entities.SelectMany(e => e.DomainEvents).ToList();
entities.ForEach(e => e.ClearDomainEvents());
var result = await base.SaveChangesAsync(ct);
// Dispatch AFTER save — ensures the data is committed
await _dispatcher.DispatchAsync(events, ct);
return result;
}
SaveChangesAsync run in the same transaction. If a handler fails, the entire transaction rolls back. Design handlers to be idempotent or move them after commit.record types.tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.