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

AstraFlow mediator Pipeline (5 Behaviors)
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);
    }
}