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.

Pipeline behaviors are MediatR’s equivalent of middleware: each behavior wraps the next handler in the chain, forming an ordered pipeline that applies cross-cutting concerns — such as logging, validation, and transaction management — to every command or query without polluting individual handler implementations. SharedKernel provides eight purpose-built pipeline services and registers them automatically via two extension methods on IServiceCollection.

Registration

Call the appropriate extension method when configuring your service container. Both methods live in ApplicationDependencyInjection:
// Program.cs / Startup.cs
services.AddSharedKernelApplicationCommandBus();
services.AddSharedKernelApplicationQueryBus();
AddSharedKernelApplicationCommandBus() registers all eight services (including RequestUnitOfWorkService). AddSharedKernelApplicationQueryBus() registers seven services — everything except RequestUnitOfWorkService, because queries must never modify state.
You still need to register MediatR itself (e.g., services.AddMediatR(...)) and provide concrete implementations of IUnitOfWork, ICacheService, IIdentityService, and IUserContextService in your infrastructure layer.

Pipeline Services

Interface: IRequestLoggerService
Class: RequestLoggerService
Logs every incoming request at Debug level before it reaches the handler. The log entry includes the request type name, the current user ID (from IUserContextService), and the serialized request body. If the request implements ISanitizableRequest, the sanitized representation is serialized instead of the raw object.
public interface IRequestLoggerService
{
    Task LogRequestAsync<TRequest>(TRequest request, CancellationToken cancellationToken = default)
        where TRequest : notnull;
}
Log output (structured):
Request: CreateOrderCommand  User ID: "user-123"  Request Data: {"customerId":"...","lines":[...]}
Runs first in the pipeline, before any other concern.
Interface: IRequestExceptionHandlerService
Class: RequestExceptionHandlerService
Wraps the rest of the pipeline in a try/catch. On any unhandled exception it logs at Error level — including the request name, user ID, and sanitized request data — and then re-throws, letting the presentation layer’s exception middleware decide on the HTTP status code.
public interface IRequestExceptionHandlerService
{
    Task<TResponse> ExecuteWithExceptionHandlingAsync<TRequest, TResponse>(
        TRequest request,
        Func<CancellationToken, Task<TResponse>> next,
        CancellationToken cancellationToken)
        where TRequest : notnull;
}
Log output on failure (structured):
Unhandled Exception. Request: CreateOrderCommand  User ID: "user-123"  Request Data: {...}
Interface: IRequestValidationService<TRequest, TResponse>
Class: RequestValidationService<TRequest, TResponse>
Resolves all registered IValidator<TRequest> implementations from the DI container and runs them concurrently. If any validators produce failures, the service returns Result.Invalid(errors) (or Result<T>.Invalid(errors)) immediately — the handler is never invoked. Validation failures are logged at Information level with the full list of broken rules.
public interface IRequestValidationService<TRequest, TResponse>
    where TRequest : notnull
    where TResponse : IResult
{
    Task<TResponse?> TryValidateAsync(TRequest request, CancellationToken cancellationToken = default);
}
Register a validator the standard FluentValidation way:
public sealed class CreateOrderCommandValidator
    : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId).NotEmpty();
        RuleFor(x => x.Lines).NotEmpty();
    }
}
MediatR’s DI scanning will pick it up automatically when you call services.AddValidatorsFromAssembly(...).
Interface: IRequestAuthorizationService<TRequest, TResponse>
Class: RequestAuthorizationService<TRequest, TResponse>
Reads [Authorize] attributes from the request class and enforces them via IIdentityService. Supports both role-based and policy-based authorization:
  • If the user is anonymous, returns Result.Unauthorized().
  • If the user lacks a required role, returns Result.Forbidden().
  • If the user fails a named policy check, returns Result.Forbidden().
