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 provides a ICacheService abstraction with first-class pipeline integration. Rather than manually calling a cache inside every query handler, you declare caching intent on the request object itself by implementing ICacheRequest or IInvalidateCacheRequest. The MediatR pipeline automatically handles cache reads, writes, and invalidation through CachingBehavior and InvalidateCachingBehavior — keeping your handlers clean and focused on business logic.

Core Abstractions

ICacheService

The central caching abstraction. CacheService is the built-in implementation registered by AddSharedKernelInfrastructure. Its backing store is planned for Microsoft.Extensions.Caching.Hybrid but is not yet fully implemented — all methods currently throw NotImplementedException. You can substitute your own ICacheService implementation before the SharedKernel registration to supply a working cache.
public interface ICacheService
{
    Task SetAsync<T>(
        string cacheKey,
        T cacheValue,
        TimeSpan? expiration = null,
        CancellationToken cancellationToken = default);

    Task<ICacheValue<T>> GetAsync<T>(
        string cacheKey,
        CancellationToken cancellationToken = default);

    Task RemoveByPrefixAsync(
        string prefix,
        CancellationToken cancellationToken = default);
}
MethodDescription
SetAsync<T>Stores a value under cacheKey with an optional absolute expiration.
GetAsync<T>Returns an ICacheValue<T> wrapper. Check HasValue and IsNull before reading Value.
RemoveByPrefixAsyncRemoves all cache entries whose key starts with prefix. Useful for tag-style bulk invalidation.

ICacheValue<T>

The return type of ICacheService.GetAsync<T>. It distinguishes between “not cached” and “cached as null”:
public interface ICacheValue<out T>
{
    bool HasValue { get; }  // true when the cache hit returned a value
    bool IsNull { get; }    // true when the stored value was null
    T Value { get; }        // the cached value (valid only when HasValue && !IsNull)
}
A typical consumption pattern:
var cached = await cacheService.GetAsync<Order>(cacheKey, cancellationToken);
if (!cached.IsNull && cached.HasValue)
{
    return cached.Value; // cache hit
}
// cache miss — fetch from DB and store

Opting Into Caching

ICacheRequest

Implement this interface on a query to tell CachingBehavior that the response should be cached:
public interface ICacheRequest
{
    string CacheKey { get; }

    TimeSpan? AbsoluteExpirationInSeconds { get; }
}
  • CacheKey — the exact key under which the response is stored. Use a key that encodes all query parameters so different inputs produce different entries (e.g., $"orders_{id}").
  • AbsoluteExpirationInSecondsnull means no expiration (use the backing store’s default). Set this to a TimeSpan for short-lived query results.

Example — GetOrderByIdQuery

public sealed record GetOrderByIdQuery(Guid OrderId)
    : IQuery<OrderResponse>, ICacheRequest
{
    public string CacheKey => $"orders_{this.OrderId}";

    public TimeSpan? AbsoluteExpirationInSeconds => TimeSpan.FromMinutes(5);
}
When the query bus dispatches this query, CachingBehavior first calls ICacheService.GetAsync<Result<OrderResponse>>(CacheKey). On a cache miss it calls the handler, then stores the result with the configured expiration.

IInvalidateCacheRequest

Implement this interface on a command to invalidate related cache entries after the command succeeds:
public interface IInvalidateCacheRequest
{
    public string PrefixCacheKey { get; }
}
  • PrefixCacheKey — all cache entries whose key starts with this prefix are removed. This works as a tag-based bulk eviction: if all order queries use keys starting with "orders_", a single PrefixCacheKey = "orders_" evicts them all.

Example — UpdateOrderCommand

public sealed record UpdateOrderCommand(Guid OrderId, decimal NewAmount)
    : ICommand, ITransactionalCommand, IInvalidateCacheRequest
{
    public string PrefixCacheKey => $"orders_{this.OrderId}";
}
InvalidateCachingBehavior calls ICacheService.RemoveByPrefixAsync(PrefixCacheKey) after the handler returns successfully.

Pipeline Behaviors

CachingBehavior<TRequest, TResponse> activates for any request that implements ICacheRequest. It delegates to IRequestCachingService.HandleWithCacheAsync, which:
  1. Calls ICacheService.GetAsync<TResponse>(request.CacheKey).
  2. On a hit, returns the cached TResponse immediately — the handler is never called.
  3. On a miss, calls next() (the handler), then stores the response with SetAsync.
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : ICacheRequest
    where TResponse : IResult
{
    private readonly IRequestCachingService cacheRequestHandlerService;

    public Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
        => this.cacheRequestHandlerService.HandleWithCacheAsync(
            request,
            _ => next(cancellationToken),
            cancellationToken);
}

Pipeline Registration

Caching pipeline services are registered through the Application and Infrastructure DI extension methods. Both the command bus and query bus register the caching services:
// Application layer — registers IRequestCachingService and IRequestInvalidateCachingService
builder.Services.AddSharedKernelApplicationCommandBus();
builder.Services.AddSharedKernelApplicationQueryBus();

