Inline sequence validation for idempotent event handlers. Covers the exact guard pattern using Sequence != @event.Sequence - 1, returning true for already-processed duplicates, and false for out-of-order gaps. No helper class — logic is inlined in each handler. Trigger: sequence check, idempotent, event ordering, gap detection.
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.
int Sequence starting at 1, incrementing by 1 per aggregateint Sequence { get; private set; }if (entity.Sequence != @event.Sequence - 1)true tells the listener to CompleteMessage (processed or already done)false tells the listener to AbandonMessage (retry later)Sequence to @event.SequenceThe sequence check is a single if statement that handles both duplicates and gaps:
if (entity.Sequence != @event.Sequence - 1)
return entity.Sequence >= @event.Sequence;
This single expression covers TWO cases:
true (idempotent)false (retry)public async Task<bool> Handle(
Event<OrderUpdatedData> @event,
CancellationToken cancellationToken)
{
var order = await _unitOfWork.Orders.FindAsync(@event.AggregateId);
// Entity not found — creation event hasn't been processed yet
if (order is null)
return false;
// THE CORE GUARD: check sequence is exactly next expected
if (order.Sequence != @event.Sequence - 1)
return order.Sequence >= @event.Sequence;
// entity.Sequence >= event.Sequence -> true (already processed, complete msg)
// entity.Sequence < event.Sequence - 1 -> false (gap, abandon msg)
// Process: call entity behavior method (which sets Sequence internally)
order.UpdateDetails(@event.Data, @event.Sequence);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
Creation handlers do NOT use the sequence guard. They check for existence instead:
public async Task<bool> Handle(
Event<OrderCreatedData> @event,
CancellationToken cancellationToken)
{
var order = await _unitOfWork.Orders.FindAsync(@event.AggregateId);
// Already exists — idempotent, return true
if (order is not null)
return true;
order = new Order(@event); // Constructor sets Sequence = @event.Sequence
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
The entity behavior method is where Sequence gets updated:
// In the entity class
public void UpdateDetails(OrderUpdatedData data, int sequence)
{
CustomerName = data.CustomerName;
Email = data.Email;
Total = data.Total;
Sequence = sequence; // <-- Sequence updated to @event.Sequence
}
Some entities use IncrementSequence() after calling the behavior method:
if (product.Sequence == @event.Sequence - 1)
{
product.Update(@event);
product.IncrementSequence(); // Sequence++
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
return product.Sequence >= @event.Sequence;
Condition | Return | Message Action | Why
---------------------------------------|--------|----------------|--------------------
entity is null (update event) | false | Abandon | Creation not yet processed
entity.Sequence == @event.Sequence - 1 | true | Complete | Next in order, process it
entity.Sequence >= @event.Sequence | true | Complete | Already processed (idempotent)
entity.Sequence < @event.Sequence - 1 | false | Abandon | Gap — missing intermediate events
entity is not null (creation event) | true | Complete | Already created (idempotent)
Returning true for already-processed events is critical because:
CompleteMessageAsync when handler returns truefalse, the message would be abandoned and retried forevertrue = "I have handled this (or it was already handled), move on"Returning false for sequence gaps causes:
AbandonMessageAsync| Anti-Pattern | Correct Approach |
|---|---|
SequenceChecker helper class | Inline the check in each handler |
if (@event.Sequence <= entity.Sequence) return true | if (entity.Sequence != @event.Sequence - 1) return entity.Sequence >= @event.Sequence |
| Separate if statements for duplicate and gap | Single combined expression |
| Throwing on sequence mismatch | Return bool — let listener decide action |
| Not updating Sequence after processing | Behavior method must set Sequence = sequence |
# Find the core sequence guard pattern
grep -r "Sequence != @event.Sequence - 1" --include="*.cs" Application/
# Find Sequence >= comparison for idempotency
grep -r "Sequence >= @event.Sequence" --include="*.cs" Application/
# Find entity Sequence property
grep -r "public int Sequence" --include="*.cs" Domain/Entities/
if (entity.Sequence != @event.Sequence - 1) return entity.Sequence >= @event.Sequence;Sequence to the new valuetrue is returned (not reprocessed)false is returned (abandoned for retry)