CachedQueries.Redis 3.0.2

dotnet add package CachedQueries.Redis --version 3.0.2
                    
NuGet\Install-Package CachedQueries.Redis -Version 3.0.2
                    
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="CachedQueries.Redis" Version="3.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CachedQueries.Redis" Version="3.0.2" />
                    
Directory.Packages.props
<PackageReference Include="CachedQueries.Redis" />
                    
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 CachedQueries.Redis --version 3.0.2
                    
#r "nuget: CachedQueries.Redis, 3.0.2"
                    
#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 CachedQueries.Redis@3.0.2
                    
#: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=CachedQueries.Redis&version=3.0.2
                    
Install as a Cake Addin
#tool nuget:?package=CachedQueries.Redis&version=3.0.2
                    
Install as a Cake Tool

CachedQueries

NuGet NuGet Downloads CI Coverage Status License: MIT

A .NET library for seamless caching of Entity Framework Core queries. Cache IQueryable results directly within EF with automatic invalidation on SaveChanges, transaction-aware invalidation, multi-context isolation, and pluggable cache providers.

Installation

dotnet add package CachedQueries

For Redis support:

dotnet add package CachedQueries.Redis

Quick Start

1. Configure Services

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddCachedQueries();
builder.Services.AddCacheInvalidation<AppDbContext>();

var app = builder.Build();
app.Services.UseCachedQueries();
app.Run();

2. Cache Queries

using CachedQueries.Extensions;

// Cache a collection (default 30 min expiration)
var orders = await db.Orders
    .Include(o => o.Items)
    .Cacheable()
    .ToListAsync();

// Cache with custom options
var goods = await db.Goods
    .Where(g => g.Category == "Electronics")
    .Cacheable(o => o
        .Expire(TimeSpan.FromMinutes(10))
        .WithTags("catalog", "electronics"))
    .ToListAsync();

// Cache single item
var customer = await db.Customers
    .Where(c => c.Id == id)
    .Cacheable()
    .FirstOrDefaultAsync();

// Cache scalar results
var count = await db.Orders.Cacheable().CountAsync();
var exists = await db.Customers.Cacheable().AnyAsync(c => c.Email == email);

That's it. Cache is automatically invalidated when SaveChanges() modifies related entities.

Fluent API

The .Cacheable() extension returns a CacheableQuery<T> that supports all common terminal methods:

Method Description
.ToListAsync() Cache as list
.FirstOrDefaultAsync() Cache first item
.SingleOrDefaultAsync() Cache single item
.CountAsync() Cache count
.AnyAsync() Cache existence check

Configure per-query behavior with the fluent builder:

.Cacheable(o => o
    .Expire(TimeSpan.FromMinutes(5))      // Absolute expiration
    .SlidingExpiration(TimeSpan.FromMinutes(5))  // Or sliding
    .WithKey("my-custom-key")             // Custom cache key
    .WithTags("orders", "reports")        // Tags for grouped invalidation
    .SkipIf(bypassCache)                  // Conditional skip
    .UseTarget(CacheTarget.Collection))   // Override provider selection

Cache Invalidation

Automatic (default)

Cache is invalidated automatically on SaveChanges(). The library detects which entity types were modified and invalidates all cached queries that depend on them.

db.Orders.Add(newOrder);
await db.SaveChangesAsync(); // All cached Order queries are invalidated

Transaction-Aware

Inside a transaction, invalidation is deferred until commit. Rollback discards pending invalidations.

await using var tx = await db.Database.BeginTransactionAsync();

db.Orders.Add(order);
await db.SaveChangesAsync();  // Invalidation DEFERRED

await tx.CommitAsync();       // Invalidation fires NOW
// Or: await tx.RollbackAsync();  // No invalidation

Manual

using CachedQueries.Extensions;

