CacheLens
v0.1.1 · live on the VS Code Marketplace

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.

CACHELENS CacheLens.Sample (9844)
4 entries 0 hits 13.1 KB tracked

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.

Today Program.cs
// 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 _));
});
With CacheLens Program.cs
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.

Playing automatically
Terminal bash

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.

Not published yet Build from source

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 Marketplace

Safety

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.

Race condition

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.

Wrong premise

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.

Protocol drift

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.

  • WorksIMemoryCache tracking, 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
  • LaterIDistributedCache and HybridCache, including Redis SCAN
  • LaterZero-install attach via EventPipe, with reduced fidelity