A lightweight caching library for ASP.NET Core featuring group-based keys, per-entry and per-group duration, configurable via appsettings.json, and a DI-ready design that wraps IMemoryCache in a clean, testable abstraction.
Why AspNetCoreCacheKit?
IMemoryCacheis powerful but low-level. AspNetCoreCacheKit adds group-based key management, per-group and per-entry expiration,appsettings.jsonconfiguration, and a consistent type-safe API that makes caching easy to use and easy to mock in tests.
- 🔑 Group-based keys — organise cache entries with prefixes like
"users:123" - ⚡
GetOrCreateandGetOrCreateAsync— read-through pattern out of the box - 🛡️ Stampede-safe — concurrent misses on the same key are serialized, so the factory runs once instead of once per caller
- ⏱️ Per-group and per-entry duration — fine-grained expiration control
- 🗑️
Delete— remove a single entry by key or group + key - ✅ Configuration validation with DataAnnotations
- 📐 Nullable reference types and generic type-safe
Set<T>support - 🎛️
appsettings.jsonconfiguration with sensible defaults - 🧪 DI-ready — register with one line, mock
ICacheServicein tests
| Requirement | Minimum version |
|---|---|
| .NET | 10.0+ |
| ASP.NET Core | 10.0+ |
dotnet add package AspNetCoreCacheKit{
"CacheOptions": {
"IsEnabled": true,
"DurationMinutes": 60,
"GroupDurations": {
"users": 30,
"tokens": 5,
"countries": 1440
}
}
}All duration values are expressed in minutes.
GroupDurationsis optional — groups without an entry use the globalDurationMinutes.
// With appsettings.json
builder.Services.AddAspNetCoreCacheKit(builder.Configuration);
// Programmatically, no appsettings.json needed
builder.Services.AddAspNetCoreCacheKit(options =>
{
options.DurationMinutes = 30;
options.GroupDurations["users"] = 10;
});
// Without appsettings (uses defaults)
builder.Services.AddAspNetCoreCacheKit();[ApiController]
public class UsersController : ControllerBase
{
private readonly ICacheService _cache;
public UsersController(ICacheService cache)
{
_cache = cache;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id, CancellationToken ct)
{
// Uses GroupDurations["users"] = 30 min from appsettings
var user = await _cache.GetOrCreateAsync(
"users",
id.ToString(),
_ => GetUserFromDb(id),
cancellationToken: ct);
return user is null ? NotFound() : Ok(user);
}
}Note:
keymust not be null or empty on any method —ArgumentExceptionis thrown otherwise.groupKeymay be omitted or empty (falls back to the global duration).
Every method that writes to the cache resolves the expiration following this priority chain — first wins:
durationparameter passed explicitly to the methodGroupDurations[groupKey]configured inappsettings.json- Global
DurationMinutesfromappsettings.json
// 1. Explicit duration wins — cached for 2 minutes regardless of everything else
await _cache.GetOrCreateAsync("tokens", userId, _ => GenerateTokenAsync(), TimeSpan.FromMinutes(2));
// 2. No explicit duration — uses GroupDurations["users"] = 30 min from appsettings
await _cache.GetOrCreateAsync("users", "42", _ => GetUserFromDb(42));
// 3. No explicit duration, no group config — uses global DurationMinutes = 60 min
await _cache.GetOrCreateAsync("misc", "key", _ => LoadSomethingAsync());// With group — uses GroupDurations["users"] or global fallback
var user = await _cache.GetOrCreateAsync("users", "42", _ => GetUserFromDb(42), cancellationToken: ct);
// With group + explicit duration override
var token = await _cache.GetOrCreateAsync(
"tokens", userId,
_ => GenerateTokenAsync(userId),
duration: TimeSpan.FromMinutes(5),
ct);
// Without group key
var config = await _cache.GetOrCreateAsync("app:config", _ => LoadConfigAsync(), cancellationToken: ct);// Uses GroupDurations["countries"] = 1440 min from appsettings
var countries = _cache.GetOrCreate("countries", () => LoadCountries());
// Explicit override
var result = _cache.GetOrCreate("countries", "IT", () => LoadItaly(), TimeSpan.FromHours(2));_cache.Set("users", "42", user); // group duration or global
_cache.Set("users", "42", user, TimeSpan.FromMinutes(10)); // explicit override_cache.Delete("users", "42");
_cache.Delete("app:config");| Option | Type | Default | Description |
|---|---|---|---|
IsEnabled |
bool |
true |
Enables or disables caching entirely. When false, factories are always invoked. |
DurationMinutes |
int |
60 |
Global default cache duration in minutes. |
GroupDurations |
Dictionary<string, int> |
{} |
Per-group durations in minutes. Key = group name, Value = duration in minutes. |
Setting IsEnabled: false is useful in development or testing environments where you want to bypass the cache without changing code.
Note:
CacheOptionsis bound once at startup (IOptions<CacheOptions>) and captured by the singletonICacheService. Changes toappsettings.jsonat runtime require an application restart to take effect.
ICacheService is a plain interface — mock it directly in unit tests:
var cacheMock = new Mock<ICacheService>();
cacheMock
.Setup(c => c.GetOrCreateAsync(
"users", "1",
It.IsAny<Func<ICacheEntry, Task<User>>>(),
It.IsAny<TimeSpan?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new User { Id = 1, Name = "Simone" });AspNetCoreCacheKit is part of a cohesive set of lightweight, DI-ready toolkits for ASP.NET Core:
| Package | Description |
|---|---|
| AspNetCoreAuthKit | JWT, API Key, Refresh Tokens with rotation & dynamic claim policies |
| AspNetCoreResponseKit | Standardized ApiResponse<T>, global exception handling & smart factories |
| AspNetCoreHttpKit | Typed HTTP client with result pattern & StructLog integration |
| AspNetCoreCacheKit | Group-based caching with per-entry & per-group duration |
| StructLog | Structured logging with EventCode support & custom enrichers |
Each package works independently — use one or all five.
If you find AspNetCoreCacheKit useful, consider sponsoring its development.
MIT — see LICENSE for details.