await Cache.InvalidateByKeyAsync("my-key");            // By exact cache key
await Cache.InvalidateByKeysAsync(["key1", "key2"]);   // By multiple keys
await Cache.InvalidateAsync<Order>();                  // By entity type
await Cache.InvalidateByTagAsync("reports");           // By single tag
await Cache.InvalidateByTagsAsync(["orders", "reports"]); // By multiple tags
await Cache.ClearContextAsync();                       // Current context only (e.g. for app user or tenant)
await Cache.ClearAllAsync();                           // Everything

Key-based invalidation is useful when you cache with a custom key via WithKey():

// Cache with a custom key
var order = await db.Orders
    .Where(o => o.Id == id)
    .Cacheable(o => o.WithKey($"order:{id}"))
    .FirstOrDefaultAsync();

// Later, invalidate by that same key — no tags or entity tracking needed
await Cache.InvalidateByKeyAsync($"order:{id}");

Redis Support

// Automatic setup (recommended)
builder.Services.AddCachedQueriesWithRedis("localhost:6379");

// Or with existing IDistributedCache
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
builder.Services.AddCachedQueriesWithRedis();

Multi-Context Caching

For example, implement ICacheContextProvider to isolate cache per tenant:

public class TenantCacheContextProvider(IHttpContextAccessor http) : ICacheContextProvider
{
    public string? GetContextKey()
        => http.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault();
}

Register it:

builder.Services.AddHttpContextAccessor();
builder.Services.AddCachedQueries(config =>
    config.UseContextProvider<TenantCacheContextProvider>());

Cache keys are automatically prefixed: tenant-a:CQ:abc123.

Configuration

builder.Services.AddCachedQueries(config =>
{
    config.DefaultOptions = new CachingOptions(TimeSpan.FromHours(1));
    config.AutoInvalidation = true;  // default
    config.EnableLogging = true;     // default
});

Multi-Provider Setup

Use different cache backends for different query types:

builder.Services.AddCachedQueries(config => config
    .UseSingleItemProvider<RedisCacheProvider>()    // FirstOrDefault, SingleOrDefault
    .UseCollectionProvider<MongoCacheProvider>()    // ToList
    .UseScalarProvider<RedisCacheProvider>());      // Count, Any

// Or same provider for all
builder.Services.AddCachedQueries(config => config
    .UseProvider<RedisCacheProvider>());

Cache Targets

Target Auto-selected for Description
Single FirstOrDefault, SingleOrDefault Individual entities
Collection ToList Lists and arrays
Scalar Count, Any Aggregation results
Auto - Automatically determined (default)

Custom Cache Provider

public class MyCacheProvider : ICacheProvider
{
    public Task<T?> GetAsync<T>(string key, CancellationToken ct = default) { ... }
    public Task SetAsync<T>(string key, T value, CachingOptions options, CancellationToken ct = default) { ... }
    public Task RemoveAsync(string key, CancellationToken ct = default) { ... }
    public Task InvalidateByTagsAsync(IEnumerable<string> tags, CancellationToken ct = default) { ... }
    public Task ClearAsync(CancellationToken ct = default) { ... }
}

builder.Services.AddCachedQueries<MyCacheProvider>();

Legacy API

The older extension methods are still supported:

await db.Orders.ToListCachedAsync(ct);
await db.Products.FirstOrDefaultCachedAsync(p => p.Id == id, ct);
await db.Orders.CountCachedAsync(ct);
await db.Orders.AnyCachedAsync(o => o.Status == OrderStatus.Pending, ct);

Demo Project

See examples/ for a full working demo with Docker Compose, PostgreSQL, Redis, multi-context isolation, and 73 integration tests.

cd examples
docker compose up --build        # Run the API
dotnet test Demo.Api.Tests       # Run integration tests

Requirements

  • .NET 8.0, 9.0, or 10.0
  • Entity Framework Core 8.0+

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

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 was computed.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.2 28 3/19/2026
3.0.1 30 3/19/2026
3.0.0 31 3/18/2026
3.0.0-rc.3 32 3/18/2026