Your .NET cache is a black box.
IMemoryCache won't tell you what it's holding — not the values, not what's
about to expire, not which keys anyone actually reads. So you guess, or you bolt on a
debug endpoint and keep a parallel key list in sync by hand.
CacheLens just shows you — live, in VS Code, without touching a single
call site.
The gap
A list of keys is not a cache viewer.
.NET 9 added MemoryCache.Keys, and it's genuinely useful — but it hands
you bare key objects and stops there. No values, no expiry, no sizes, no per-key hit
counts. It sits on the concrete class rather than the IMemoryCache your code
is handed by DI, and on .NET 8 it doesn't exist at all. So every team still builds
the same workaround — and it drifts out of sync the moment an entry expires on its own.
// Keep a shadow copy of every key you ever set…
private static readonly HashSet<string> _keys = new();
app.MapGet("/debug/cache", (IMemoryCache cache) =>
{
// …then hope it still matches reality.
// Evictions don't tell you. TTLs aren't here.
// Sizes aren't here. It's stale on arrival.
return _keys.Where(k => cache.TryGetValue(k, out _));
});
if (builder.Environment.IsDevelopment())
{
builder.Services.AddCacheLens();
}
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapCacheLens();
}
// That's it. Your Set/Get/GetOrCreate calls
// stay exactly as they are.
What the lens shows
Every key, and everything about it.
CacheLens wraps your cache registration with a decorator that keeps its own index — so it reports what's genuinely there, including entries that expired on their own while you weren't looking.
Real keys, real values
Browse every tracked key and read its value as formatted JSON — the way you'd scan keys in a Redis viewer.
Expiry you can watch
Absolute and sliding expirations render as live countdowns, so you can see an entry go cold instead of guessing why a request got slow.
Hit counts per key
See which keys actually earn their place and which ones are cached for nobody — per entry, not just an aggregate ratio.
Secrets stay secret
Keys that look secret-shaped never send their value to the editor. You see the entry exists and how big it is — never what's in it.
Evict without a restart
Drop one key or clear the lot, straight from the tree, then hit the endpoint again to watch it repopulate.
Export a snapshot
Save the whole cache state to JSON — useful for a bug report, or for diffing what changed between two runs.
Setup
Four steps. Watch it happen.
Your app writes a discovery file when it starts, and the extension watches for it — so there's no host, port, or token to copy anywhere. Step through it below.
Get it
Two halves, one install each.
The package instruments your app; the extension reads it. You need both — they version independently and negotiate a protocol version on connect.
NuGet package
CacheLens.AspNetCore — wraps your IMemoryCache and serves a
loopback-only endpoint. Targets .NET 8 and 9.
VS Code extension
The viewer: tree of live keys, value inspector, evict and export. Search CacheLens in the Extensions panel, or install from the Marketplace.
Available now Install from MarketplaceSafety
A cache viewer is a data leak waiting to happen.
So CacheLens is built to fail closed. These aren't settings you have to remember to turn on — they're the defaults.
Off outside development
The usual wiring gates registration behind IsDevelopment(), so it never reaches production by accident.
Loopback only
Every request is checked against the loopback interface regardless of what address Kestrel is bound to. Remote callers get a 403.
A fresh token every run
Each process generates its own bearer token and writes it to the discovery file. The extension reads it; you never type it.
Secret-shaped keys are redacted
Keys containing password, token, secret and friends send metadata only. The list is yours to extend.
Large values stay put
Anything over 64 KB is reported by size instead of being serialized and shipped to your editor.
Engineering notes
What broke, and what we did about it.
A cache viewer is only worth having if you can trust what it shows you. These are the real problems found while building it — including one where the thing we were wrong about was our own pitch.
Entries vanished from the view while still cached
The problem
MemoryCache fires eviction callbacks on a thread-pool thread, not
inline. When a key expired and was immediately recreated, the old entry's late
callback removed the new entry from our index — so a key that was very
much in the cache disappeared from the panel.
The fix
An identity-checked removal: only drop the entry if the index still holds
that exact instance, using ConcurrentDictionary's
compare-and-remove overload. A key replaced in the meantime is now left alone.
Caught by expiring and recreating one key in a tight loop: 30 of 200 iterations failed before the fix, 0 of 200 after.
Our own headline claim was out of date
The problem
This page used to say Microsoft never shipped a way to look inside
IMemoryCache. Checking it properly showed that .NET 9 added
MemoryCache.Keys — so the claim was simply false, and it was sitting
on a listing Microsoft reviews.
The fix
Rewrote the claim to what is actually true: Keys returns bare key
objects and stops there — no values, expiry, sizes or per-key hits, on the concrete
class rather than the injected interface, and absent on .NET 8.
Verified by compiling against each framework, not by reading docs: net8.0 → compile error, net9.0 → compiles, the interface → still no Keys.
The two halves disagreed on JSON casing
The problem
The discovery file was written in PascalCase while the HTTP endpoints
answered in camelCase. Any client written against one would silently
misread the other — the kind of bug that surfaces as an empty panel with no error.
The fix
Both now use the same serializer defaults, so one set of field names covers the
whole protocol. The /meta handshake also carries a version number, so
a genuine mismatch says so plainly instead of failing quietly.
Honest status
What works, and what doesn't yet.
This is pre-release and nothing is published. Here's exactly where it stands, so you can judge whether it's worth your time today.
- Works
IMemoryCachetracking, values, TTLs, hit counts, evict and clear - WorksZero-config discovery, the VS Code tree, and the value inspector
- NextLive push updates over WebSocket — the extension polls every few seconds today
- WorksPublished on the VS Code Marketplace
- NextPublishing to NuGet and Open VSX
- LaterA dashboard view — every cached key across your whole project alongside the endpoints that populate them, plus exportable reports
- Later
IDistributedCacheandHybridCache, including RedisSCAN - LaterZero-install attach via EventPipe, with reduced fidelity