Skip to content

Conditional Caching

Aghogho Bernard edited this page May 6, 2026 · 1 revision

Conditional Caching

CacheWeave gives you fine-grained control over when a response is written to the cache. This prevents caching error responses, empty results, or responses that vary by request body.

NoCacheWhen

The NoCacheWhen property on [CacheWeave] controls when caching is skipped for a specific action. It accepts a NoCacheCondition flags enum.

[Flags]
public enum NoCacheCondition
{
    Never          = 0,
    OnError        = 1 << 0,   // skip on non-2xx HTTP status
    OnEmpty        = 1 << 1,   // skip on null, empty string, or empty collection
    OnErrorOrEmpty = OnError | OnEmpty  // default
}

Examples

// Default — skip caching on errors and empty results
[CacheWeave("products:list")]
public Task<IActionResult> GetAll() { ... }

// Cache everything, including errors and empty results
[CacheWeave("products:list", NoCacheWhen = NoCacheCondition.Never)]
public Task<IActionResult> GetAll() { ... }

// Skip only on errors — cache empty collections
[CacheWeave("products:list", NoCacheWhen = NoCacheCondition.OnError)]
public Task<IActionResult> GetAll() { ... }

// Skip only on empty — cache error responses (unusual, but valid)
[CacheWeave("products:list", NoCacheWhen = NoCacheCondition.OnEmpty)]
public Task<IActionResult> GetAll() { ... }

Global Default

Set the default for all actions in CacheWeaveOptions:

builder.Services.AddCacheWeave(options =>
{
    options.DefaultNoCacheCondition = NoCacheCondition.OnError;
});

Individual [CacheWeave] attributes override the global default.


Sliding Expiry

By default, a cache entry's TTL is fixed from the time it was written. With sliding expiry, the TTL resets on every cache hit — the entry only expires if it goes unaccessed for the full TTL duration.

// Entry expires 10 minutes after the last access, not after the first write
[HttpGet("{id}")]
[CacheWeave("products:detail", SlidingExpiry = true, ExpirySeconds = 600)]
public Task<IActionResult> GetById(Guid id) { ... }

Implementation note: CacheWeave emulates sliding expiry by re-writing the entry (with a fresh TTL) on every cache hit. This is compatible with all providers, including those that do not natively support sliding expiry (e.g. Redis SET ... EX).

Trade-off: Sliding expiry generates a write on every read. For high-traffic endpoints, this can increase write pressure on the backing store. Use fixed expiry for hot keys.


Body Hashing (POST Endpoints)

For POST endpoints where the request body determines the response, append a hash of the body to the cache key so different request bodies get different cache entries.

[HttpPost("search")]
[CacheWeave("products:search", HashBody = true, ExpirySeconds = 120)]
public Task<IActionResult> Search([FromBody] SearchRequest request) { ... }

Selective Field Hashing

If the body contains noise fields (request IDs, timestamps, correlation IDs) that do not affect the result, hash only the stable fields:

public record SearchRequest(
    string Term,
    Guid? CategoryId,
    string[]? Filters,
    Guid RequestId,       // noise — changes every request
    DateTimeOffset SentAt // noise — changes every request
);

[HttpPost("search")]
[CacheWeave("products:search",
    HashBody = true,
    HashBodyFields = ["Term", "CategoryId", "Filters"],
    ExpirySeconds = 120)]
public Task<IActionResult> Search([FromBody] SearchRequest request) { ... }

Only Term, CategoryId, and Filters are extracted and hashed. RequestId and SentAt are ignored.

Hash algorithm: SHA-256, truncated to 16 hex characters for key brevity.


Combining Conditions

All conditions compose naturally:

[HttpPost("search")]
[CacheWeave("products:search",
    HashBody = true,
    HashBodyFields = ["Term", "CategoryId"],
    ExpirySeconds = 120,
    SlidingExpiry = false,
    NoCacheWhen = NoCacheCondition.OnErrorOrEmpty)]
public Task<IActionResult> Search([FromBody] SearchRequest request) { ... }

This caches search results keyed by Term + CategoryId, for 2 minutes, skipping empty or error responses.


Programmatic Conditional Caching

Use ICacheWeaveService directly when attribute-based conditions are insufficient:

public class ProductsController(ICacheWeaveService cache) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] int page = 1)
    {
        var key = $"products:list:page={page}";

        var result = await cache.GetOrSetAsync<PagedResult<ProductDto>>(
            key,
            factory: async () =>
            {
                var data = await _db.Products
                    .Skip((page - 1) * 20).Take(20)
                    .ToListAsync();
                return data.Count > 0 ? new PagedResult<ProductDto>(data) : null;
                // returning null skips the cache write
            },
            expiry: TimeSpan.FromMinutes(5));

        return result is null ? NoContent() : Ok(result);
    }
}

Returning null from the factory skips the cache write — equivalent to NoCacheCondition.OnEmpty.

See Programmatic API for the full ICacheWeaveService reference.

Clone this wiki locally