Common Mistakes
These are the most common issues developers encounter when using Pragmatic.Caching. Each section shows the wrong approach, the correct approach, and explains why.
1. Forgetting partial on the Cacheable Type
Section titled “1. Forgetting partial on the Cacheable Type”Wrong:
[Cacheable(Duration = "5m", Tags = ["products"])]public class GetProductById{ public required int ProductId { get; init; }}Compile result: PRAG1700 error — “Type ‘GetProductById’ must be declared as partial to use [Cacheable]”.
Right:
[Cacheable(Duration = "5m", Tags = ["products"])]public partial class GetProductById{ public required int ProductId { get; init; }}Why: The source generator emits GetCacheKey(), GetCacheOptions(), and the ICacheable interface implementation into a partial class. Without partial, the compiler cannot merge the generated code with your class. The same rule applies to [InvalidatesCache] types.
2. Using Invalid Duration Format
Section titled “2. Using Invalid Duration Format”Wrong:
[Cacheable(Duration = "5 minutes")]public partial class GetUsers { }
[Cacheable(Duration = "300")]public partial class GetOrders { }Compile result: PRAG1701 error — “Invalid cache duration format ‘5 minutes’”. The second example ("300") also fails because bare numbers without a suffix are not a valid format.
Right:
[Cacheable(Duration = "5m")]public partial class GetUsers { }
[Cacheable(Duration = "300s")] // 300 secondspublic partial class GetOrders { }
[Cacheable(Duration = "00:05:00")] // TimeSpan format also workspublic partial class GetProducts { }Why: The source generator parses duration strings at compile time. Supported formats are:
| Format | Example | Meaning |
|---|---|---|
Ns | "30s" | N seconds |
Nm | "5m" | N minutes |
Nh | "1h" | N hours |
Nd | "1d" | N days |
| TimeSpan string | "00:15:00" | 15 minutes |
Bare numbers, English words (“minutes”), and other formats are not recognized.
3. Tag Placeholders Referencing Non-Existent Properties
Section titled “3. Tag Placeholders Referencing Non-Existent Properties”Wrong:
[Cacheable(Duration = "5m", Tags = ["user:{UserId}"])]public partial class GetUserProfile{ public required Guid Id { get; init; } // Property is "Id", not "UserId"}Compile result: PRAG1703 error — “Placeholder ‘{UserId}’ in tag/key refers to non-existent property on type ‘GetUserProfile’”.
Right:
[Cacheable(Duration = "5m", Tags = ["user:{Id}"])]public partial class GetUserProfile{ public required Guid Id { get; init; }}Or rename the property to match:
[Cacheable(Duration = "5m", Tags = ["user:{UserId}"])]public partial class GetUserProfile{ public required Guid UserId { get; init; }}Why: The source generator validates placeholder references at compile time. The placeholder name inside {...} must exactly match a public property name on the type. This is a compile-time safety check — without it, placeholders would silently expand to empty strings at runtime, causing cache keys and tags that do not differentiate entries correctly.
4. Excluding All Properties from the Cache Key
Section titled “4. Excluding All Properties from the Cache Key”Wrong:
[Cacheable(Duration = "5m")]public partial class GetGlobalConfig{ [CacheKey(Exclude = true)] public bool IncludeDebug { get; init; }
[CacheKey(Exclude = true)] public string? Format { get; init; }}Compile result: PRAG1751 warning — “All properties on type ‘GetGlobalConfig’ are excluded from cache key generation. This may cause cache collisions.” The code compiles, but the generated key is just "GetGlobalConfig" with no property discrimination — every call returns the same cached result regardless of IncludeDebug or Format.
Right:
If the type truly has no differentiating parameters, remove the [CacheKey(Exclude = true)] attributes:
[Cacheable(Duration = "5m")]public partial class GetGlobalConfig{ // No properties = key is "GetGlobalConfig" — intentional singleton cache}If IncludeDebug and Format should affect the cache key, keep them included:
[Cacheable(Duration = "5m")]public partial class GetGlobalConfig{ public bool IncludeDebug { get; init; } public string? Format { get; init; }}// Key: "GetGlobalConfig:IncludeDebug=True:Format=json"Why: A cache key with no discriminating properties means every caller gets the same cached result. This is correct for a true singleton query (e.g., global configuration) but dangerous when different input combinations should produce different results. PRAG1751 warns you to confirm the intent.
5. Forgetting to Register HybridCache Before AddPragmaticCaching
Section titled “5. Forgetting to Register HybridCache Before AddPragmaticCaching”Wrong:
// Missing: builder.Services.AddHybridCache()builder.Services.AddPragmaticCaching();Runtime result: InvalidOperationException when resolving ICacheStack, because HybridCacheStack requires HybridCache from DI. The constructor throws because the HybridCache dependency cannot be satisfied.
Right:
builder.Services.AddHybridCache(); // Required: registers HybridCachebuilder.Services.AddPragmaticCaching(); // Registers ICacheStack -> HybridCacheStackFor distributed caching (L2), also register a distributed cache backend before AddHybridCache:
builder.Services.AddStackExchangeRedisCache(opts =>{ opts.Configuration = "localhost:6379";});builder.Services.AddHybridCache();builder.Services.AddPragmaticCaching();Why: AddPragmaticCaching registers ICacheStack as HybridCacheStack, which wraps Microsoft.Extensions.Caching.Hybrid.HybridCache. If HybridCache is not registered, the DI container cannot construct HybridCacheStack. AddHybridCache() is always required, even if you only use in-memory caching (L1).
6. Caching User-Specific Data Without Key Differentiation
Section titled “6. Caching User-Specific Data Without Key Differentiation”Wrong:
[Cacheable(Duration = "5m")]public partial class GetMyOrders{ // UserId comes from ICurrentUser at runtime, not a property}The generated key is "GetMyOrders" — the same for all users. User A’s orders are cached, and User B sees them.
Right:
[Cacheable(Duration = "5m", Tags = ["orders:user:{UserId}"])]public partial class GetMyOrders{ public required Guid UserId { get; init; } // Set from ICurrentUser before caching}// Key: "GetMyOrders:UserId=abc-123" — unique per userOr if you want the user ID as part of the key without a tag:
[Cacheable(Duration = "5m")]public partial class GetMyOrders{ [CacheKey(Order = 0)] public required Guid UserId { get; init; }}Why: The source generator builds the cache key from public properties declared on the type. If the differentiating value (user ID, tenant ID) is not a property on the cacheable type, it is not included in the key. Always include any value that changes the result as a property on the cacheable type.
7. Broadcasting Invalidation When Targeted Would Suffice
Section titled “7. Broadcasting Invalidation When Targeted Would Suffice”Wrong:
[InvalidatesCache("user:{UserId}")]public partial class UserRolesChanged{ public Guid UserId { get; init; }}This broadcasts the invalidation to ALL registered cache categories. If you have OutputCache, Permissions, and RateLimiting categories registered, all three receive the invalidation call — even though only the Permissions cache is affected by a role change.
Right:
[InvalidatesCache("user:{UserId}", Category = typeof(CacheCategories.Permissions))]public partial class UserRolesChanged{ public Guid UserId { get; init; }}Why: Broadcast invalidation (the default when Category is null) calls InvalidateByTagAsync on every registered category’s ICacheStack. This is correct when the event affects multiple subsystems (e.g., a user deletion should clear both permission cache and output cache). But when only one subsystem is affected, targeted invalidation avoids unnecessary work and reduces the risk of evicting unrelated entries that happen to share a tag pattern.
8. Using Sliding Expiration for Write-Heavy Data
Section titled “8. Using Sliding Expiration for Write-Heavy Data”Wrong:
[Cacheable(Duration = "30m", Sliding = true)]public partial class GetProductInventory{ public required int ProductId { get; init; }}The inventory changes frequently (every purchase), but the sliding window resets on every read. If the product page is popular, the cache entry is never evicted by time — it only goes away when explicitly invalidated. During the gap between the write and the invalidation event, stale inventory counts are served.
Right:
[Cacheable(Duration = "30s", Tags = ["inventory:{ProductId}"])]public partial class GetProductInventory{ public required int ProductId { get; init; }}
[InvalidatesCache("inventory:{ProductId}")]public partial class InventoryChanged{ public int ProductId { get; init; }}Why: Sliding expiration is designed for data that is read often and changes rarely — like user preferences or system configuration. For write-heavy data, use a short absolute duration combined with tag-based invalidation. This ensures that even if the invalidation event is delayed or missed, the stale data expires quickly.
9. Duplicate CacheKey Order Values
Section titled “9. Duplicate CacheKey Order Values”Wrong:
[Cacheable(Duration = "5m")]public partial class SearchProducts{ [CacheKey(Order = 0)] public required int TenantId { get; init; }
[CacheKey(Order = 0)] // Same Order as TenantId! public required string Category { get; init; }
public int Page { get; init; } = 1;}Compile result: PRAG1750 warning — “Properties ‘TenantId’ and ‘Category’ have the same Order value 0. Order may be non-deterministic.” The key might be "SearchProducts:TenantId=1:Category=shoes:Page=1" or "SearchProducts:Category=shoes:TenantId=1:Page=1" depending on compiler internals. Different builds could produce different key orderings, causing cache misses after deployment.
Right:
[Cacheable(Duration = "5m")]public partial class SearchProducts{ [CacheKey(Order = 0)] public required int TenantId { get; init; }
[CacheKey(Order = 1)] public required string Category { get; init; }
public int Page { get; init; } = 1;}// Key: "SearchProducts:TenantId=1:Category=shoes:Page=1" — deterministicWhy: When two properties share the same Order value, their position in the cache key is non-deterministic. This can cause cache misses across builds or even across different compilation targets. Assign unique Order values to ensure deterministic key ordering. Properties without explicit Order use int.MaxValue and sort by declaration order.
10. Not Using Categories for Subsystem Isolation
Section titled “10. Not Using Categories for Subsystem Isolation”Wrong:
// Permission cache[Cacheable(Duration = "5m", Tags = ["perms:{UserId}"])]public partial class GetUserPermissions{ public required Guid UserId { get; init; }}
// Output cache for product pages[Cacheable(Duration = "1m", Tags = ["product:{ProductId}"])]public partial class GetProductPage{ public required int ProductId { get; init; }}Both share the same default ICacheStack. A global KeyPrefix in CachingOptions applies to both. There is no way to set different default TTLs for permissions vs. product pages without specifying Duration on every single attribute.
Right:
[Cacheable(Duration = "5m", Category = typeof(CacheCategories.Permissions), Tags = ["user:{UserId}"])]public partial class GetUserPermissions{ public required Guid UserId { get; init; }}
[Cacheable(Duration = "1m", Category = typeof(CacheCategories.OutputCache), Tags = ["product:{ProductId}"])]public partial class GetProductPage{ public required int ProductId { get; init; }}With registration:
builder.Services.AddPragmaticCaching(cache =>{ cache.ForCategory<CacheCategories.Permissions>(o => { o.KeyPrefix = "perms:"; o.DefaultDuration = TimeSpan.FromMinutes(5); }); cache.ForCategory<CacheCategories.OutputCache>(o => { o.KeyPrefix = "oc:"; o.DefaultDuration = TimeSpan.FromMinutes(1); });});Why: Without categories, all cache entries share a single namespace. Category routing provides three benefits: (1) key prefix isolation prevents collisions, (2) per-category default durations reduce attribute verbosity, and (3) targeted invalidation avoids cross-subsystem eviction noise. Use categories whenever two subsystems have different caching requirements.
11. Manually Building Cache Keys Instead of Using Generated Helpers
Section titled “11. Manually Building Cache Keys Instead of Using Generated Helpers”Wrong:
// In an invalidation handler:var key = $"GetProductById:ProductId={evt.ProductId}";await cache.RemoveAsync(key, ct);This duplicates the key format from the [Cacheable] class. If the property name changes or the key format evolves, this string falls out of sync.
Right:
// Use the generated static helpervar key = GetProductByIdCacheKeys.Create(evt.ProductId);await cache.RemoveAsync(key, ct);Or use tag-based invalidation instead of key-based removal:
[InvalidatesCache("product:{ProductId}")]public partial class ProductUpdated{ public int ProductId { get; init; }}Why: The source generator produces a {Type}CacheKeys static helper class with a Create(...) method that builds the cache key using the same format as GetCacheKey(). Using this helper ensures the key format stays in sync. Even better, prefer tag-based invalidation via [InvalidatesCache] — it eliminates the need to know individual key formats entirely.
Quick Reference
Section titled “Quick Reference”| Mistake | Diagnostic / Symptom |
|---|---|
Missing partial | PRAG1700 compile error |
| Invalid duration format | PRAG1701 compile error |
| Placeholder references non-existent property | PRAG1703 compile error |
| All properties excluded from key | PRAG1751 warning, potential cache collisions |
Missing AddHybridCache() | InvalidOperationException at runtime |
| No user-differentiating property | All users share the same cached result |
| Broadcast invalidation when targeted suffices | Unnecessary evictions in unrelated categories |
| Sliding expiration on write-heavy data | Stale data served indefinitely |
Duplicate [CacheKey(Order)] values | PRAG1750 warning, non-deterministic key ordering |
| No category isolation | Key collisions, configuration bleed, invalidation noise |
| Manual key construction | Key format drift when properties change |