skills/microservice/query/event-handler/SKILL.md
Use when implementing query-side event handlers with sequence checking and idempotency.
npx skillsauth add faysilalshareef/dotnet-ai-kit event-handlerInstall 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.
IRequestHandler<Event<TData>, bool> via MediatRtrue = message processed or already processed (CompleteMessage)false = cannot process yet (AbandonMessage, retry from Service Bus)_unitOfWork.Orders)handler(IUnitOfWork unitOfWork)true if duplicate (idempotent)entity.Sequence != @event.Sequence - 1_unitOfWork.SaveChangesAsync(cancellationToken)namespace {Company}.{Domain}.Query.Application.Features.Events;
public class OrderCreatedHandler(IUnitOfWork unitOfWork)
: IRequestHandler<Event<OrderCreatedData>, bool>
{
private readonly IUnitOfWork _unitOfWork = unitOfWork;
public async Task<bool> Handle(
Event<OrderCreatedData> @event,
CancellationToken cancellationToken)
{
var order = await _unitOfWork.Orders.FindAsync(@event.AggregateId);
// Already exists — idempotent, return true to complete message
if (order is not null)
return true;
// Create entity from event
order = new Order(@event);
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
}
namespace {Company}.{Domain}.Query.Application.Features.Events;
public class OrderUpdatedHandler(IUnitOfWork unitOfWork)
: IRequestHandler<Event<OrderUpdatedData>, bool>
{
private readonly IUnitOfWork _unitOfWork = unitOfWork;
public async Task<bool> Handle(
Event<OrderUpdatedData> @event,
CancellationToken cancellationToken)
{
var order = await _unitOfWork.Orders.FindAsync(@event.AggregateId);
// Entity not yet created — return false to abandon and retry
if (order is null)
return false;
// Sequence check: must be exactly current + 1
if (order.Sequence != @event.Sequence - 1)
return order.Sequence >= @event.Sequence;
// Already processed -> true (idempotent)
// Gap/out-of-order -> false (retry)
// Apply state change via behavior method
order.UpdateDetails(@event.Data, @event.Sequence);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
}
namespace {Company}.{Domain}.Query.Application.Features.Events;
public class OrderStatusChangedHandler(IUnitOfWork unitOfWork)
: IRequestHandler<Event<OrderStatusChangedData>, bool>
{
private readonly IUnitOfWork _unitOfWork = unitOfWork;
public async Task<bool> Handle(
Event<OrderStatusChangedData> @event,
CancellationToken cancellationToken)
{
var order = await _unitOfWork.Orders.FindAsync(@event.AggregateId);
if (order is null)
return false;
if (order.Sequence != @event.Sequence - 1)
return order.Sequence >= @event.Sequence;
order.ChangeStatus(@event.Data, @event.Sequence);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
}
namespace {Company}.{Domain}.Query.Application.Features.Events;
public class ProductCreatedHandler(IUnitOfWork unitOfWork)
: IRequestHandler<Event<ProductCreatedData>, bool>
{
private readonly IUnitOfWork _unitOfWork = unitOfWork;
public async Task<bool> Handle(
Event<ProductCreatedData> @event,
CancellationToken cancellationToken)
{
var product = await _unitOfWork.Products.FindAsync(@event.AggregateId);
if (product is not null)
return true;
product = Product.Create(@event);
await _unitOfWork.Products.AddAsync(product);
// Add related entities to their own repositories
await _unitOfWork.ProductVariants.AddRangeAsync(
@event.Data.Variants.Select(v => new ProductVariant(v, @event.AggregateId)));
await _unitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
}
Some entities use IncrementSequence() instead of passing sequence to behavior:
public async Task<bool> Handle(
Event<ProductUpdatedData> @event,
CancellationToken cancellationToken)
{
var product = await _unitOfWork.Products.FindAsync(@event.AggregateId);
if (product != null)
{
if (product.Sequence == @event.Sequence - 1)
{
product.Update(@event);
product.IncrementSequence();
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
return product.Sequence >= @event.Sequence;
}
return false;
}
| Scenario | Return | Why |
|---|---|---|
| Entity created successfully | true | Complete message |
| Entity already exists (creation) | true | Idempotent — already processed |
| Entity updated successfully | true | Complete message |
| Already processed (Sequence >= event) | true | Idempotent duplicate |
| Entity not found (update event) | false | Abandon — creation event not yet processed |
| Sequence gap (out of order) | false | Abandon — wait for missing events |
| Anti-Pattern | Correct Approach |
|---|---|
| unitOfWork.Repository<T>() generic | Named properties: _unitOfWork.Orders |
| Throwing exceptions in handlers | Return false for retry, true for skip |
| if (@event.Sequence <= order.Sequence) | if (order.Sequence != @event.Sequence - 1) return order.Sequence >= @event.Sequence; |
| Separate sequence check and return | Combine into single conditional expression |
| Direct DbContext usage | Use IUnitOfWork with named repository properties |
| SaveChangesAsync() without cancellation token | SaveChangesAsync(cancellationToken) — always pass cancellation token |
# Find event handlers returning bool
grep -r "IRequestHandler<Event<.*>, bool>" --include="*.cs" Application/
# Find sequence checking pattern
grep -r "Sequence != @event.Sequence - 1" --include="*.cs" Application/
# Find UnitOfWork usage
grep -r "IUnitOfWork" --include="*.cs" Application/Features/Events/
{EventType}HandlerIUnitOfWork unitOfWork parameterprivate readonly IUnitOfWork _unitOfWork = unitOfWork;Application/Features/Events/{Aggregate}/ directoryAddMediatR)true if duplicatedata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.