CQRS Pattern
Command/Query Responsibility Segregation with SCRIPE mediator pipeline, behaviors, validation, and caching.
SCRIPE uses the CQRS (Command Query Responsibility Segregation) pattern to separate read and write operations. Commands mutate state and go through validation + audit behaviors. Queries read state and can leverage caching. SCRIPE mediator acts as the mediator between controllers and handlers.
What is CQRS?
CQRS separates your application into two sides: Commands (writes) and Queries (reads). Each side can be optimized independently commands focus on data integrity and validation, while queries focus on performance and caching.
Command Side (Write)
- Commands CHANGE state (Create, Update, Delete)
- Always return Result<T> or Result<Unit>
- Go through validation + audit behaviors
- Invalidate related caches on success
- Named: CreateXxxCommand, UpdateXxxCommand
Query Side (Read)
- Queries READ state (Get, List, Search)
- Return domain entities or DTOs
- Skip audit behavior (read-only)
- Can leverage caching
- Named: GetXxxQuery, ListXxxQuery
SCRIPE Mediator Pipeline
ISender.Send(command)
1. LoggingBehavior
2. ValidationBehavior
Valid
3. FeatureCheckBehavior
Allowed
4. WebhookDispatchBehavior
5. CachingBehavior
Cache miss / mutation
CommandHandler.Handle()
Result<T>
Validation Behavior
ValidationBehavior.csC#
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : AstraFlow.Mediator.IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
if (!_validators.Any()) return await next();
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, ct)));
var failures = results
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}Command Example
C#
public record CreateAdminCommand(
string Name,
string Email,
string Password,
Guid? TenantId
) : ICommand<AdminResponse>;Query Example
GetAdminByIdQuery + HandlerC#
// Query
public record GetAdminByIdQuery(Guid Id) : IQuery<AdminDetailResponse>;
// Handler
public class GetAdminByIdQueryHandler
: IQueryHandler<GetAdminByIdQuery, AdminDetailResponse>
{
private readonly IAdminRepository _repo;
private readonly AstraFlow.Mapper.IMapper _mapper;
private readonly ICacheService _cache;
public async Task<Result<AdminDetailResponse>> Handle(
GetAdminByIdQuery request, CancellationToken ct)
{
var cacheKey = $"admin:{request.Id}";
var cached = await _cache.GetAsync<AdminDetailResponse>(cacheKey);
if (cached != null) return Result.Success(cached);
var admin = await _repo.GetByIdWithDetailsAsync(request.Id, ct);
if (admin == null)
return Result<AdminDetailResponse>.Failure("Admin not found");
var response = _mapper.Map<AdminDetailResponse>(admin);
await _cache.SetAsync(cacheKey, response, TimeSpan.FromMinutes(5));
return Result<AdminDetailResponse>.Success(response);
}
}