Skip to content
  • English Deutsch

benjamin-abt.com

Performance is a feature | Cloud - AI - Azure - .NET - Sustainability

  • Home
  • Blog
  • Portfolio
    • Overview
    • Workshops & Training
    • Publications
  • Contact
  • Home
  • Blog

IMemoryCache Entry Invalidation (Manual Cache Busting)

IMemoryCache Entry Invalidation (Manual Cache Busting)
December 5, 2025 5 min read BEN ABT
.NET Csharp Caching Performance Architecture

IMemoryCache is great for speeding up expensive operations (database reads, HTTP calls, heavy computations). But many real systems need more than a TTL:

  • You cache a value.
  • The underlying data changes.
  • You want to invalidate the cache entry immediately.

Typical scenarios:

  • A user updates their profile → cached UserDto must be refreshed.
  • An admin flips a setting/feature → cached configuration must be cleared.
  • A background job writes a new catalog version → all catalog caches must be busted.

This post focuses on entry invalidation: how to remove (or expire) cached values on demand - without sprinkling cache.Remove(...) everywhere.

The core idea

There are three practical levels of invalidation:

  1. Direct invalidation: call cache.Remove(key) with consistent keys.
  2. Targeted invalidation: centralize invalidation behind a small service (so writes invalidate reads).
  3. Token-based invalidation: attach an IChangeToken to cache entries and cancel tokens to invalidate many entries at once (per key / per group).

Step 1: Use consistent cache keys

Before you invalidate anything, make cache keys predictable.

C#
1public static class CacheKeys
2{
3    public static string User(string userId) => $"user:{userId}";
4    public const string Catalog = "catalog";
5}

Then reads and writes share the same key functions.

Step 2: Manual invalidation with Remove() (but centralized)

If you only need to invalidate one entry at a time, the simplest option is still the best:

C#
cache.Remove(CacheKeys.User(userId));

The problem is not Remove() - it’s when you do it in 20 places.

Create a small invalidator service and use it from write paths:

C#
 1using Microsoft.Extensions.Caching.Memory;
 2
 3public interface ICacheInvalidator
 4{
 5    void InvalidateUser(string userId);
 6    void InvalidateCatalog();
 7}
 8
 9public sealed class CacheInvalidator : ICacheInvalidator
10{
11    private readonly IMemoryCache _cache;
12
13    public CacheInvalidator(IMemoryCache cache)
14    {
15        _cache = cache;
16    }
17
18    public void InvalidateUser(string userId)
19        => _cache.Remove(CacheKeys.User(userId));
20
21    public void InvalidateCatalog()
22        => _cache.Remove(CacheKeys.Catalog);
23}

This makes invalidation explicit and discoverable.

Step 3: Token-based invalidation (per key / per group)

If you want to invalidate multiple entries without tracking every key manually, use IChangeToken.

The pattern:

  • cache entries attach a token
  • you cancel the token to invalidate all entries that depend on it

An invalidation token source

C#
 1using System.Collections.Concurrent;
 2using Microsoft.Extensions.Primitives;
 3
 4public interface ICacheInvalidationTokens
 5{
 6    IChangeToken GetToken(string scope);
 7    void Invalidate(string scope);
 8}
 9
10public sealed class CacheInvalidationTokens : ICacheInvalidationTokens
11{
12    private readonly ConcurrentDictionary<string, CancellationTokenSource> _sources =
13        new(StringComparer.Ordinal);
14
15    public IChangeToken GetToken(string scope)
16    {
17        var cts = _sources.GetOrAdd(scope, static _ => new CancellationTokenSource());
18        return new CancellationChangeToken(cts.Token);
19    }
20
21    public void Invalidate(string scope)
22    {
23        if (_sources.TryRemove(scope, out var cts))
24        {
25            cts.Cancel();
26            cts.Dispose();
27        }
28    }
29}

Notes:

  • Tokens are single-use: after invalidation you remove the CTS; next GetToken(scope) creates a new one.

Using the token when caching

C#
 1using Microsoft.Extensions.Caching.Memory;
 2
 3public sealed class CatalogService
 4{
 5    private readonly IMemoryCache _cache;
 6    private readonly ICacheInvalidationTokens _tokens;
 7
 8    public CatalogService(IMemoryCache cache, ICacheInvalidationTokens tokens)
 9    {
10        _cache = cache;
11        _tokens = tokens;
12    }
13
14    public Task<Catalog> GetCatalogAsync()
15    {
16        return _cache.GetOrCreateAsync(CacheKeys.Catalog, entry =>
17        {
18            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
19            entry.AddExpirationToken(_tokens.GetToken("catalog"));
20
21            return FetchCatalogAsync();
22        })!;
23    }
24
25    public void InvalidateCatalog()
26        => _tokens.Invalidate("catalog");
27
28    private static Task<Catalog> FetchCatalogAsync()
29        => throw new NotImplementedException();
30}

Now you can invalidate the whole “catalog” scope from anywhere (admin action, webhook, background job) without knowing all keys.

Handling cache stampedes (important)

Even with invalidation, there’s a subtle problem under load:

  • Many concurrent requests see the entry as invalid/missing.
  • All of them recompute at the same time.

This is a cache stampede.

The easiest fix is to add a per-key lock (or a Lazy<Task<T>> approach) so only one request refreshes while others await.