// Infrastructure layer — registers MediatR pipeline with CachingBehavior
builder.Services.AddSharedKernelInfrastructureCommandBus();
builder.Services.AddSharedKernelInfrastructureQueryBus();
The IRequestCachingService and IRequestInvalidateCachingService pipeline services are registered as Transient by AddSharedKernelApplicationCommandBus / AddSharedKernelApplicationQueryBus:
services.AddTransient<IRequestCachingService, RequestCachingService>();
services.AddTransient<IRequestInvalidateCachingService, RequestInvalidateCachingService>();
ICacheService itself is registered by AddSharedKernelInfrastructure as Transient<CacheService>. The built-in CacheService has no external dependencies — its backing store using Microsoft.Extensions.Caching.Hybrid is planned but not yet implemented. If you need a functioning cache today, register your own ICacheService implementation before calling AddSharedKernelInfrastructure so that your registration takes precedence:
// Register your own ICacheService implementation first
builder.Services.AddTransient<ICacheService, MyRedisCacheService>();

// Then register SharedKernel infrastructure
builder.Services.AddSharedKernelInfrastructure();

BaseCachedSpecificationRepository Integration

For repository-level caching (as opposed to pipeline caching), the EF Core package provides BaseCachedSpecificationRepository, which wraps every read operation in ICacheService:
// Cache key is the aggregate's full type name, e.g. "MyApp.Domain.Order"
public string CacheKey => $"{typeof(TAggregate)}";

// GetByIdAsync uses a composite key: "{TypeName}_{id}"
public override async Task<TAggregate?> GetByIdAsync(
    TId id, CancellationToken cancellationToken = default)
{
    var cacheKeyId = $"{this.CacheKey}_{id}";

    var cacheResponse = await this.CacheGetAsync<TAggregate>(cacheKeyId, cancellationToken);
    if (cacheResponse != null) return cacheResponse;

    var response = await base.GetByIdAsync(id, cancellationToken);
    await this.CacheSetAsync(cacheKeyId, response, cancellationToken);
    return response;
}
Write operations (AddAsync, UpdateAsync, DeleteAsync, and their range variants) automatically call RemoveByPrefixAsync(CacheKey) to evict all cached entries for that aggregate type.

Complete Example

1

Define a cacheable query

public sealed record GetOrderByIdQuery(Guid OrderId)
    : IQuery<OrderResponse>, ICacheRequest
{
    // Unique key per order ID
    public string CacheKey => $"orders_{this.OrderId}";

    // Cache results for 5 minutes
    public TimeSpan? AbsoluteExpirationInSeconds => TimeSpan.FromMinutes(5);
}
2

Define a command that invalidates the cache

public sealed record UpdateOrderCommand(Guid OrderId, decimal NewAmount)
    : ICommand, ITransactionalCommand, IInvalidateCacheRequest
{
    // Evicts all "orders_{OrderId}" entries
    public string PrefixCacheKey => $"orders_{this.OrderId}";
}
3

Implement the query handler (no manual cache code needed)

public class GetOrderByIdQueryHandler
    : IQueryHandler<GetOrderByIdQuery, OrderResponse>
{
    private readonly IOrderReadRepository repository;

    public GetOrderByIdQueryHandler(IOrderReadRepository repository)
        => this.repository = repository;

    public async Task<Result<OrderResponse>> Handle(
        GetOrderByIdQuery query,
        CancellationToken cancellationToken)
    {
        // CachingBehavior handles cache lookup/store before/after this runs
        var order = await this.repository.GetByIdAsync(
            new OrderId(query.OrderId), cancellationToken);

        if (order is null)
            return Result.NotFound();

        return Result.Success(new OrderResponse(order.Id, order.TotalAmount));
    }
}
4

Register services

// SharedKernel infrastructure (registers CacheService as ICacheService)
builder.Services.AddSharedKernelInfrastructure();

// Application pipeline services (registers RequestCachingService etc.)
builder.Services.AddSharedKernelApplicationQueryBus();
builder.Services.AddSharedKernelApplicationCommandBus();

// Infrastructure pipeline behaviors (registers CachingBehavior etc.)
builder.Services.AddSharedKernelInfrastructureQueryBus();
builder.Services.AddSharedKernelInfrastructureCommandBus();

Cache key naming conventions and prefix-based invalidation:
  • Use a consistent prefix that scopes entries to a single aggregate type or use-case: "orders_", "customers_", etc.
  • For single-entity lookups, append the entity ID: "orders_{id}".
  • For list/specification queries, append the specification’s full type name: "orders_{typeof(ActiveOrdersSpec).FullName}".
  • When a command should invalidate all entries for an aggregate type (e.g., after a bulk update), set PrefixCacheKey = "orders_" to evict every entry that starts with that prefix in a single RemoveByPrefixAsync call.
  • Avoid using mutable state or user-specific data in cache keys shared between tenants — always include the tenant or partition identifier when multi-tenancy is active.

Build docs developers (and LLMs) love