Agent skill
cqrs-mediatr-guidelines
CQRS (Command Query Responsibility Segregation) patterns with MediatR for ISLAMU Event. Covers commands, queries, handlers, validation, and pipeline behaviors.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/cqrs-mediatr-guidelines-islamu-ngo-explore
SKILL.md
CQRS + MediatR Guidelines
π― Purpose
Provides best practices for implementing CQRS (Command Query Responsibility Segregation) using MediatR in ISLAMU Event project. Ensures consistent, testable, and maintainable application logic.
β‘ When This Skill Activates
Triggered by:
- Keywords: "command", "query", "handler", "mediatr", "cqrs", "validation", "validator"
- Intent patterns: "create feature", "add endpoint", "implement use case"
- File patterns:
**/*Command.cs,**/*Query.cs,**/*Handler.cs,**/*Validator.cs - Content patterns:
IRequest,IRequestHandler,AbstractValidator
π CQRS Pattern Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CQRS with MediatR β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β WRITE OPERATIONS (Commands) β
β ββββββββββββββββββββββββββββ β
β βββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
β β Controller ββββΆβ CreateEvent ββββΆβ Event β β
β β or Page β β Command β β Created β β β
β βββββββββββββββ β β ββββββββββββββββ β
β β β’ Mutates state β β
β β β’ Returns ID β β
β β β’ Validated β β
β βββββββββββββββββββ β
β β
β READ OPERATIONS (Queries) β
β βββββββββββββββββββββββββ β
β βββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
β β Controller ββββΆβ GetEventList ββββΆβ EventDto[] β β
β β or Page β β Query β β (Read-only) β β
β βββββββββββββββ β β ββββββββββββββββ β
β β β’ Read-only β β
β β β’ Returns DTOs β β
β β β’ No mutations β β
β βββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Key Principles
- Separation: Commands (write) and Queries (read) are separate
- Single Responsibility: One handler per request
- Class Requests: Commands/Queries are classes (not records)
- Validation: FluentValidation at Application boundary
- Thin Controllers: Controllers just send requests to MediatR
- CancellationToken: Always pass to async methods
- Repository Returns Entities: Handlers map entities to DTOs
- Validators Use Manual Instantiation: Validators are instantiated in handlers, NOT injected via DI
π Resources
| Resource | Description |
|---|---|
| command-patterns.md | Command structure, naming, handlers |
| query-patterns.md | Query structure, pagination, projections |
| handler-patterns.md | Handler implementation, DI, error handling |
| validation-integration.md | FluentValidation pipeline integration |
| complete-examples.md | End-to-end feature examples |
β‘ Quick Reference
Create a New Feature (Command + Query)
Step 1: Command (Write Operation)
// File: Explore.Application/Features/Events/Requests/Commands/CreateEventCommand.cs
namespace Explore.Application.Features.Events.Requests.Commands;
using MediatR;
using Explore.Application.DTOs.Event;
public class CreateEventCommand : IRequest<BaseCommandResponse<Guid>>
{
public CreateEventDto EventDto { get; set; }
}
Step 2: Command Validator
Real Example from CreateEventDtoValidator.cs:
// File: Explore.Application/DTOs/Event/Validators/CreateEventDtoValidator.cs
using FluentValidation;
using Explore.Application.Contracts.Persistence;
public class CreateEventDtoValidator : AbstractValidator<CreateEventDto>
{
private readonly IAudienceAgeRepository _audienceAgeRepository;
private readonly IAudienceGenderRepository _audienceGenderRepository;
private readonly IEventTypeRepository _eventTypeRepository;
private readonly IActorRepository _actorRepository;
private readonly IStorageObjectRepository _storageObjectRepository;
public CreateEventDtoValidator(
IAudienceAgeRepository audienceAgeRepository,
IAudienceGenderRepository audienceGenderRepository,
IEventTypeRepository eventTypeRepository,
IActorRepository actorRepository,
IStorageObjectRepository storageObjectRepository)
{
_audienceAgeRepository = audienceAgeRepository;
_audienceGenderRepository = audienceGenderRepository;
_eventTypeRepository = eventTypeRepository;
_actorRepository = actorRepository;
_storageObjectRepository = storageObjectRepository;
// Standard validation rules
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title is required")
.MaximumLength(500);
RuleFor(x => x.Description)
.MaximumLength(5000);
// Foreign key validation with repository
RuleFor(x => x.AudienceAgeId)
.NotEmpty().WithMessage("Audience Age is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _audienceAgeRepository.Exists(id);
return exists;
})
.WithMessage("Audience Age not found");
RuleFor(x => x.AudienceGenderId)
.NotEmpty().WithMessage("Audience Gender is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _audienceGenderRepository.Exists(id);
return exists;
})
.WithMessage("Audience Gender not found");
RuleFor(x => x.EventTypeId)
.NotEmpty().WithMessage("Event Type is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _eventTypeRepository.Exists(id);
return exists;
})
.WithMessage("Event Type not found");
RuleFor(x => x.ActorId)
.NotEmpty().WithMessage("Actor is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _actorRepository.Exists(id);
return exists;
})
.WithMessage("Actor not found");
// Optional FK validation
RuleFor(x => x.FeaturedImage)
.MustAsync(async (id, cancellation) =>
{
if (!id.HasValue) return true;
var exists = await _storageObjectRepository.Exists(id.Value);
return exists;
})
.WithMessage("Featured Image not found");
}
}
Step 3: Command Handler
Real Example from Explore.Application/Features/Events/Handlers/Commands/CreateEventCommandHandler.cs:
namespace Explore.Application.Features.Events.Handlers.Commands;
using System.Linq;
using System.Threading;
using System.Threading.Task;
using AutoMapper;
using Explore.Application.Contracts.Persistence;
using Explore.Application.DTOs.Event.Validators;
using Explore.Application.Features.Events.Requests.Commands;
using Explore.Application.Responses;
using Explore.Domain;
using MediatR;
public class CreateEventCommandHandler : IRequestHandler<CreateEventCommand, BaseCommandResponse<Guid>>
{
private readonly IEventRepository _eventRepository;
private readonly IAudienceAgeRepository _audienceAgeRepository;
private readonly IAudienceGenderRepository _audienceGenderRepository;
private readonly IEventTypeRepository _eventTypeRepository;
private readonly IActorRepository _actorRepository;
private readonly IStorageObjectRepository _storageObjectRepository;
private readonly IMapper _mapper;
public CreateEventCommandHandler(
IEventRepository eventRepository,
IAudienceAgeRepository audienceAgeRepository,
IAudienceGenderRepository audienceGenderRepository,
IEventTypeRepository eventTypeRepository,
IActorRepository actorRepository,
IStorageObjectRepository storageObjectRepository,
IMapper mapper)
{
_eventRepository = eventRepository;
_audienceAgeRepository = audienceAgeRepository;
_audienceGenderRepository = audienceGenderRepository;
_eventTypeRepository = eventTypeRepository;
_actorRepository = actorRepository;
_storageObjectRepository = storageObjectRepository;
_mapper = mapper;
}
public async Task<BaseCommandResponse<Guid>> Handle(CreateEventCommand request, CancellationToken cancellationToken)
{
var response = new BaseCommandResponse<Guid>();
// CRITICAL: Validate using FluentValidation - Validator instantiated manually with dependencies
var validator = new CreateEventDtoValidator(
_audienceAgeRepository,
_audienceGenderRepository,
_eventTypeRepository,
_actorRepository,
_storageObjectRepository);
var validationResult = await validator.ValidateAsync(request.EventDto);
if (!validationResult.IsValid)
{
response.Success = false;
response.Message = "Event creation failed.";
response.Errors = validationResult.Errors.Select(e => e.ErrorMessage).ToList();
return response;
}
// Map DTO to Entity
var @event = _mapper.Map<Event>(request.EventDto);
@event.TotalViews = 0; // Set non-mapped properties
// Save through repository
@event = await _eventRepository.Create(@event);
response.Success = true;
response.Id = @event.Id;
response.Message = "Event created successfully.";
return response;
}
}
Step 4: Query (Read Operation)
// File: Explore.Application/Features/Events/Requests/Queries/GetEventListRequest.cs
namespace Explore.Application.Features.Events.Requests.Queries;
using MediatR;
public class GetEventListRequest : IRequest<List<EventListDto>>
{
}
Step 5: Query Handler
Real Example from Explore.Application/Features/Events/Handlers/Queries/GetEventListRequestHandler.cs:
namespace Explore.Application.Features.Events.Handlers.Queries;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Explore.Application.Contracts.Persistence;
using Explore.Application.DTOs.Event;
using Explore.Application.Features.Events.Requests.Queries;
using MediatR;
public class GetEventListRequestHandler : IRequestHandler<GetEventListRequest, List<EventListDto>>
{
private readonly IEventRepository _eventRepository;
private readonly IMapper _mapper;
public GetEventListRequestHandler(IEventRepository eventRepository, IMapper mapper)
{
_eventRepository = eventRepository;
_mapper = mapper;
}
public async Task<List<EventListDto>> Handle(GetEventListRequest request, CancellationToken cancellationToken)
{
// Repository returns ENTITIES
var events = await _eventRepository.GetEventsWithDetails();
// AutoMapper maps ENTITIES to DTOs
return _mapper.Map<List<EventListDto>>(events);
}
}
Step 6: Controller (Thin)
Real Example from Explore.API/Controllers/EventController.cs:
namespace Explore.API.Controllers;
using Explore.Application.DTOs.Event;
using Explore.Application.Features.Events.Requests.Commands;
using Explore.Application.Features.Events.Requests.Queries;
using Explore.Application.Responses;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Route("api/v1/[controller]")]
[ApiController]
public class EventController : ControllerBase
{
private readonly IMediator _mediator;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<EventController> _logger;
public EventController(
IMediator mediator,
IHttpContextAccessor httpContextAccessor,
ILogger<EventController> logger)
{
_mediator = mediator;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
// GET: api/v1/event
[HttpGet]
[EndpointSummary("Get all Events (Conference, Webinar, Workshop ...)")]
[EndpointDescription("Get A List of all the Events")]
[AllowAnonymous]
public async Task<ActionResult<List<EventListDto>>> GetAll()
{
var events = await _mediator.Send(new GetEventListRequest());
return Ok(events);
}
// GET: api/v1/event/{id}
[HttpGet("{id}")]
[EndpointSummary("Get Event Details")]
[EndpointDescription("Get Details of the Event")]
[AllowAnonymous]
public async Task<ActionResult<EventDto>> GetById(Guid id)
{
var @event = await _mediator.Send(new GetEventDetailsRequest { Id = id });
return Ok(@event);
}
// POST: api/v1/event
[HttpPost]
[EndpointSummary("Create an Event")]
[EndpointDescription("Create a new event")]
[Authorize]
public async Task<ActionResult<BaseCommandResponse<Guid>>> Create([FromBody] CreateEventDto @event)
{
var command = new CreateEventCommand { EventDto = @event };
var response = await _mediator.Send(command);
return Ok(response);
}
// PUT: api/v1/event/{id}
[HttpPut("{id}")]
[EndpointSummary("Update an Event")]
[EndpointDescription("Update an existing event")]
[Authorize]
public async Task<ActionResult<BaseCommandResponse<Guid>>> Update(Guid id, [FromBody] UpdateEventDto @event)
{
if (id != @event.Id)
{
return BadRequest(new { error = "Event ID mismatch" });
}
var command = new UpdateEventCommand { EventDto = @event };
var response = await _mediator.Send(command);
if (!response.Success)
{
return BadRequest(response);
}
return Ok(response);
}
// DELETE: api/v1/event/{id}
[HttpDelete("{id}")]
[EndpointSummary("Delete an Event")]
[EndpointDescription("Delete an event (only if user owns the organization)")]
[Authorize]
public async Task<ActionResult> Delete(Guid id)
{
try
{
var userId = _httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value
?? _httpContextAccessor.HttpContext?.User?.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value
?? _httpContextAccessor.HttpContext?.User?.FindFirst("sid")?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized(new { error = "User ID not found in token" });
}
var command = new DeleteEventCommand { Id = id, UserId = userId };
var result = await _mediator.Send(command);
if (!result)
{
return NotFound(new { error = "Event not found or you don't have permission to delete it" });
}
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting event {EventId}", id);
return StatusCode(500, new { error = ex.Message });
}
}
}
β Do's
- β DO use classes (not records) for Commands/Queries
- β
DO suffix with
CommandorQuery - β
DO suffix handlers with
Handler - β
DO pass
CancellationTokento all async methods - β DO use repositories that return entities (not DTOs)
- β DO validate inputs with FluentValidation
- β DO keep handlers focused (Single Responsibility)
- β DO use AutoMapper for entity β DTO mapping
- β
DO use
[AllowAnonymous]for GET endpoints - β
DO use
[Authorize]for POST/PUT/DELETE - β DO instantiate validators manually with dependencies (NOT DI inject)
- β
DO implement
IDisposablefor event cleanup
β Don'ts
- β DON'T use records (use classes instead)
- β DON'T return entities from queries (use DTOs)
- β DON'T put business logic in controllers
- β DON'T use
IRequestwithout a response type - β DON'T forget
CancellationToken - β DON'T use
.Resultor.Wait()(useawait) - β DON'T query in commands (use repositories)
- β DON'T mutate state in queries
- β DON'T throw exceptions for validation (use FluentValidation)
- β DON'T extract userId without fallback pattern (sub β nameidentifier β sid)
- β DON'T inject validators via DI (instantiate manually with dependencies)
π MediatR Pipeline
Request
β
βΌ
[Pre-Processors] β Audit logging
β
βΌ
[Pipeline Behaviors] β Validation (FluentValidation)
β β Logging
βΌ β Performance monitoring
[Handler] β Your business logic
β
βΌ
[Post-Processors] β Caching
β
βΌ
Response
π Deep Dive
For comprehensive guidance:
- Command Patterns: command-patterns.md
- Query Patterns: query-patterns.md
- Handler Patterns: handler-patterns.md
- Validation: validation-integration.md
- Complete Examples: complete-examples.md
Related Skills:
clean-architecture-rules- Ensures handlers are in correct layerdotnet-efcore-guidelines- Database access patterns for handlersbackend-dev-guidelines- Overall backend architectureblazor-mudblazor-guidelines- Blazor component patterns
Enforcement Level: π‘ SUGGEST (Provides guidance, doesn't block)
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?