NexGen.MediatR.Extensions.Caching 2.0.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NexGen.MediatR.Extensions.Caching" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NexGen.MediatR.Extensions.Caching" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="NexGen.MediatR.Extensions.Caching" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NexGen.MediatR.Extensions.Caching --version 2.0.0
                    
#r "nuget: NexGen.MediatR.Extensions.Caching, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NexGen.MediatR.Extensions.Caching@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NexGen.MediatR.Extensions.Caching&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=NexGen.MediatR.Extensions.Caching&version=2.0.0
                    
Install as a Cake Tool

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

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/or Database) 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 expirationInSeconds on [RequestOutputCache] always wins (including 0 for never expire). Provider DefaultExpirationInSeconds only replaces the library default when the attribute uses the constructor default (300). Explicit 300 is 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: TResponse should be a reference type (class, record, or interface), consistent with typical MediatR query responses.

Important: For EF auto-evict, include nameof for every related entity type in tags.

[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

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

Release v2.0.0 — Native Redis/Garnet Pub/Sub eviction; remove eviction bus; success-only [RequestOutputCacheEvict]; memory cache stays process-local