DEV Community

Cover image for Building a Swappable Cache Abstraction in .NET — How the PowerCSharp Cache Feature Works
Mario Alberto Arce
Mario Alberto Arce

Posted on

Building a Swappable Cache Abstraction in .NET — How the PowerCSharp Cache Feature Works

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));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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 } } }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Provider ideas teams could build:

  • RedisStackExchange.Redis-backed distributed cache
  • MemcachedEnyimMemcachedCore-backed cache
  • Hybrid — L1 BitFaster + L2 Redis (multi-tier)
  • Azure Blob — cold storage cache for large payloads
  • SQL ServerIDistributedCache over SQL for compliance environments
  • NCache — enterprise in-process distributed cache

PowerCSharp - Enhanced C# extension methods and utilities for .NET developers

GitHub: https://github.com/marioarce/PowerCSharp

NuGet: PowerCSharp.Feature.Cache + a provider package

Top comments (0)