Pandatech.DistributedCache
6.0.0
dotnet add package Pandatech.DistributedCache --version 6.0.0
NuGet\Install-Package Pandatech.DistributedCache -Version 6.0.0
<PackageReference Include="Pandatech.DistributedCache" Version="6.0.0" />
<PackageVersion Include="Pandatech.DistributedCache" Version="6.0.0" />
<PackageReference Include="Pandatech.DistributedCache" />
paket add Pandatech.DistributedCache --version 6.0.0
#r "nuget: Pandatech.DistributedCache, 6.0.0"
#:package Pandatech.DistributedCache@6.0.0
#addin nuget:?package=Pandatech.DistributedCache&version=6.0.0
#tool nuget:?package=Pandatech.DistributedCache&version=6.0.0
Pandatech.DistributedCache
A focused .NET library that implements Microsoft's HybridCache abstraction on top of Redis. Provides strongly typed
caching with MessagePack serialization, distributed locking, stampede protection, tag-based invalidation, and rate
limiting — in under 500 lines of code.
Targets net9.0 and net10.0 only. HybridCache graduated from preview in .NET 9; net8 is not supported.
Table of Contents
- Features
- Installation
- Getting Started
- Caching
- Tag-Based Invalidation
- HybridCache Extensions
- Rate Limiting
- Distributed Locking
- String Extensions
- Configuration Reference
- MessagePack Serialization
- Health Check
Features
- HybridCache implementation — backs Microsoft's
HybridCacheabstraction with Redis, no local L1 layer - Stampede protection — concurrent
GetOrCreateAsynccalls on the same key are serialized; only one caller hits the factory - Tag-based invalidation — group cache entries under one or more tags and invalidate them all in one call
- Distributed locking — Redis-backed
IDistributedLockServicewith atomic acquire/release via Lua - Rate limiting — business-logic-level rate limiting with per-action, per-identity counters
- MessagePack serialization — binary, compact, fast; enforced uniformly across all cache entries
HybridCacheextension methods —GetOrDefaultAsync,TryGetAsync,ExistsAsync- String key helpers —
PrefixWithAssemblyNameandPrefixWithfor structured, collision-safe key naming - Redis health check — auto-registered with a 3-second timeout on
AddDistributedCache
Installation
dotnet add package Pandatech.DistributedCache
Getting Started
One call in Program.cs wires everything up:
builder.AddDistributedCache(options =>
{
options.RedisConnectionString = "localhost:6379"; // required
options.ChannelPrefix = "myapp"; // optional, default: null
});
AddDistributedCache registers:
IConnectionMultiplexer(singleton, with exponential reconnect)HybridCache→RedisDistributedCache(singleton)IRateLimitService→RedisRateLimitService(singleton)IDistributedLockService→RedisLockService(singleton)- Redis health check with a 3-second timeout
Caching
Preparing a cached model
Decorate your model with [MessagePackObject] and implement ICacheEntity:
[MessagePackObject]
public class UserSessionCache : ICacheEntity
{
[Key(0)] public Guid UserId { get; set; }
[Key(1)] public string Role { get; set; } = string.Empty;
[Key(2)] public DateTime ExpiresAt { get; set; }
}
ICacheEntity is a marker interface with no members. It exists to make the intent explicit at the type level.
GetOrCreateAsync
Inject HybridCache directly. If the key is absent, the factory runs once — concurrent callers block until the first
writer is done (stampede protection):
public class SessionService(HybridCache cache)
{
public async Task<UserSessionCache> GetSessionAsync(Guid userId, CancellationToken ct = default)
{
return await cache.GetOrCreateAsync(
$"session:{userId}",
async _ => await LoadFromDbAsync(userId, ct),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
tags: [$"user:{userId}"],
cancellationToken: ct);
}
}
SetAsync
await cache.SetAsync(
$"session:{userId}",
session,
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
tags: [$"user:{userId}"],
cancellationToken: ct);
If Expiration is omitted, DefaultExpiration from configuration is used (default: 15 minutes). Pass
TimeSpan.MaxValue to store without an expiry.
RemoveAsync
await cache.RemoveAsync($"session:{userId}", ct);
Tag-Based Invalidation
Tags let you invalidate a group of related entries without knowing their individual keys. Calling RemoveByTagAsync
writes a tombstone timestamp for that tag. The next read of any entry carrying that tag checks the tombstone — if the
tag was updated after the entry was written, the entry is evicted and re-fetched.
// Invalidate all cache entries tagged with "user:{userId}"
await cache.RemoveByTagAsync($"user:{userId}", ct);
An entry can carry multiple tags:
tags: ["user:42", "tenant:7"]
Invalidating either tag is enough to evict the entry on next read.
HybridCache Extensions
Three extension methods on HybridCache cover the most common patterns that the base API handles awkwardly.
GetOrDefaultAsync
Returns a cached value or a caller-supplied default without writing anything to Redis:
var value = await cache.GetOrDefaultAsync("feature-flag:dark-mode", defaultValue: false, ct);
TryGetAsync
Returns whether the key exists alongside its value in one round-trip:
var (exists, session) = await cache.TryGetAsync<UserSessionCache>($"session:{userId}", ct);
if (!exists)
{
// key is not in cache
}
ExistsAsync
Checks presence without deserializing the value:
var isActive = await cache.ExistsAsync<UserSessionCache>($"session:{userId}", ct);
All three extensions are implemented against the HybridCache abstraction, so they work with any compatible
implementation — not just this one.
Rate Limiting
IRateLimitService applies business-logic rate limits per action type and identity. State is stored in Redis and is
consistent across all instances of your service.
Define action types and configurations
public enum ActionType
{
SmsOtp = 1,
EmailOtp = 2,
Login = 3
}
public static class RateLimits
{
public static RateLimitConfiguration SmsOtp() => new()
{
ActionType = (int)ActionType.SmsOtp,
MaxAttempts = 3,
TimeToLive = TimeSpan.FromMinutes(10)
};
public static RateLimitConfiguration Login() => new()
{
ActionType = (int)ActionType.Login,
MaxAttempts = 10,
TimeToLive = TimeSpan.FromMinutes(15)
};
}
Apply the limit
public class AuthService(IRateLimitService rateLimitService)
{
public async Task<RateLimitState> RequestOtpAsync(string phoneNumber, CancellationToken ct = default)
{
var config = RateLimits.SmsOtp().SetIdentifiers(phoneNumber);
var state = await rateLimitService.RateLimitAsync(config, ct);
if (state.Status == RateLimitStatus.Exceeded)
{
// state.TimeToReset — how long until the window resets
// state.RemainingAttempts — always 0 here
throw new TooManyRequestsException($"Try again in {state.TimeToReset.TotalSeconds:0}s.");
}
// state.RemainingAttempts — how many calls are left in the window
await SendSmsAsync(phoneNumber, ct);
return state;
}
}
SetIdentifiers takes a primary identifier (e.g. phone number) and an optional secondary identifier (e.g. tenant ID).
The two together form a unique rate-limit key for that action type.
RateLimitState always contains:
| Property | Meaning |
|---|---|
Status |
NotExceeded or Exceeded |
TimeToReset |
Remaining TTL of the current window |
RemainingAttempts |
Calls left before Exceeded (0 when already exceeded) |
Distributed Locking
IDistributedLockService is available for cases where you need explicit locking outside of the cache layer. The
implementation uses SET NX for acquire and a Lua script for atomic release — the standard Redis lock pattern.
public class InventoryService(IDistributedLockService locks)
{
public async Task DeductStockAsync(int productId, int quantity, CancellationToken ct = default)
{
var key = $"product:{productId}";
var token = Guid.NewGuid().ToString();
if (!await locks.AcquireLockAsync(key, token))
{
await locks.WaitUntilLockIsReleasedAsync(key, ct);
// re-read state and decide what to do
return;
}
try
{
// exclusive access to this product's stock
}
finally
{
await locks.ReleaseLockAsync(key, token);
}
}
}
| Method | Behaviour |
|---|---|
AcquireLockAsync(key, token) |
Returns true if the lock was taken; false if already held by another caller |
HasLockAsync(key) |
Returns true if any lock currently exists on this key |
WaitUntilLockIsReleasedAsync |
Polls every 10 ms; throws TimeoutException if the lock isn't released within 2 × DistributedLockMaxDuration |
ReleaseLockAsync(key, token) |
Releases the lock only if the stored token matches; safe against accidental cross-caller release |
String Extensions
Utilities for building structured, collision-safe Redis key names.
// Prefix with a literal string
"user:42".PrefixWith("myapp"); // → "myapp:user:42"
// Prefix with the calling assembly's name (resolved at call site)
"user:42".PrefixWithAssemblyName(); // → "MyService.Api:user:42"
// Batch prefix
new[] { "user:1", "user:2" }.PrefixWith("myapp"); // → ["myapp:user:1", "myapp:user:2"]
new[] { "user:1", "user:2" }.PrefixWithAssemblyName();
PrefixWithAssemblyName calls Assembly.GetCallingAssembly(), so it captures the assembly that actually calls the
method — useful for shared utilities that should tag keys with the service that owns them.
Configuration Reference
All options except RedisConnectionString have sensible defaults and are optional.
| Option | Type | Default | Description |
|---|---|---|---|
RedisConnectionString |
string |
— | Required. Standard StackExchange.Redis connection string. |
ChannelPrefix |
string? |
null |
Optional namespace prefix inserted between DistributedCache and your key. |
ConnectRetry |
int |
10 |
Number of connection retries on startup. |
ConnectTimeout |
TimeSpan |
10s |
Timeout for establishing a connection. |
SyncTimeout |
TimeSpan |
5s |
Timeout for synchronous Redis commands. |
DistributedLockMaxDuration |
TimeSpan |
8s |
TTL applied to each lock key. Also governs the wait timeout (2 × this value). |
DefaultExpiration |
TimeSpan |
15min |
Fallback TTL when no Expiration is supplied in HybridCacheEntryOptions. |
Key naming
All cache keys are stored in Redis under the pattern:
DistributedCache[:{ChannelPrefix}]:{yourKey}
Tag tombstone keys follow:
DistributedCache[:{ChannelPrefix}]:tag:{tagName}
Lock keys append :lock to the prefixed cache key.
MessagePack Serialization
All cache values are serialized with MessagePack. This is not configurable — by design.
MessagePack is binary, compact (~50% of equivalent JSON), and significantly faster to serialize and deserialize than JSON or Protobuf in most .NET benchmarks. It also renders as a JSON-like view in most Redis desktop clients (e.g. Another Redis Desktop Manager), so debugging is not meaningfully harder than with JSON.
Enforcing a single serializer removes an entire class of subtle bugs (mismatched serializers between writers and readers, type name handling differences, DateTime encoding differences) and keeps the library surface small.
The trade-off: your cached models must carry [MessagePackObject] and [Key(n)] attributes. This is a one-time,
mechanical annotation and does not affect your domain logic.
Health Check
AddDistributedCache automatically registers a Redis health check via AspNetCore.HealthChecks.Redis with a 3-second
timeout. No additional configuration is needed.
If you expose a health endpoint:
app.MapHealthChecks("/health");
Redis connectivity is included in the response automatically. This integrates with Kubernetes liveness/readiness probes, load-balancer health checks, and any monitoring stack that speaks the ASP.NET Core health check protocol.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AspNetCore.HealthChecks.Redis (>= 9.0.0)
- MessagePack (>= 3.1.4)
- StackExchange.Redis.Extensions.AspNetCore (>= 11.0.0)
- StackExchange.Redis.Extensions.MsgPack (>= 11.0.0)
-
net9.0
- AspNetCore.HealthChecks.Redis (>= 9.0.0)
- MessagePack (>= 3.1.4)
- StackExchange.Redis.Extensions.AspNetCore (>= 11.0.0)
- StackExchange.Redis.Extensions.MsgPack (>= 11.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Pandatech.DistributedCache:
| Package | Downloads |
|---|---|
|
Pandatech.SharedKernel
Opinionated ASP.NET Core 10 infrastructure kernel: OpenAPI (Swagger + Scalar), Serilog, MediatR, FluentValidation, CORS, SignalR, OpenTelemetry, health checks, maintenance mode, resilience pipelines, and shared utilities. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.0 | 91 | 2/28/2026 |
| 5.0.1 | 135 | 1/26/2026 |
| 5.0.0 | 106 | 12/28/2025 |
| 4.0.9 | 373 | 8/7/2025 |
| 4.0.8 | 286 | 6/1/2025 |
| 4.0.7 | 265 | 2/28/2025 |
| 4.0.6 | 201 | 2/17/2025 |
| 4.0.5 | 210 | 1/29/2025 |
| 4.0.4 | 185 | 1/29/2025 |
| 4.0.3 | 173 | 1/29/2025 |
| 4.0.2 | 185 | 1/28/2025 |
| 4.0.1 | 160 | 1/28/2025 |
| 4.0.0 | 154 | 1/27/2025 |
| 3.0.1 | 273 | 11/22/2024 |
| 3.0.0 | 192 | 11/21/2024 |
| 2.0.0 | 238 | 9/5/2024 |
| 1.2.3 | 222 | 8/16/2024 |
| 1.2.2 | 212 | 8/16/2024 |
| 1.2.1 | 239 | 8/16/2024 |
| 1.2.0 | 227 | 8/16/2024 |
Multi-target net9.0/net10.0, removed BuildServiceProvider anti-pattern, source-generated logging, internal sealed service implementations