NexGen.MediatR.Extensions.Caching
2.0.0
See the version list below for details.
dotnet add package NexGen.MediatR.Extensions.Caching --version 2.0.0
NuGet\Install-Package NexGen.MediatR.Extensions.Caching -Version 2.0.0
<PackageReference Include="NexGen.MediatR.Extensions.Caching" Version="2.0.0" />
<PackageVersion Include="NexGen.MediatR.Extensions.Caching" Version="2.0.0" />
<PackageReference Include="NexGen.MediatR.Extensions.Caching" />
paket add NexGen.MediatR.Extensions.Caching --version 2.0.0
#r "nuget: NexGen.MediatR.Extensions.Caching, 2.0.0"
#:package NexGen.MediatR.Extensions.Caching@2.0.0
#addin nuget:?package=NexGen.MediatR.Extensions.Caching&version=2.0.0
#tool nuget:?package=NexGen.MediatR.Extensions.Caching&version=2.0.0
NexGen.MediatR.Extensions.Caching
<p align="center"> <img src="https://raw.githubusercontent.com/MatinGhanbari/NexGen.MediatR.Extensions.Caching/main/assets/images/logo.png" alt="NexGen.MediatR.Extensions.Caching" width="280" /> </p>
<p align="center"> <strong>MediatR output caching</strong> with pipeline behaviors, tag-based invalidation, and optional Entity Framework auto-eviction. </p>
<p align="center"> <a href="https://github.com/MatinGhanbari/NexGen.MediatR.Extensions.Caching/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/MatinGhanbari/NexGen.MediatR.Extensions.Caching/.github%2Fworkflows%2Fci.yml?style=flat-square&label=CI" alt="CI" /></a> <a href="https://www.nuget.org/packages/NexGen.MediatR.Extensions.Caching"><img src="https://img.shields.io/nuget/v/NexGen.MediatR.Extensions.Caching.svg?style=flat-square" alt="NuGet" /></a> <a href="https://www.nuget.org/packages/NexGen.MediatR.Extensions.Caching"><img src="https://img.shields.io/nuget/dt/NexGen.MediatR.Extensions.Caching?style=flat-square" alt="Downloads" /></a> <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License" /></a> <img src="https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4?style=flat-square" alt=".NET 8 | 9 | 10" /> </p>
Table of contents
- About
- Features
- Packages
- Requirements
- Installation
- Quick start
- Configuration
- Caching requests
- Invalidation
- How it works
- Examples
- Samples and benchmarks
- Changelog
- Contributing
- License
About
NexGen.MediatR.Extensions.Caching extends MediatR with opt-in response caching as a cross-cutting concern. Mark a request with [RequestOutputCache], and a pipeline behavior serves cached responses on hits and stores results on misses.
Invalidation is tag-based: associate tags with cached requests, then evict by tag with [RequestOutputCacheEvict], manually, or automatically when Entity Framework Core saves related entity changes. In-memory cache is local to one process. Redis and Garnet add Pub/Sub so other hosts sharing the same cache prefix drop the same tags.
Features
| Feature | Description |
|---|---|
| Opt-in attribute caching | Only requests decorated with [RequestOutputCache] are cached; unmarked requests pass through unchanged. |
| MediatR pipeline behavior | Transparent get / miss / set flow via RequestOutputCacheBehavior<,> — no changes inside handlers for cache hits. |
| Multi-target frameworks | Ships net8.0, net9.0, and net10.0 in one NuGet package set. |
| In-memory provider | Built into the core package using IMemoryCache for local and development scenarios. |
| Redis provider | Distributed cache via IDistributedCache + StackExchange.Redis (NexGen.MediatR.Extensions.Caching.Redis). |
| Garnet provider | Distributed Garnet-compatible provider mirrored with Redis (NexGen.MediatR.Extensions.Caching.Garnet). |
| Tag-based invalidation | Group related cache entries with tags and evict with EvictByTagsAsync or [RequestOutputCacheEvict]. |
| EF Core auto-evict | On SaveChanges / SaveChangesAsync, evict tags matching changed entity type names (UseMediatROutputCacheAutoEvict). |
| Redis / Garnet Pub/Sub | Distributed tag eviction across CQRS hosts and microservices (on by default; set EnableDistributedEviction = false to opt out). |
| Command eviction attribute | [RequestOutputCacheEvict(tag1, tag2, ...)] invalidates those tags after a successful handler. |
| Deterministic cache keys | Key = NexGen.MediatR.Extensions:{Namespace:segments}:{TypeName}:{SHA-256(JSON)} — namespaced, Redis-tree friendly, collision-safe across namespaces. |
| Per-request expiration | expirationInSeconds on the attribute (default 300); 0 means no absolute expiration. Provider DefaultExpirationInSeconds can replace the library default when the attribute omits an explicit value. |
| Flush all | IRequestOutputCacheInvalidator.FlushAll clears the entire cache store for the provider. |
| Clear on startup | Optional ClearCacheOnStartup() during DI configuration. |
| FluentResults | Cache get/set/evict APIs return Result / Result<T> for success and failure paths. |
| ASP.NET Core DI | Integrates with IServiceCollection and standard Microsoft.Extensions.Caching abstractions. |
| Enterprise packaging | Central Package Management, SourceLink, symbol packages (.snupkg), XML docs on public APIs. |
Packages
| Package | Role |
|---|---|
NexGen.MediatR.Extensions.Caching |
Core: attribute, behavior, contracts, in-memory provider |
NexGen.MediatR.Extensions.Caching.Redis |
Redis distributed provider |
NexGen.MediatR.Extensions.Caching.Garnet |
Garnet distributed provider |
NexGen.MediatR.Extensions.Caching.EntityFramework |
EF Core ChangeTracker auto-eviction |
All four packages share the same version (lockstep releases).
Requirements
- .NET 8, .NET 9, or .NET 10
- MediatR (registered in your app as usual)
- Optional: Redis/Garnet for distributed cache; EF Core for auto-evict
Installation
Core
dotnet add package NexGen.MediatR.Extensions.Caching
Providers (as needed)
dotnet add package NexGen.MediatR.Extensions.Caching.Redis
dotnet add package NexGen.MediatR.Extensions.Caching.Garnet
dotnet add package NexGen.MediatR.Extensions.Caching.EntityFramework
Or via Package Manager Console:
Install-Package NexGen.MediatR.Extensions.Caching
Quick start
// Program.cs
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseMemoryCache();
});
[RequestOutputCache(tags: ["weather"], expirationInSeconds: 300)]
public sealed class WeatherForecastRequest : IRequest<IEnumerable<WeatherForecastDto>>
{
public int Limit { get; set; } = 10;
}
Send the request through MediatR as usual; the first call executes the handler and caches the response. Later identical requests (same type + payload) are served from cache until expiration or eviction.
Configuration
Register one cache provider via AddMediatROutputCache. Configuring more than one throws.
In-memory cache
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseMemoryCache();
});
In-memory cache is process-local. [RequestOutputCacheEvict] and EF auto-evict run only in this host. Cross-service or split CQRS invalidation is not supported with the memory provider — use Redis or Garnet for that.
Optional provider defaults (applied when the attribute omits an explicit expirationInSeconds, i.e. uses the library constant 300):
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseMemoryCache(o => o.DefaultExpirationInSeconds = 600);
});
Redis
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseRedisCache("localhost:6379,password=YourRedisPassword");
});
Provider-specific options (InstanceName, Database, default TTL, or advanced ConfigurationOptions):
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseRedisCache(o =>
{
o.ConnectionString = builder.Configuration.GetConnectionString("Redis")!;
o.InstanceName = "my-app:";
o.Database = 1;
o.DefaultExpirationInSeconds = 300;
});
});
Multiple apps on one Redis: set a distinct
InstanceName(and/orDatabase) per service. CLR namespaces in response cache keys do not isolate the shared container index keys (…:Container:*). Without a prefix, apps share that metadata on the same database.
Garnet
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseGarnetCache("localhost:6379,password=YourGarnetPassword");
});
Same nested options pattern as Redis via UseGarnetCache(Action<GarnetRequestOutputCacheOptions>). Use a distinct InstanceName / Database when multiple apps share one Garnet instance (same guidance as Redis above).
TTL precedence: an explicit
expirationInSecondson[RequestOutputCache]always wins (including0for never expire). ProviderDefaultExpirationInSecondsonly replaces the library default when the attribute uses the constructor default (300). Explicit300is indistinguishable from that default.
Entity Framework auto-evict
After a successful SaveChanges / SaveChangesAsync, the interceptor collects distinct entity CLR type names and invalidates those tags. Request tags must match (typically nameof(YourEntity)). With Redis or Garnet, the same tags are also published on Pub/Sub when distributed eviction is enabled.
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(connectionString);
options.UseMediatROutputCacheAutoEvict(sp);
});
Distributed eviction (Redis / Garnet)
Query and command hosts use the same registration. No second DI entry point and no message bus.
// every host
builder.Services.AddMediatROutputCache(opt =>
opt.UseRedisCache(o =>
{
o.ConnectionString = builder.Configuration.GetConnectionString("Redis")!;
o.InstanceName = "my-app:";
// o.EnableDistributedEviction = true; // default
}));
[RequestOutputCache(tags: [nameof(User)], expirationInSeconds: 300)]
public sealed record GetUsersQuery : IRequest<Result<List<UserDto>>>;
[RequestOutputCacheEvict(nameof(User), nameof(Order), "dashboard-stats")]
public sealed record CreateUserCommand(string Name) : IRequest<Result>;
| Setting | Behavior |
|---|---|
EnableDistributedEviction |
Default true. Publishes and subscribes on a shared Redis/Garnet channel so other hosts evict the same tags. Set false to keep eviction in this process only. |
EvictionChannel |
Optional. Defaults to NexGen.MediatR.Extensions.Caching:Evict, prefixed by InstanceName when set. |
InstanceName |
Isolates cache keys and the eviction channel so co-tenant apps do not cross-evict. |
The publishing host evicts locally first, then notifies others. Each host ignores its own Pub/Sub echo. Redis Pub/Sub is at-most-once; a missed message is repaired by TTL.
UseGarnetCache mirrors the same options (EnableDistributedEviction, EvictionChannel, InstanceName).
Clear cache on startup
builder.Services.AddMediatROutputCache(opt =>
{
opt.UseMemoryCache();
opt.ClearCacheOnStartup();
});
Caching requests
Apply [RequestOutputCache] on the request type (the class that implements IRequest<TResponse>).
Note:
TResponseshould be a reference type (class, record, or interface), consistent with typical MediatR query responses.
Important: For EF auto-evict, include
nameoffor every related entity type intags.
[RequestOutputCache(
tags: ["weather", nameof(WeatherForecastDbEntity)],
expirationInSeconds: 3600)]
public sealed class WeatherForecastRequest : IRequest<IEnumerable<WeatherForecastDto>>
{
public int Limit { get; set; } = 10;
public int Offset { get; set; } = 0;
}
| Attribute parameter | Behavior |
|---|---|
tags |
Labels for grouping and invalidation |
expirationInSeconds |
Absolute lifetime in seconds. Default: 300. Use 0 for no absolute expiration |
Invalidation
Manual (by tags)
Inject IRequestOutputCacheInvalidator or IRequestOutputCache<TRequest, TResponse>:
public sealed class WeatherForecastUpdateHandler(
IRequestOutputCacheInvalidator cache)
: IRequestHandler<WeatherForecastUpdateRequest, string>
{
public async Task<string> Handle(
WeatherForecastUpdateRequest request,
CancellationToken cancellationToken)
{
await cache.EvictByTagsAsync(["weather"], cancellationToken);
return "Evicted";
}
}
Flush everything
await cache.FlushAll(cancellationToken);
Automatic (EF Core)
When UseMediatROutputCacheAutoEvict is configured, you usually do not need manual eviction for data that changes through that DbContext. With Redis or Garnet, the interceptor also notifies other hosts when distributed eviction is enabled.
Command attribute
Eviction runs only after the handler returns successfully. A thrown exception or a failed FluentResults IResultBase skips eviction. Pass any number of tags; they are sent in one call.
[RequestOutputCacheEvict(nameof(User), nameof(Order), "dashboard-stats")]
public sealed record CreateUserCommand(string Name) : IRequest<Result>;
How it works
[RequestOutputCache] → RequestOutputCacheBehavior
│
├─ hit → return cached TResponse
└─ miss → handler → store → return TResponse
[RequestOutputCacheEvict] → handler succeeds → RequestOutputCacheEvictionDispatcher
├─ local EvictByTagsAsync
└─ Redis/Garnet Pub/Sub (when enabled) → other hosts EvictByTagsAsync
Key: NexGen.MediatR.Extensions:{Namespace:with:colons}:{TypeName}:{sha256(json(request))}
Index: tag → request types → cache keys (via IRequestOutputCacheContainer)
Evict: EvictByTagsAsync(tags)
or EF ChangeTracker → entity type Name as tags
Distributed providers (Redis / Garnet) also keep request→response type metadata so payloads can be deserialized correctly across nodes.
Examples
Cache a query
[RequestOutputCache(tags: ["weather"], expirationInSeconds: 300)]
public sealed class WeatherForecastRequest : IRequest<IEnumerable<WeatherForecastDto>>
{
public int Limit { get; set; } = 10;
}
public sealed class WeatherForecastRequestHandler
: IRequestHandler<WeatherForecastRequest, IEnumerable<WeatherForecastDto>>
{
public async Task<IEnumerable<WeatherForecastDto>> Handle(
WeatherForecastRequest request,
CancellationToken cancellationToken)
{
await Task.Delay(2000, cancellationToken); // simulate work
// ... build and return forecast list
return [];
}
}
Invalidate after an update
[RequestOutputCacheEvict("weather")]
public sealed class WeatherForecastUpdateRequest : IRequest<string>;
Or call the invalidator from a handler:
public sealed class WeatherForecastUpdateRequestHandler(
IRequestOutputCacheInvalidator cache)
: IRequestHandler<WeatherForecastUpdateRequest, string>
{
public async Task<string> Handle(
WeatherForecastUpdateRequest request,
CancellationToken cancellationToken)
{
await cache.EvictByTagsAsync(["weather"], cancellationToken);
return "Evicted!";
}
}
Samples and benchmarks
| Area | Location |
|---|---|
| Integration / consumer sample | tests/NexGen.MediatR.Extensions.Caching.IntegrationTest (includes docker-compose.yml for Redis/SQL) |
| Unit tests | tests/NexGen.MediatR.Extensions.Caching.UnitTest |
| Benchmarks | benchmarks/NexGen.MediatR.Extensions.Caching.Benchmark |

