Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jordiaragonzaragoza/JordiAragonZaragoza.SharedKernel/llms.txt

Use this file to discover all available pages before exploring further.

SharedKernel implements the Command Query Responsibility Segregation (CQRS) pattern by providing a set of marker interfaces and handler contracts built on top of MediatR. Every command and query returns an Ardalis.Result value, giving callers a consistent, exception-free way to handle success, validation failures, not-found cases, and authorization errors without throwing exceptions across layer boundaries.

Commands

A command expresses intent to change state. SharedKernel ships two command shapes — one that returns a typed result and one that returns a plain Result (void-equivalent).

ICommand<TResponse> and ICommand

// Command with a typed response (e.g. returns the new aggregate Id)
public interface ICommand<TResponse> : IRequest<Result<TResponse>>, ITransactionalCommand
    where TResponse : notnull
{
}

// Pure DDD command — no return value beyond success/failure
public interface ICommand : IRequest<Result>, ITransactionalCommand
{
}
Both interfaces extend ITransactionalCommand, which tells the RequestUnitOfWorkService pipeline behavior to wrap execution in a database transaction automatically.

ITransactionalCommand and INonTransactionalCommand

// Marker — command runs inside a Unit of Work / transaction
public interface ITransactionalCommand
{
}

// Command that operates on a single aggregate whose boundary IS the unit of work.
// ⚠️ Domain events must be published by the database infrastructure when using these.
public interface INonTransactionalCommand<TResponse> : IRequest<Result<TResponse>>
    where TResponse : notnull
{
}

public interface INonTransactionalCommand : IRequest<Result>
{
}
Use INonTransactionalCommand only when the aggregate persistence layer (e.g. an event store with built-in transactional outbox) acts as its own unit of work. With standard EF Core repositories, always prefer ICommand.

ICommandHandler<TCommand, TResponse> and ICommandHandler<TCommand>

public interface ICommandHandler<TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
    where TCommand : ICommand<TResponse>
    where TResponse : notnull
{
}

public interface ICommandHandler<TCommand> : IRequestHandler<TCommand, Result>
    where TCommand : ICommand
{
}
Corresponding non-transactional handlers follow the same pattern:
public interface INonTransactionalCommandHandler<TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
    where TCommand : INonTransactionalCommand<TResponse>
    where TResponse : notnull
{
}

public interface INonTransactionalCommandHandler<TCommand> : IRequestHandler<TCommand, Result>
    where TCommand : INonTransactionalCommand
{
}

ICommandBus

Dispatch commands through ICommandBus rather than injecting IMediator directly. This keeps your application services decoupled from MediatR.
public interface ICommandBus
{
    Task<Result> SendAsync(ICommand command, CancellationToken cancellationToken = default);

    Task<Result<TResponse>> SendAsync<TResponse>(ICommand<TResponse> command, CancellationToken cancellationToken = default)
        where TResponse : notnull;
}

Queries

A query reads state without modifying it. Queries are never wrapped in a unit of work.

IQuery<TResponse>

public interface IQuery<TResponse> : IRequest<Result<TResponse>>
{
}

IQueryHandler<TQuery, TResponse>

public interface IQueryHandler<TQuery, TResponse> : IRequestHandler<TQuery, Result<TResponse>>
    where TQuery : IQuery<TResponse>
{
}

IQueryBus

public interface IQueryBus
{
    Task<Result<TResponse>> SendAsync<TResponse>(IQuery<TResponse> query, CancellationToken cancellationToken = default);
}

Complete Examples

Define the command:
public sealed record CreateOrderCommand(
    Guid CustomerId,
    IEnumerable<OrderLineDto> Lines) : ICommand<Guid>;
Implement the handler:
public sealed class CreateOrderCommandHandler
    : ICommandHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _repository;

    public CreateOrderCommandHandler(IOrderRepository repository)
        => _repository = repository;

    public async Task<Result<Guid>> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(request.CustomerId, request.Lines);
        await _repository.AddAsync(order, cancellationToken);

        return Result<Guid>.Success(order.Id);
    }
}
Send via ICommandBus:
var command = new CreateOrderCommand(customerId, lines);
Result<Guid> result = await _commandBus.SendAsync(command, cancellationToken);

if (!result.IsSuccess)
{
    // handle validation / not-found / forbidden etc.
}

Guid newOrderId = result.Value;

Paginated Queries

IPaginatedQuery

Implement IPaginatedQuery on any query that returns a paged collection:
public interface IPaginatedQuery
{
    int PageNumber { get; }
    int PageSize { get; }
}
Example:
public sealed record GetOrdersQuery(int PageNumber, int PageSize)
    : IQuery<PaginatedCollectionOutputDto<OrderOutputDto>>, IPaginatedQuery;

BasePaginatedQueryValidator<TQuery>

Extend BasePaginatedQueryValidator<TQuery> to get free PageNumber ≥ 0 and PageSize ≥ 0 validation rules baked in:
public sealed class GetOrdersQueryValidator
    : BasePaginatedQueryValidator<GetOrdersQuery>
{
    public GetOrdersQueryValidator()
    {
        // Additional rules for this query can go here.
        RuleFor(x => x.PageSize).LessThanOrEqualTo(100);
    }
}
BasePaginatedQueryValidator<TQuery> inherits from AbstractValidator<TQuery> and automatically adds:
  • PageNumber must be >= 0
  • PageSize must be >= 0

IPaginatedSpecification<TReadModel>

For repository queries driven by Ardalis.Specification:
public interface IPaginatedSpecification<TReadModel> : ISpecification<TReadModel>
{
    IPaginatedQuery Request { get; }
}

Read Models and Output DTOs

BaseReadModel

All application-layer read models inherit from BaseReadModel:
public abstract record class BaseReadModel : IReadModel
{
    protected BaseReadModel(Guid id) { ... }

    public Guid Id { get; private set; }
    public uint Version { get; set; }
}
Example:
public sealed record OrderReadModel(Guid Id, Guid CustomerId, decimal Total)
    : BaseReadModel(Id);

PaginatedCollectionOutputDto<T>

The standard return type for paginated queries:
public record class PaginatedCollectionOutputDto<T>(
    int ActualPage,
    int TotalPages,
    int TotalItems,
    IEnumerable<T> Items);

ISanitizableRequest

Requests that contain sensitive data (passwords, tokens, PII) should implement ISanitizableRequest so that pipeline behaviors log a scrubbed representation instead of the raw values:
public interface ISanitizableRequest
{
    /// <summary>
    /// Returns a secure representation of the request with sensitive fields removed or anonymized.
    /// </summary>
    object GetSanitized();
}
Example — hide a password field from logs:
public sealed record ChangePasswordCommand(
    Guid UserId,
    string NewPassword) : ICommand, ISanitizableRequest
{
    public object GetSanitized()
        => new { UserId, NewPassword = "***" };
}
RequestLoggerService, RequestValidationService, RequestExceptionHandlerService, and RequestPerformanceTrackingService all check for ISanitizableRequest before serializing the request to the log. You only need to implement the interface once per request type.

Build docs developers (and LLMs) love