CQRS Pattern¶
Command Query Responsibility Segregation using MediatR.
Overview¶
CQRS separates read operations (Queries) from write operations (Commands):
| Type | Purpose | Returns | Side Effects |
|---|---|---|---|
| Query | Read data | Data | No |
| Command | Write data | Result/void | Yes |
File Structure¶
Features/{Feature}/
├── Commands/
│ ├── CreateFeatureCommand.cs
│ └── UpdateFeatureCommand.cs
├── Queries/
│ ├── GetFeatureQuery.cs
│ └── GetFeaturesQuery.cs
├── Models/
│ ├── FeatureRequest.cs
│ └── FeatureResponse.cs
└── Validators/
└── FeatureRequestValidator.cs
Command Example¶
public sealed record CreateMarkerCommand(CreateMarkerRequest Request)
: IRequest<MarkerResponse>, IRequestWithUserId, IRequestWithGroupId
{
public Guid UserId { get; set; }
public Guid GroupId { get; set; }
}
internal sealed class CreateMarkerCommandHandler(
DataContext context,
IDateTimeProvider dateTimeProvider)
: IRequestHandler<CreateMarkerCommand, MarkerResponse>
{
public async Task<MarkerResponse> Handle(
CreateMarkerCommand request,
CancellationToken cancellationToken)
{
var marker = new Marker
{
Title = request.Request.Title,
Latitude = request.Request.Latitude,
Longitude = request.Request.Longitude,
UserId = request.UserId,
GroupId = request.GroupId,
CreatedDate = dateTimeProvider.UtcNow
};
context.Markers.Add(marker);
await context.SaveChangesAsync(cancellationToken);
return marker.ToResponse();
}
}
Query Example¶
public sealed record GetMarkersQuery
: IRequest<List<MarkerResponse>>, IRequestWithGroupId, ICacheable
{
public Guid GroupId { get; set; }
public string CacheKey => CacheConstants.Markers.List(GroupId);
public TimeSpan CacheDuration => CacheConstants.DefaultDuration;
}
internal sealed class GetMarkersQueryHandler(DataContext context)
: IRequestHandler<GetMarkersQuery, List<MarkerResponse>>
{
public async Task<List<MarkerResponse>> Handle(
GetMarkersQuery request,
CancellationToken cancellationToken)
{
return await context.Markers
.Where(m => m.GroupId == request.GroupId)
.Where(m => !m.IsDeleted)
.Select(m => m.ToResponse())
.ToListAsync(cancellationToken);
}
}
Pipeline Behaviors¶
MediatR pipeline handles cross-cutting concerns:
flowchart TB
A[HTTP Request] --> B[Controller]
B --> C[MediatR]
C --> D[ValidationBehavior]
D -->|Invalid| X[400 Error]
D -->|Valid| E[UserIdBehavior]
E --> F[GroupIdBehavior]
F --> G[CachingBehavior]
G -->|Cache Hit| Y[Cached Response]
G -->|Cache Miss| H[Handler]
H --> I[Response] | Behavior | Purpose |
|---|---|
ValidationBehavior | Runs FluentValidation |
CachingBehavior | Caches query responses |
UserIdBehavior | Injects current user ID |
GroupIdBehavior | Injects current group ID |
Marker Interfaces¶
See Pipeline Behaviors for the full list of marker interfaces.
Related¶
- Validation — Request validation
- Caching — Response caching
- Architecture — System design