Skip to main content

Caching

Excalibur.Dispatch.Caching integrates caching directly into the dispatch pipeline. Actions that implement ICacheable<T> are automatically cached, with support for memory, distributed, and hybrid cache modes.

Before You Start

  • .NET 8.0+ (or .NET 9/10 for latest features)
  • Install the required package:
    dotnet add package Excalibur.Dispatch.Caching
  • Familiarity with pipeline concepts and middleware

Package

PackagePurpose
Excalibur.Dispatch.CachingPipeline caching middleware, ICacheable<T>, ICacheProvider, attribute-based caching

Setup

Register caching services with IDispatchBuilder:

using Microsoft.Extensions.DependencyInjection;

builder.AddDispatchCaching();

// With options
builder.WithCachingOptions(options =>
{
// Configure caching behavior
});

// With a result cache policy
builder.WithResultCachePolicy(policy =>
{
// Configure result-level caching
});

ICacheable Actions

Make actions cacheable by implementing ICacheable<T>. Since ICacheable<T> extends IDispatchAction<T>, your action is automatically a dispatch action that returns a result:

using Excalibur.Dispatch.Caching;

public class GetProductAction : ICacheable<ProductDto>
{
public Guid ProductId { get; set; }

// Cache for 5 minutes
public int ExpirationSeconds => 300;

// Unique cache key for this action
public string GetCacheKey()
=> $"product:{ProductId}";

// Tags for grouped invalidation
public string[] GetCacheTags()
=> [$"product:{ProductId}", "products"];

// Conditional caching (receives the handler result)
public bool ShouldCache(object? result)
=> ProductId != Guid.Empty && result is not null;
}

The pipeline automatically checks the cache before executing the handler. On a cache hit, the handler is skipped and the cached result is returned.

ICacheable Members

MemberPurpose
ExpirationSecondsHow long to cache the result
GetCacheKey()Unique key identifying this specific request
GetCacheTags()Tags for grouped invalidation
ShouldCache(object? result)Whether to cache this particular result

Attribute-Based Caching

Use [CacheResult] to add caching without implementing ICacheable<T>:

using Excalibur.Dispatch.Caching;

[CacheResult(
ExpirationSeconds = 600,
Tags = new[] { "products" },
OnlyIfSuccess = true,
IgnoreNullResult = true)]
public class ListProductsAction : IDispatchAction<IReadOnlyList<ProductDto>>
{
public string Category { get; set; } = string.Empty;
}

CacheResultAttribute Properties

PropertyTypeDefaultDescription
ExpirationSecondsintCache duration in seconds
Tagsstring[]Tags for grouped invalidation
OnlyIfSuccessbooltrueOnly cache successful results
IgnoreNullResultbooltrueSkip caching when result is null

Cache Providers

Implement ICacheProvider for custom cache backends:

using Excalibur.Dispatch.Caching;

public class RedisCacheProvider : ICacheProvider
{
public async Task<T?> GetAsync<T>(
string key,
CancellationToken cancellationToken)
{
// Retrieve from Redis
}

public async Task SetAsync<T>(
string key,
T value,
CancellationToken cancellationToken,
TimeSpan? expiration = null,
string[]? tags = null)
{
// Store in Redis
}

public async Task RemoveAsync(
string key,
CancellationToken cancellationToken)
{
// Remove single key
}

public async Task RemoveByTagAsync(
string tag,
CancellationToken cancellationToken)
{
// Remove all entries with this tag
}

public async Task<bool> ExistsAsync(
string key,
CancellationToken cancellationToken)
{
// Check if key exists
}

public async Task ClearAsync(
CancellationToken cancellationToken)
{
// Clear all cached entries
}
}

ICacheProvider Methods

MethodPurpose
GetAsync<T>(key, ct)Retrieve a cached value
SetAsync<T>(key, value, ct, expiration?, tags?)Store a value with optional expiration and tags
RemoveAsync(key, ct)Remove a specific entry
RemoveByTagAsync(tag, ct)Remove all entries with a tag
ExistsAsync(key, ct)Check if a key exists
ClearAsync(ct)Clear all cached entries

Cache Modes

CacheMode controls where cache data is stored:

ModeDescription
MemoryIn-process memory cache (fastest, per-instance)
DistributedExternal cache store (Redis, SQL, etc.)
HybridCheck memory first, fall back to distributed

Cache Invalidation

Invalidate caches by key or tag:

public class UpdateProductHandler : IActionHandler<UpdateProductAction>
{
private readonly ICacheProvider _cache;

public UpdateProductHandler(ICacheProvider cache)
{
_cache = cache;
}

public async Task HandleAsync(
UpdateProductAction action,
CancellationToken ct)
{
// Update the product...

// Invalidate specific entry
await _cache.RemoveAsync($"product:{action.ProductId}", ct);

// Invalidate all product listings
await _cache.RemoveByTagAsync("products", ct);
}
}

What's Next

See Also

  • Built-in Middleware — Overview of all built-in middleware including caching integration
  • Auto-Freeze — Immutable message optimization for improved caching and thread safety
  • Performance Tuning — Operational guidance for tuning Dispatch performance in production