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;
Define the query:public sealed record GetOrderByIdQuery(Guid OrderId)
: IQuery<OrderOutputDto>;
Implement the handler:public sealed class GetOrderByIdQueryHandler
: IQueryHandler<GetOrderByIdQuery, OrderOutputDto>
{
private readonly IOrderReadRepository _readRepository;
public GetOrderByIdQueryHandler(IOrderReadRepository readRepository)
=> _readRepository = readRepository;
public async Task<Result<OrderOutputDto>> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
var order = await _readRepository.GetByIdAsync(request.OrderId, cancellationToken);
if (order is null)
return Result<OrderOutputDto>.NotFound($"Order {request.OrderId} was not found.");
return Result<OrderOutputDto>.Success(order.ToDto());
}
}
Send via IQueryBus:var query = new GetOrderByIdQuery(orderId);
Result<OrderOutputDto> result = await _queryBus.SendAsync(query, cancellationToken);
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.