Caching is one of those things that seems simple until you try to make it swappable. You start with IMemoryCache. Then you need distributed cache. Then you need disk. Then you need to test without any cache at all. And suddenly you have #if blocks and constructor juggling everywhere.
Here's the pattern I built into PowerCSharp: a cache abstraction that's provider-independent, safe-off by default, and hot-swappable via configuration.
The Contract: ICacheService
var cache = services.GetRequiredService<ICacheService>();
// Async-first, always returns CacheResult<T>
var result = await cache.GetAsync<UserProfile>("user:42");
if (result.Hit)
{
return result.Value!;
}
// Stampede protection built-in
var profile = await cache.GetOrCreateAsync(
"user:42",
async () => await database.GetUserAsync(42),
ttl: TimeSpan.FromMinutes(15));
No provider-specific imports. No IMemoryCache or IDistributedCache in your service. Just ICacheService.
The Safe-Off Floor
When the Cache feature is disabled (or no provider package is referenced), a NoOpCacheService is registered. Every GetAsync returns a miss. Every SetAsync is a no-op. Your services compile and run — you just don't get cache hits.
This is intentional. Production teams can disable cache for debugging. Test environments get the NoOp automatically without mock setup.
Two-Layer Gating
Package reference gate → Runtime flag gate → Active provider or NoOp
| Scenario | Result |
|---|---|
| No package ref | Feature doesn't exist — not even a NoOp registered |
| Package ref + flag off |
NoOpCacheService registered |
| Package ref + flag on + provider = BitFaster |
BitFasterCacheService registered |
| Package ref + flag on + provider = Disk |
DiskCacheService registered |
Swapping Providers With Zero Code Changes
// In-memory LRU during development
{ "PowerFeatures": { "Cache": { "Enabled": true, "Provider": "BitFaster", "Capacity": 1000 } } }
// Disk cache in constrained environments
{ "PowerFeatures": { "Cache": { "Enabled": true, "Provider": "Disk" } } }
// No cache during debugging
{ "PowerFeatures": { "Cache": { "Enabled": false } } }
The ICacheService injection point in your services never changes. The provider changes behind it.
Build Your Own Provider
The contract is open. Implement ICacheService (or IDiskCacheService) and register it:
// In your provider package
services.AddSingleton<ICacheService, RedisBasedCacheService>();
// Plain AddSingleton takes precedence over the NoOp TryAddSingleton
Provider ideas teams could build:
-
Redis —
StackExchange.Redis-backed distributed cache -
Memcached —
EnyimMemcachedCore-backed cache - Hybrid — L1 BitFaster + L2 Redis (multi-tier)
- Azure Blob — cold storage cache for large payloads
-
SQL Server —
IDistributedCacheover SQL for compliance environments - NCache — enterprise in-process distributed cache
GitHub: https://github.com/marioarce/PowerCSharp
NuGet: PowerCSharp.Feature.Cache + a provider package

Top comments (0)