The Cache pluggable feature family: framework-agnostic contracts, the feature module, and two provider implementations. Each package in the family versions independently under
PowerCSharpFeatureCacheVersion.
| Package | Role | Target Frameworks | Version |
|---|---|---|---|
PowerCSharp.Feature.Cache.Abstractions |
Contracts + NoOp floor | netstandard2.0 + net8.0 |
$(PowerCSharpFeatureCacheVersion) |
PowerCSharp.Feature.Cache |
Module + options + ASP.NET Core wiring | net8.0 |
$(PowerCSharpFeatureCacheVersion) |
PowerCSharp.Feature.Cache.BitFaster |
BitFaster in-memory LRU provider | netstandard2.0 + net8.0 |
$(PowerCSharpFeatureCacheVersion) |
PowerCSharp.Feature.Cache.Disk |
Disk-backed LRU provider | net8.0 |
$(PowerCSharpFeatureCacheVersion) |
PowerCSharp.Feature.Cache.Abstractions (contracts + NoOp, no third-party deps)
▲ ▲
│ │
PowerCSharp.Feature.Cache │ (module + options → depends on engine abstractions too)
▲ │
│ │
PowerCSharp.Feature.Cache.BitFaster PowerCSharp.Feature.Cache.Disk
(isolates BitFaster.Caching) (no third-party deps)
public interface ICacheService
{
// Sync
bool TryGet<T>(string key, out T? value);
void Set<T>(string key, T value, TimeSpan? ttl = null);
bool Remove(string key);
void Clear();
IEnumerable<string> GetKeys();
// Async
ValueTask<CacheResult<T>> GetAsync<T>(string key, CancellationToken ct = default);
ValueTask SetAsync<T>(string key, T value, TimeSpan? ttl = null, CancellationToken ct = default);
ValueTask<bool> RemoveAsync(string key, CancellationToken ct = default);
ValueTask ClearAsync(CancellationToken ct = default);
// Stampede protection
T GetOrCreate<T>(string key, Func<T> factory, TimeSpan? ttl = null);
ValueTask<T> GetOrCreateAsync<T>(string key, Func<ValueTask<T>> factory, TimeSpan? ttl = null, CancellationToken ct = default);
// Metadata
CacheMetadata? GetMetadata(string key);
}Extends ICacheService with disk-specific members:
public interface IDiskCacheService : ICacheService
{
string CacheDirectory { get; }
long ApproximateSizeBytes { get; }
ValueTask CleanupAsync(CancellationToken ct = default);
ValueTask<bool> ExistsAsync(string key, CancellationToken ct = default);
ValueTask<CacheMetadata?> GetMetadataAsync(string key, CancellationToken ct = default);
}Low-level store abstraction used internally by providers. Not intended for direct consumption.
public readonly struct CacheResult<T>
{
public bool Hit { get; }
public T? Value { get; }
public CacheMetadata? Metadata { get; }
public static CacheResult<T> Miss();
public static CacheResult<T> Found(T value, CacheMetadata? metadata = null);
}public sealed class CacheMetadata
{
public string Key { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime? ExpiresAt { get; init; }
public long? SizeBytes { get; init; }
public CacheProvider Provider { get; init; }
public long HitCount { get; set; }
}public enum CacheFileKind
{
Data, // main cache entry file
Lock, // cross-process file lock
Metadata // sidecar metadata file
}public enum CacheProvider
{
None,
NoOp,
BitFaster,
Disk,
Memory // future
}| Class | Interface | Behaviour |
|---|---|---|
NoOpCacheService |
ICacheService |
All reads return miss; writes are no-ops. Safe-off floor when no provider is active. |
NoOpDiskCacheService |
IDiskCacheService |
Extends NoOpCacheService; disk-specific members return empty/zero. |
IFeatureModule implementation. Auto-discoverable (parameterless constructor). Always registered via ConfigureServices (Model A — module self-gates).
public const string Key = "Cache"; // FeatureKey
public int Order => 100;ConfigureServices behaviour:
- Binds
CacheFeatureOptionsfromPowerFeatures:Cacheconfig section. - Registers
NoOpCacheServiceandNoOpDiskCacheServiceviaTryAddSingleton(safe-off floor). - Logs an info message when the feature is disabled.
- Provider packages use plain
AddSingletonwhich takes precedence over theTryAddNoOp.
public sealed class CacheFeatureOptions : FeatureOptionsBase
{
public bool Enabled { get; set; } // inherited
public CacheProvider Provider { get; set; }
public int Capacity { get; set; } = 1000;
public TimeSpan? DefaultTtl { get; set; }
}IServiceCollection AddCacheFeature(
this IServiceCollection services,
IConfiguration configuration)Explicit alternative to auto-discovery. Calls ConfigureServices directly without going through the engine scan.
{
"PowerFeatures": {
"Cache": {
"Enabled": true,
"Provider": "BitFaster",
"Capacity": 5000,
"DefaultTtl": "00:05:00"
}
}
}Implements ICacheService backed by BitFaster.Caching.ConcurrentLru<string, CacheEntry<T>>. The BitFaster.Caching dependency is isolated to this package.
| Feature | Detail |
|---|---|
| Target | netstandard2.0 + net8.0 |
| Eviction | Concurrent LRU (W-TinyLFU variant via BitFaster) |
| Stampede protection | GetOrCreate / GetOrCreateAsync with GetOrAdd atomicity |
| Metadata | Hit count, created/expires timestamps, provider tag |
| Thread safety | Fully thread-safe (lock-free via BitFaster internals) |
Provider packages register via plain AddSingleton (takes precedence over the NoOp TryAdd):
services.AddSingleton<ICacheService, BitFasterCacheService>();This is wired automatically when CacheFeatureModule runs and the Provider option resolves to BitFaster.
Implements IDiskCacheService with a local-disk LRU store.
| Feature | Detail |
|---|---|
| Target | net8.0 |
| Storage | JSON-serialized entries under DiskCacheFeatureOptions.CacheDirectory |
| Eviction | LRU eviction when MaxEntries is exceeded |
| Atomic writes | Write-to-temp + atomic rename prevents partial reads |
| Cross-process locking | Per-key .lock files using FileStream with FileShare.None |
| Background cleanup | DiskCacheBackgroundService (IHostedService) runs periodic cleanup |
| Metadata | Sidecar .meta files store CacheMetadata alongside data files |
| Debugging | DebugHelper provides DumpState() for diagnostics |
public sealed class DiskCacheFeatureOptions
{
public string CacheDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "PowerCSharp.Cache");
public int MaxEntries { get; set; } = 1000;
public TimeSpan DefaultTtl { get; set; } = TimeSpan.FromHours(1);
public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromMinutes(15);
}{
"PowerFeatures": {
"Cache": {
"Enabled": true,
"Provider": "Disk"
},
"Cache:Disk": {
"CacheDirectory": "/var/cache/myapp",
"MaxEntries": 5000,
"DefaultTtl": "02:00:00",
"CleanupInterval": "00:30:00"
}
}
}<CacheDirectory>/
<key-hash>.dat CacheFileKind.Data — serialised value
<key-hash>.meta CacheFileKind.Metadata — CacheMetadata JSON
<key-hash>.lock CacheFileKind.Lock — cross-process lock (transient)
| No package ref | Package ref + flag off | Package ref + flag on | |
|---|---|---|---|
| Result | Absent — no code, no deps | NoOp registered (safe-off) | Active provider registered |
// Program.cs
// Option A: auto-discovery via engine scan
builder.Services.AddPowerFeatures(builder.Configuration, options =>
{
options.ScanAssemblies(typeof(CacheFeatureModule).Assembly);
});
// Option B: explicit registration (no reflection)
builder.Services.AddCacheFeature(builder.Configuration);
var app = builder.Build();
app.UsePowerFeatures();// In a service/controller
public class MyService(ICacheService cache)
{
public async Task<MyData> GetDataAsync(string key)
{
var result = await cache.GetAsync<MyData>(key);
if (result.Hit)
return result.Value!;
var data = await FetchFromSourceAsync(key);
await cache.SetAsync(key, data, TimeSpan.FromMinutes(10));
return data;
}
}PowerCSharp.Features.Architecture.md— Two-tier design, dependency topologyPowerCSharp.Features.Authoring-Guide.md— Cache family as the canonical worked examplePowerCSharp.Feature.Cache.Disk.Plan.md— Disk cache design notes and implementation historyPowerCSharp.Features.md— Features engine API reference