public interface IRequestAuthorizationService<TRequest, TResponse>
    where TRequest : notnull
    where TResponse : IResult
{
    Task<TResponse?> TryAuthorizeAsync(TRequest request, CancellationToken cancellationToken = default);
}
The [Authorize] attribute:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class AuthorizeAttribute : Attribute
{
    public string Roles { get; set; } = string.Empty;   // comma-delimited
    public string Policy { get; set; } = string.Empty;
}
Usage on a command:
[Authorize(Roles = "Admin,OrderManager")]
public sealed record DeleteOrderCommand(Guid OrderId) : ICommand;
Policy-based usage:
[Authorize(Policy = "CanManageOrders")]
public sealed record CancelOrderCommand(Guid OrderId) : ICommand;
Interface: IRequestCachingService
Class: RequestCachingService
Activated only for requests that implement ICacheRequest. On the first call the handler executes and the result is stored in ICacheService. Subsequent calls with the same cache key return the cached value directly, bypassing the handler entirely.
public interface IRequestCachingService
{
    Task<TResponse> HandleWithCacheAsync<TRequest, TResponse>(
        TRequest request,
        Func<CancellationToken, Task<TResponse>> next,
        CancellationToken cancellationToken)
            where TRequest : ICacheRequest
            where TResponse : notnull;
}
ICacheRequest contract:
public interface ICacheRequest
{
    string CacheKey { get; }
    TimeSpan? AbsoluteExpirationInSeconds { get; }
}
Add caching to a query:
public sealed record GetOrderByIdQuery(Guid OrderId)
    : IQuery<OrderOutputDto>, ICacheRequest
{
    public string CacheKey => $"orders:{OrderId}";
    public TimeSpan? AbsoluteExpirationInSeconds => TimeSpan.FromMinutes(5);
}
Interface: IRequestInvalidateCachingService
Class: RequestInvalidateCachingService
Activated only for requests that implement IInvalidateCacheRequest. After the handler runs successfully, all cache entries whose key starts with the given prefix are removed via ICacheService.RemoveByPrefixAsync.
public interface IRequestInvalidateCachingService
{
    Task<TResponse> HandleAndInvalidateCacheAsync<TRequest, TResponse>(
        TRequest request,
        Func<CancellationToken, Task<TResponse>> next,
        CancellationToken cancellationToken)
            where TRequest : IInvalidateCacheRequest
            where TResponse : notnull;
}
IInvalidateCacheRequest contract:
public interface IInvalidateCacheRequest
{
    string PrefixCacheKey { get; }
}
Invalidate the orders cache after a mutation:
public sealed record CreateOrderCommand(
    Guid CustomerId,
    IEnumerable<OrderLineDto> Lines)
    : ICommand<Guid>, IInvalidateCacheRequest
{
    public string PrefixCacheKey => "orders:";
}
Interface: IRequestUnitOfWorkService
Class: RequestUnitOfWorkService
Wraps command handlers that implement ITransactionalCommand (i.e., those using ICommand or ICommand<TResponse>) in a database transaction via IUnitOfWork.ExecuteInTransactionAsync. The service also translates two domain exceptions into typed Result values so they never surface as HTTP 500 errors:
  • NotFoundExceptionResult.NotFound(...)
  • BusinessRuleValidationExceptionResult.Invalid(...)
public interface IRequestUnitOfWorkService
{
    Task<TResponse> HandleWithTransactionAsync<TResponse>(
        Func<CancellationToken, Task<TResponse>> next,
        CancellationToken cancellationToken = default)
        where TResponse : IResult;
}
The IUnitOfWork interface it delegates to:
public interface IUnitOfWork
{
    Task<TResponse> ExecuteInTransactionAsync<TResponse>(
        Func<Task<TResponse>> operation,
        CancellationToken cancellationToken = default)
        where TResponse : IResult;
}
RequestUnitOfWorkService is not registered by AddSharedKernelApplicationQueryBus(). Queries are always non-transactional.
Interface: IRequestPerformanceTrackingService
Class: RequestPerformanceTrackingService
Times every request with a Stopwatch. After the handler completes it logs at Debug level with elapsed milliseconds, request name, user ID, and both the sanitized request and response values. If execution exceeds 1 500 ms it additionally logs a Warning so slow endpoints are immediately visible in your observability tooling.
public interface IRequestPerformanceTrackingService
{
    Task<TResponse> TrackPerformanceAsync<TRequest, TResponse>(
        TRequest request,
        Func<CancellationToken, Task<TResponse>> next,
        CancellationToken cancellationToken)
        where TRequest : notnull;
}
Warning log (structured):
Long Running Request: GetReportQuery  Elapsed Time: 2340ms  User ID: "user-42"  Request: {...}

Command Pipeline Execution Order

The behaviors are composed by the infrastructure layer’s MediatR registration. The intended execution order for a transactional command is:
1

RequestLoggerService

Logs the incoming request (name, user ID, sanitized body) at Debug level.
2

RequestExceptionHandlerService

Opens a try/catch around the remaining pipeline. Logs and re-throws on any unhandled exception.
3

RequestValidationService

Runs all IValidator<TCommand> implementations. Returns Result.Invalid immediately if any rule fails.
4

RequestAuthorizationService

Inspects [Authorize] attributes on the command class. Returns Result.Unauthorized or Result.Forbidden if the check fails.
5

RequestCachingService

If the command implements ICacheRequest, attempts a cache read and short-circuits on a hit.
6

RequestInvalidateCachingService

If the command implements IInvalidateCacheRequest, executes the handler and then removes the cache prefix on success.
7

RequestUnitOfWorkService

Wraps the handler in a database transaction. Translates NotFoundException and BusinessRuleValidationException into typed Result values.
8

RequestPerformanceTrackingService

Times the handler, logs completion at Debug, and emits a Warning when execution exceeds 1 500 ms.
For the query pipeline, step 7 (RequestUnitOfWorkService) is omitted. The remaining seven behaviors run in the same order.

Using ISanitizableRequest with Pipelines

All pipeline services that serialize request data to logs (RequestLoggerService, RequestValidationService, RequestExceptionHandlerService, RequestPerformanceTrackingService) check whether the request implements ISanitizableRequest. If it does, they call GetSanitized() and serialize that object instead:
public interface ISanitizableRequest
{
    object GetSanitized();
}
Example — redact a card number:
[Authorize(Roles = "Finance")]
public sealed record ProcessPaymentCommand(
    Guid OrderId,
    string CardNumber,
    decimal Amount) : ICommand, ISanitizableRequest
{
    public object GetSanitized()
        => new { OrderId, CardNumber = "****-****-****-" + CardNumber[^4..], Amount };
}
No additional wiring is required. Implementing the interface is sufficient.

Build docs developers (and LLMs) love