OrionCache
Cache-aside done right. One GetOrCreateAsync that never stampedes — the factory runs exactly once even under a burst of concurrent misses for the same key — with expiration driven by an OrionClock TimeProvider so it is deterministic in tests, and OpenTelemetry by default.
Every service re-implements the same three-line cache-aside and gets it wrong the same way: a hot key expires, 500 concurrent requests all miss, all hit the database, all recompute the same value. IMemoryCache gives you Get/Set but no coordination — the thundering herd is left as an exercise, and everyone hand-rolls a SemaphoreSlim-per-key single-flight (usually with a lock leak or a race). OrionCache absorbs that: the stampede protection and the deterministic clock are the product, not a TODO.
Features
- Single-flight
GetOrCreateAsync— under concurrent misses for one key the factory runs once; the losers await the winner's result. (Verified by a test firing 1,000 parallel calls on a cold key and asserting the factory ran exactly once.) - Deterministic expiration on
OrionClock— absolute and sliding TTLs are measured on the family clock, so aFakeOrionClockexpires entries with no real waiting and no flakiness. A stale value is never returned. - Tag invalidation — group related in-memory entries and expire them together, including entries whose factory was in flight during the invalidation.
Option<T>reads —TryGetAsyncreturns anOption<T>from OrionResult, not the(bool, out T)dance.- OpenTelemetry by default — a
Moongazing.OrionCachemeter withorion.cache.hits,orion.cache.misses,orion.cache.factory_runs, andorion.cache.stampede_waits. - AOT- and trim-clean, verified by a native-binary smoke test in CI. Multi-targets
net8.0,net9.0,net10.0.
Install
dotnet add package OrionCache
Usage
using Moongazing.OrionCache;
using Moongazing.OrionCache.DependencyInjection;
services.AddOrionCache(o =>
{
o.DefaultExpiration = TimeSpan.FromMinutes(5);
o.EnableStampedeProtection = true;
});
public sealed class ProductService(IOrionCache cache, AppDb db)
{
public Task<Product?> GetAsync(int id, CancellationToken ct) =>
cache.GetOrCreateAsync(
key: $"product:{id}",
factory: async _ => await db.Products.FindAsync([id], ct),
options: new CacheEntryOptions { Expiration = TimeSpan.FromMinutes(10) },
ct);
}
Tag invalidation
var cache = services.GetRequiredService<ITaggedOrionCache>();
await cache.SetAsync("product:42", product,
new CacheEntryOptions { Tags = ["catalog", "product:42"] });
await cache.InvalidateTagAsync("product:42");
Tag names are case-sensitive; each entry accepts up to 32 tag values. Invalidation affects only
the current cache instance, not other application instances. A factory already in flight may return
its value to its current caller, but its invalidated tagged result will not be served from the cache.
An existing cached value is not retroactively assigned tags by a later GetOrCreateAsync call.
Testing — expiration fast-forwards, no real waits
Because TTLs are measured on OrionClock, a FakeOrionClock advances a whole expiration window instantly and deterministically:
var clock = new FakeOrionClock();
var cache = new MemoryOrionCache(new MemoryCache(new MemoryCacheOptions()), clock,
new CacheDiagnostics(), Options.Create(new OrionCacheOptions()));
var calls = 0;
await cache.GetOrCreateAsync("k", _ => { calls++; return Task.FromResult(1); },
new CacheEntryOptions { Expiration = TimeSpan.FromMinutes(5) });
clock.Advance(TimeSpan.FromMinutes(4));
await cache.GetOrCreateAsync("k", _ => { calls++; return Task.FromResult(2); }); // still cached
clock.Advance(TimeSpan.FromMinutes(2)); // now +6m, expired
await cache.GetOrCreateAsync("k", _ => { calls++; return Task.FromResult(3); }); // factory runs again
Assert.Equal(2, calls);
Roadmap
The in-memory cache-aside core now includes single-flight, OrionClock-driven expiration, and local tag invalidation. Later waves add a Redis L2 with single-flight escalated to an OrionLock lease (once per key across instances), factory failures wrapped through OrionResilience, an ASP.NET output-caching provider, and a coherent L1+L2 two-tier cache with backplane invalidation. See CHANGELOG.md.
OrionCache orchestrates over memory/Redis; it does not reimplement Redis, does no implicit query-result caching (cache-aside is explicit by design), and is not a session store. Serialization (when the Redis L2 arrives) is System.Text.Json source-gen only.
Versioning
Version 1.0.0 stabilizes the in-memory cache-aside API, including single-flight, expiration,
and local tag invalidation. It does not promise distributed cache consistency. OrionCache follows
Semantic Versioning and targets net8.0, net9.0, and net10.0.
Its current dependencies include Orion.Abstractions 1.x, OrionClock 0.9.x, and
OrionResult 0.9.x; applications should test dependency upgrades independently.
Documentation
- CHANGELOG.md — release notes.
Contributing
Contributions are welcome. See CONTRIBUTING.md and the CODE_OF_CONDUCT.md.
More from the Orion family
Focused .NET libraries built to one quality bar. Each is usable on its own; several share the small Orion.Abstractions contracts spine, but there is no deep dependency web — pick only what you need:
- Orion.Abstractions — the shared contracts spine: telemetry, options, result, clock
- OrionClock — a
TimeProvider-based clock with TTL / deadline vocabulary - OrionResult — Result/Option types and a shared error vocabulary
- OrionLock — distributed locks with fencing tokens (single-flight backend, later wave)
- OrionResilience — retry, backoff, and timeout on OrionClock
- OrionRate — rate limiting on OrionClock
- OrionPage — keyset/cursor pagination for EF Core
- OrionEnvelope — one HTTP contract: envelope + problem+json
- OrionGuard — validation, guard clauses, DDD primitives, domain events
- OrionAudit — automatic EF Core change-audit trail
- OrionBeacon — leader election with fencing tokens
- OrionGrant — permission / authorization checks
- OrionInbox — transactional inbox for exactly-once effects
- OrionKey — source-generated strongly-typed IDs
- OrionLedger — API-key issuance, verification, and rotation
- OrionLens — ambient correlation-context propagation
- OrionOnce — idempotency keys for exactly-once request handling
- OrionPatch — transactional outbox for EF Core
- OrionRelay — outbound webhook delivery (HMAC, retries, backoff)
- OrionSaga — sagas / process managers for long-running workflows
- OrionShade — sensitive-data redaction for logs and telemetry
- OrionStream — server-sent events / streaming hub
- OrionVault — field-level encryption for EF Core
See it all working together in OrionShowcase, a production-shaped banking sample.
License
MIT.
Packages
| Package | Version | Downloads |
|---|---|---|
| OrionCache | 1.0.0 | 51 |