Larger or more complex responses use more memory with the in-memory provider. Prefer Redis or Garnet for multi-instance and production workloads.
Migrating from 1.x
Version 2.0.0 removes the eviction-bus APIs. Use attributes plus one AddMediatROutputCache call.
| 1.x | 2.0 |
|---|---|
AddMediatROutputCacheEviction |
AddMediatROutputCache + UseRedisCache / UseGarnetCache on every host |
UseRedisEvictionBus / UseGarnetEvictionBus |
Built into UseRedisCache / UseGarnetCache (EnableDistributedEviction, default true) |
UseInProcessEvictionBus / InProcessRequestOutputCacheEvictionBus |
Removed. Memory cache is process-local only |
UseCustomEvictionPublisher / Subscriber / Bus |
Removed. Use Redis or Garnet Pub/Sub |
IRequestOutputCacheEvictionPublisher / Subscriber |
IRequestOutputCacheEvictionNotifier (provider-internal) + RequestOutputCacheEvictionDispatcher |
RequestOutputCacheEvictionMessage |
RequestOutputCacheEvictionNotification |
[RequestOutputCacheEvict] after any handler return |
Evicts only on success (no exception, FluentResults not failed) |
Changelog
Release notes are maintained in CHANGELOG.md (Keep a Changelog format). Check that file for what changed in each version.
Contributing
Contributions are welcome through GitHub Issues and Pull Requests.
Please read CONTRIBUTING.md for the full contribution guide (development setup, coding standards, tests, and PR expectations) before opening an issue or PR.
License
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FluentResults (>= 4.0.0)
- MediatR (>= 13.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Caching.Memory (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.10)
- Newtonsoft.Json (>= 13.0.3)
-
net8.0
- FluentResults (>= 4.0.0)
- MediatR (>= 13.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Newtonsoft.Json (>= 13.0.3)
-
net9.0
- FluentResults (>= 4.0.0)
- MediatR (>= 13.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 9.0.10)
- Microsoft.Extensions.Caching.Memory (>= 9.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.10)
- Newtonsoft.Json (>= 13.0.3)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on NexGen.MediatR.Extensions.Caching:
| Package | Downloads |
|---|---|
|
NexGen.MediatR.Extensions.Caching.Redis
Redis distributed cache provider for NexGen.MediatR.Extensions.Caching. |
|
|
NexGen.MediatR.Extensions.Caching.Garnet
Garnet distributed cache provider for NexGen.MediatR.Extensions.Caching. |
|
|
NexGen.MediatR.Extensions.Caching.EntityFramework
Entity Framework Core ChangeTracker auto-eviction for NexGen.MediatR.Extensions.Caching. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.3.1 | 0 | 8/18/2026 |
| 2.3.0 | 0 | 8/18/2026 |
| 2.2.1 | 72 | 8/17/2026 |
| 2.2.0 | 62 | 8/17/2026 |
| 2.1.0 | 64 | 8/17/2026 |
| 2.0.0 | 73 | 8/17/2026 |
| 1.4.3 | 82 | 8/11/2026 |
| 1.4.2 | 83 | 8/11/2026 |
| 1.4.1 | 108 | 8/11/2026 |
| 1.4.0 | 109 | 8/11/2026 |
| 1.3.1 | 101 | 8/11/2026 |
| 1.3.0 | 130 | 8/10/2026 |
| 1.2.0 | 130 | 8/10/2026 |
| 1.0.8 | 197 | 2/12/2026 |
| 1.0.7 | 895 | 9/26/2025 |
| 1.0.6 | 421 | 9/16/2025 |
| 1.0.5 | 230 | 9/12/2025 |
| 1.0.3 | 249 | 9/11/2025 |
| 1.0.2 | 249 | 9/10/2025 |
| 1.0.1 | 244 | 9/10/2025 |
Release v2.0.0 — Native Redis/Garnet Pub/Sub eviction; remove eviction bus; success-only [RequestOutputCacheEvict]; memory cache stays process-local