Multilayer caching for .NET — L1 in-memory + L2 Redis, cross-node sync over Redis Streams, single-flight stampede protection, hydrating cache, and CloudEvents — behind a small, opinionated DI surface.
The library powers caching in UiPath Platform services. It is built for multi-tenant workloads that need a hot in-process tier, a shared Redis tier, and cross-node coherence — without each service reinventing the same patterns.
dotnet add package UiPath.Caching
dotnet add package UiPath.Caching.Polly
dotnet add package UiPath.Caching.CloudEventsWire it once in Program.cs:
using UiPath.Caching;
using UiPath.Caching.CloudEvents;
using UiPath.Caching.Config;
using UiPath.Caching.Polly;
var section = builder.Configuration.GetSection("Caching");
builder.Services.AddCaching(
section,
b => b.AddRedisConnection().AddBroadcast()
.AddRedis().AddInMemoryRedis().AddMemory()
.AddResilienceStrategies().AddCloudEvents(),
o => { section.Bind(o); o.AppShortName = "my-service"; });Inject and call:
public class UserService(ICache<User> cache, IUserRepository repo)
{
public ValueTask<User?> GetAsync(int id, CancellationToken ct) =>
cache.GetOrAddAsync(id.ToString(), c => repo.LoadAsync(id, c),
TimeSpan.FromMinutes(5), ct);
}Minimum appsettings.json:
{
"Caching": {
"AppShortName": "my-service",
"Connections": {
"Redis": { "ConnectionString": "localhost:6379" }
}
}
}That's it. Everything else has a sensible default. Five-minute onboarding lives in docs/quickstart.md; the full settings reference is in docs/reference/settings.md.
Resiliency
- Single-flight stampede protection — one factory runs per key on a cache miss; other callers coalesce. In-process via
ILocalLock(default on); cross-node viaIDistributedLock(AddInMemoryRedis()registers the impl automatically — opt in withDistributedLockEnabled: true;AddMemory()-only or custom wiring needs an explicit.AddRedisDistributedLock()). - Hydrating cache — proactive background refresh before a key expires (
CachePolicy.RehydrateEnabled+RehydrateOptions). Avoids the expiry-miss path on hot keys so reads against still-live entries don't pay the factory cost. First-time misses still run the factory on the foreground caller. - Polly pipelines — retry + circuit-breaker around every cache op (
AddResilienceStrategies). Configurable per-op timeout, break duration, and rethrow behavior. - Connection-aware L1 fallback —
UseLocalOnlyWhenDisconnected+LocalMaxExpirationDisconnectedkeep the cache useful when Redis blips.
Distribution
- Multilayer L1 + L2 — in-process MemoryCache + Redis, written together, read L1-first. Different TTLs per tier.
- Redis Streams backplane — durable, at-least-once cross-node L1 invalidation with consumer groups and replay. Fallback to Pub/Sub if you don't need durability.
- Per-topic options — fine-grained overrides for fan-out, polling, backpressure, and the streams notify doorbell.
- Sharded Pub/Sub doorbell (Redis 7+) —
NotifyEnabled+NotifyShardedPubSubdrop publish-to-deliver latency fromPollIntervalto network RTT without leaving the shard. - Redis Cluster aware — shard-key routing, master/replica split (writes to master, reads from replicas).
Advanced
IHashCache<T>— one entry, many fields, one TTL. Subset reads viaHMGET; side-channel metadata viaHashCacheEntryOptions. BundledGET + TTLin a single transaction for hydrating-cache math.- Named cache policies — per-
ICache<T>settings keyed bytypeof(T).FullName(TTLs, jitter, rehydration, lock profile). Inherits fromDefaultCachePolicywithnull-means-inherit semantics. - L2 jitter —
JitterMaxDurationspreads cluster-wide expirations after bulk writes. - Large-value auditing —
AuditEnabled+LargeValueThresholdlog writes over a byte threshold.
Observability
ICachingTelemetryProviderseam with span-based tag bags (allocation-free when telemetry is off). An OpenTelemetry adapter (ActivitySource+Meter) ships asUiPath.Caching.OpenTelemetry; OpenTelemetry Redis instrumentation wires up via theIConnectionMultiplexerFactoryhook (see the sample).- CloudEvents — broadcast events wrapped in the CNCF CloudEvents envelope (
UiPath.Caching.CloudEvents).
flowchart LR
App["App code\n(ICache<T> / IHashCache<T>)"] --> ML[MultilayerCache]
ML -->|read first| L1[(L1: in-process)]
ML -->|miss / write| L2[(L2: Redis)]
ML -. wraps .-> SF[Single-flight\nlock]
ML -. wraps .-> POL[Polly pipeline]
L2 -.write fan-out.-> TOPIC{{Redis Streams\nbackplane}}
TOPIC -->|invalidation event| OtherL1[(Other nodes' L1)]
OtherL1 -.evicts.-> ML2[MultilayerCache on node B]
InMemoryRedis is the default provider — multilayer with the backplane wired. Redis (L2-only) and InMemory (L1-only, optional broadcast) are available for single-tier scenarios. See docs/concepts.md for the full mental model.
| Package | When you need it |
|---|---|
UiPath.Caching.Abstractions |
Always (transitive). |
UiPath.Caching |
Always — providers, topics, locks. |
UiPath.Caching.Polly |
Resilience pipelines. Recommended. |
UiPath.Caching.CloudEvents |
CloudEvents envelope for broadcast events. Recommended. |
UiPath.Caching.Queue |
Redis set ("queue") support — ISetCache / AddRedisSetCache. |
| Topic | Doc |
|---|---|
| 5-minute onboarding | docs/quickstart.md |
| Architecture & mental model | docs/concepts.md |
| Resilience (single-flight, hydrating, jitter, Polly) | docs/how-to/resilience.md |
| Broadcast (per-topic, notify doorbell, sharded Pub/Sub) | docs/how-to/broadcast.md |
| Hash cache (metadata, bundled GET+TTL) | docs/how-to/hash-cache.md |
| Telemetry + custom strategies | docs/how-to/telemetry-and-strategies.md |
| Short copy-paste patterns | docs/recipes/ |
| Settings reference (every option, every default) | docs/reference/settings.md |
| Public interface surface | docs/reference/interfaces.md |
| Glossary | docs/reference/glossary.md |
| Sample app (Aspire AppHost) | docs/sample-app.md |
| Changelog | CHANGELOG.md |
- .NET — 8.0 and 10.0.
- Redis — 6.0+. Redis 7.0+ required for sharded Pub/Sub (
SPUBLISH/SSUBSCRIBE) on the streams notify doorbell. - StackExchange.Redis — pulled transitively; no direct dependency needed in consumers.
MIT.