Here’s a pragmatic keyed-lock implementation using ConcurrentDictionary + SemaphoreSlim:

C#
 1using System.Collections.Concurrent;
 2using Microsoft.Extensions.Caching.Memory;
 3
 4public sealed class InvalidatingCache
 5{
 6    private readonly IMemoryCache _cache;
 7    private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new(StringComparer.Ordinal);
 8
 9    public InvalidatingCache(IMemoryCache cache)
10    {
11        _cache = cache;
12    }
13
14    public async Task<T> GetOrCreateAsync<T>(
15        string key,
16        Func<ICacheEntry, Task<T>> factory)
17    {
18        if (_cache.TryGetValue(key, out var boxed) && boxed is T value)
19        {
20            return value;
21        }
22
23        var gate = _locks.GetOrAdd(key, static _ => new SemaphoreSlim(1, 1));
24        await gate.WaitAsync();
25
26        try
27        {
28            if (_cache.TryGetValue(key, out boxed) && boxed is T v)
29            {
30                return v;
31            }
32
33            var created = await _cache.GetOrCreateAsync(key, factory);
34            return created ?? throw new InvalidOperationException("Cache factory returned null.");
35        }
36        finally
37        {
38            gate.Release();
39        }
40    }
41}

Notes on keyed locks

  • This prevents stampedes with very little code.
  • The dictionary can grow with many distinct keys. If you cache an unbounded key space, consider a bounded lock strategy.

What should trigger invalidation?

Good invalidation triggers are events where you know a cached value is stale:

  • after a successful DB update
  • after processing an incoming event/webhook
  • after an admin setting change
  • after deploying a new version of data (catalog, rules, etc.)

Try to invalidate as close to the write as possible.

When to prefer change tokens instead

If you control invalidation events, IChangeToken-based expiration is often cleaner than on-read validation:

  • invalidate cached entries when a configuration file changes
  • invalidate when your app receives a message (“catalog updated”)

Manual invalidation shines when you can connect invalidation to your own business events.

Related articles

Top 10 recommendations for .NET developers working with Azure Cosmos DB

Jul 10, 2026 - 11 min read

Top 10 recommendations for .NET developers working with Azure Cosmos DB

Azure Cosmos DB has a habit of exposing architectural weaknesses very early. Classic data-access patterns that work perfectly fine with …

Unio: High-Performance Discriminated Unions for C#

Mar 2, 2026 - 19 min read

Unio: High-Performance Discriminated Unions for C#

C# is a powerful language, but there is one road it has not yet fully paved: native discriminated union types. Developers have been working …

Production Rate Limiting in ASP.NET Core

Nov 3, 2026 - 16 min read

Production Rate Limiting in ASP.NET Core

Most ASP.NET Core rate-limiting examples stop after registering a fixed window. That is enough to demonstrate the middleware, but it is not …


BEN ABT

BEN ABT

Ben is Chief Engineering Officer and Principal Software Engineer at Medialesson.de, with a strong focus on .NET and Microsoft Azure. In his professional role, he designs and builds highly scalable platforms for hybrid and cloud-based scenarios. He also advises companies and C-levels on migrating to the sovereign cloud.

He runs myCSharp.de, the largest and most active German-speaking C# community forum, actively contributes to both his own and external open-source projects and regularly shares his knowledge through his blog, at community events, conferences and as a writer for technical magazines.

Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms

Modernization

Training & Consulting

Contact Me
X Facebook LinkedIn WhatsApp
Back Next Blog Post

Open for work!

Contact Me
BEN ABT BEN ABT Software Engineer · Microsoft MVP Passionate about .NET, Azure, performance optimization and sustainable software engineering.

Medialesson GmbH Logo

Microsoft MVP Logo

Recent posts

View all
Production Rate Limiting in ASP.NET Core

Production Rate Limiting in ASP.NET Core

Nov 3, 2026
The Big Decimal Bugfix with .NET 11

The Big Decimal Bugfix with .NET 11

Nov 2, 2026
Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Oct 27, 2026
Stop Using nginx as Your Default Static File Server in Docker

Stop Using nginx as Your Default Static File Server in …

Oct 24, 2026
Containerizing .NET in 2026 Without a Dockerfile

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026

Tags

.NET (195) Csharp (99) ASP.NET Core (49) Azure (41) DevOps (17) Docker (15) Microsoft (14) EF Core (11) Microsoft MVP (11) Security (11) AI (10) GitHub (10) GitHub Copilot (10) ASP.NET (9) Synology (9) .NET 11 (8) Architecture (8) Raspberry (8) Azure Functions (7) Cloudflare (7)
benjamin-abt.com

About BEN

Benjamin Abt is a Software Engineer, Platform Architect and Microsoft MVP specializing in .NET, Azure, cloud architecture, performance optimization and sustainable software development.

Connect

  • Follow me on GitHub
  • Follow me on LinkedIn
  • Follow me on Mastodon
  • Microsoft MVP profile
  • Follow SchwabenCode on GitHub
  • Follow me on X

Quick Links

  • Portfolio
  • Blog
  • Workshops & Training
  • Publications
  • Get in Touch
  • Sitemap
Open for Work

© Copyright Benjamin Abt & SchwabenCode | 2011 - 2026

Content licensed under CC BY 4.0 unless otherwise noted.

  • Impressum
  • Privacy
  • Contact