-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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
}// 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() { ... }Set the default for all actions in CacheWeaveOptions:
builder.Services.AddCacheWeave(options =>
{
options.DefaultNoCacheCondition = NoCacheCondition.OnError;
});Individual [CacheWeave] attributes override the global default.
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.
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) { ... }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.
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.
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.