skills/microservice/command/command-handler/SKILL.md
Use when implementing MediatR command handlers in event-sourced microservices.
npx skillsauth add faysilalshareef/dotnet-ai-kit command-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<TCommand> (void) or IRequestHandler<TCommand, TResponse> via MediatRICommitEventService and IUnitOfWork (for event loading and saving)IQueriesServices for cross-service gRPC queriesIRequest (MediatR) and a domain command interfaceIProblemDetailsProvider for gRPC error mappingCommands are records that implement both IRequest (MediatR) and a domain command interface.
using {Company}.{Domain}.Commands.Domain.Commands.Orders;
using MediatR;
namespace {Company}.{Domain}.Commands.Application.Features.Commands.Orders.CreateOrder;
public record CreateOrderCommand(
Guid Id,
Guid UserId,
string CustomerName,
decimal Total,
List<Guid> Items,
OrderStatus Status
) : ICreateOrderCommand, IRequest;
The domain command interface lives in the Domain layer:
namespace {Company}.{Domain}.Commands.Domain.Commands.Orders;
public interface ICreateOrderCommand
{
Guid Id { get; }
Guid UserId { get; }
string CustomerName { get; }
decimal Total { get; }
List<Guid> Items { get; }
OrderStatus Status { get; }
}
using {Company}.{Domain}.Commands.Application.Contracts.Repositories;
using {Company}.{Domain}.Commands.Application.Contracts.Services.BaseServices;
using {Company}.{Domain}.Commands.Domain.Exceptions.Orders;
using MediatR;
namespace {Company}.{Domain}.Commands.Application.Features.Commands.Orders.CreateOrder;
public class CreateOrderHandler(IUnitOfWork unitOfWork, ICommitEventService commitEventsService)
: IRequestHandler<CreateOrderCommand>
{
private readonly IUnitOfWork _unitOfWork = unitOfWork;
private readonly ICommitEventService _commitEventsService = commitEventsService;
public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var events = await _unitOfWork.Events
.GetAllByAggregateIdAsync(request.Id, cancellationToken);
if (events.Any())
throw new OrderAlreadyExistException();
var order = Order.Create(request);
await _commitEventsService.CommitNewEventsAsync(order);
}
}
Key details:
private readonly IUnitOfWork _unitOfWork = unitOfWork;Order.Create(request)_commitEventsService.CommitNewEventsAsync(order) -- passes the aggregate, not the eventsTask (void handler) -- no output DTO for simple createsusing {Company}.{Domain}.Commands.Application.Contracts.Repositories;
using {Company}.{Domain}.Commands.Application.Contracts.Services.BaseServices;
using {Company}.{Domain}.Commands.Domain.Exceptions.Orders;
using MediatR;
namespace {Company}.{Domain}.Commands.Application.Features.Commands.Orders.AddItems;
public class AddItemsHandler(ICommitEventService commitEventsService, IUnitOfWork _unitOfWork)
: IRequestHandler<AddItemsToOrderCommand>
{
private readonly ICommitEventService _commitEventsService = commitEventsService;
public async Task Handle(AddItemsToOrderCommand command, CancellationToken cancellationToken)
{
var events = await _unitOfWork.Events
.GetAllByAggregateIdAsync(command.OrderId, cancellationToken);
if (!events.Any())
throw new OrderNotFoundException(command.UserId);
var order = Order.LoadFromHistory(events);
order.AddItems(command);
await _commitEventsService.CommitNewEventsAsync(order);
}
}
Key details:
NotFoundException (not found guard)Order.LoadFromHistory(events) (static method)namespace {Company}.{Domain}.Commands.Application.Features.Commands.Orders.RegisterOrder;
public class RegisterOrderHandler(
ICommitEventService commitEventsService,
IUnitOfWork _unitOfWork,
IQueriesServices _queriesServices)
: IRequestHandler<RegisterOrderCommand, RegisterOrderOutput>
{
private readonly ICommitEventService _commitEventsService = commitEventsService;
public async Task<RegisterOrderOutput> Handle(
RegisterOrderCommand command, CancellationToken cancellationToken)
{
// Cross-service query for validation
var customerInfo = await _queriesServices.GetCustomerInfoAsync(command.CustomerId);
var events = await _unitOfWork.Events
.GetAllByAggregateIdAsync(command.OrderId, cancellationToken);
if (!events.Any())
throw new OrderNotFoundException(command.UserId);
var order = Order.LoadFromHistory(events);
order.Register(command);
await _commitEventsService.CommitNewEventsAsync(order);
return new RegisterOrderOutput(order.Id, order.Sequence);
}
}
public record RegisterOrderOutput(Guid Id, int Sequence);
The gRPC service layer maps protobuf requests to MediatR commands:
public override async Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
var userId = context.GetUserId(); // from metadata/claims
var command = new CreateOrderCommand(
Id: Guid.Parse(request.Id),
UserId: userId,
CustomerName: request.CustomerName,
Total: request.Total,
Items: request.Items.Select(Guid.Parse).ToList(),
Status: (Domain.Enums.OrderStatus)request.Status
);
await _mediator.Send(command);
return new CreateOrderResponse { Message = Phrases.OrderCreated };
}
Handlers follow a feature-folder structure:
Application/
Features/
Commands/
Orders/
CreateOrder/
CreateOrderCommand.cs
CreateOrderHandler.cs
AddItems/
AddItemsToOrderCommand.cs
AddItemsHandler.cs
UpdateOrder/
UpdateOrderCommand.cs
UpdateOrderHandler.cs
Invoices/
GenerateInvoice/
GenerateInvoiceCommand.cs
GenerateInvoiceHandler.cs
| Anti-Pattern | Correct Approach |
|---|---|
| Business logic in handler | Delegate to aggregate methods |
| new Order() in handler | Use Order.Create(command) or Order.LoadFromHistory(events) |
| Passing events list to commit | Pass the aggregate: CommitNewEventsAsync(order) |
| Returning aggregate from handler | Return void or output DTO only |
| Catching and swallowing exceptions | Let exceptions propagate to gRPC interceptor |
| Creating DbContext transactions in handler | CommitEventService handles SaveChangesAsync internally |
| Injecting ApplicationDbContext in handler | Use IUnitOfWork for data access |
# Find command handlers
grep -r "IRequestHandler<.*Command" --include="*.cs" src/Application/
# Find commit calls
grep -r "CommitNewEventsAsync" --include="*.cs" src/Application/
# Find aggregate creation in handlers
grep -r "\.Create(" --include="*.cs" src/Application/Features/
# Find LoadFromHistory in handlers
grep -r "LoadFromHistory" --include="*.cs" src/Application/Features/
# Find handler file structure
find src/Application/Features/Commands -name "*Handler.cs"
Application/Features/Commands/{Entity}/{Action}/IRequest and domain command interfaceICommitEventService and IUnitOfWorkPhrases.xxx) for response messages and exception messagesIProblemDetailsProvider for automatic gRPC status code mappingdata-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.