MediatR command handlers for event-sourced microservices. Covers IRequestHandler pattern, aggregate loading/creation via IUnitOfWork and ICommitEventService, command records with domain command interfaces, and gRPC service mapping. Trigger: command handler, MediatR, CQRS command, business logic.
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.
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 mapping