CachedQueries 3.0.2
dotnet add package CachedQueries --version 3.0.2
NuGet\Install-Package CachedQueries -Version 3.0.2
<PackageReference Include="CachedQueries" Version="3.0.2" />
<PackageVersion Include="CachedQueries" Version="3.0.2" />
<PackageReference Include="CachedQueries" />
paket add CachedQueries --version 3.0.2
#r "nuget: CachedQueries, 3.0.2"
#:package CachedQueries@3.0.2
#addin nuget:?package=CachedQueries&version=3.0.2
#tool nuget:?package=CachedQueries&version=3.0.2
CachedQueries
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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
| 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 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. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.0 && < 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.0 && < 9.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0 && < 9.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1 && < 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0 && < 9.0.0)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.0 && < 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0 && < 10.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 9.0.0 && < 10.0.0)
- Microsoft.Extensions.Caching.Memory (>= 9.0.0 && < 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0 && < 10.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CachedQueries:
| Package | Downloads |
|---|---|
|
CachedQueries.Redis
Redis distributed cache provider for CachedQueries. Provides high-performance distributed caching with atomic tag-based invalidation using StackExchange.Redis. Drop-in replacement for the built-in memory cache provider. |
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 | 37 | 3/19/2026 |
| 3.0.0 | 49 | 3/18/2026 |
| 3.0.0-rc.3 | 31 | 3/18/2026 |
| 2.0.1 | 30,723 | 12/2/2024 |
| 2.0.0 | 4,743 | 10/18/2024 |
| 1.0.18 | 2,776 | 7/5/2024 |
| 1.0.17 | 490 | 2/7/2023 |
| 1.0.16 | 74,531 | 1/31/2023 |
| 1.0.15 | 823 | 1/29/2023 |
| 1.0.14 | 1,282 | 1/25/2023 |
| 1.0.13 | 3,998 | 1/5/2023 |
| 1.0.12 | 4,523 | 12/7/2022 |
| 1.0.11 | 6,832 | 10/21/2022 |
| 1.0.10 | 11,380 | 7/14/2022 |
| 1.0.9 | 2,139 | 7/1/2022 |
| 1.0.8 | 608 | 7/1/2022 |
| 1.0.7 | 607 | 7/1/2022 |
| 1.0.6 | 12,195 | 4/11/2022 |
| 1.0.5 | 2,049 | 3/4/2022 |