CacheGraph.Extensions 1.1.0

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

CacheGraph πŸ§ πŸ”—

Cache-aside with dependency graph and cascade invalidation for .NET

CacheGraph combines the cache-aside pattern with a dependency graph that maps cache keys to business entities. When an entity changes, all cache entries that depend on it are automatically invalidated in cascade β€” no manual cache key management needed.

Unlike FusionCache, EasyCaching, or Microsoft HybridCache (which only support flat tag-based invalidation), CacheGraph models relationships as a traversable bidirectional graph, enabling true cascade invalidation across entity hierarchies.

πŸ“¦ Installation

dotnet add package CacheGraph.Core

For Microsoft.Extensions integration (DI, Logging, IMemoryCache, IDistributedCache):

dotnet add package CacheGraph.Extensions

✨ Features

  • Cache-aside with GetOrSetAsync β€” on-demand loading with factory and stampede protection
  • Dependency graph β€” bidirectional relationships between cache keys and entities
  • Cascade invalidation β€” invalidate all entries that depend on an entity, type, collection, predicate, or query
  • Stampede protection β€” distributed lock ensures only one thread executes the factory per key
  • 8 interfaces for extensibility (cache, graph, lock, events, telemetry, logging, serialization)
  • DI integration β€” AddCacheGraph() / AddCacheGraphWithLogging() for Microsoft.Extensions.DependencyInjection
  • Cache adapters β€” wrap IMemoryCache or IDistributedCache as ICacheProvider
  • Events β€” CacheSetEvent, CacheRemovedEvent, EntityInvalidatedEvent, QueryInvalidatedEvent
  • Cross-instance backplane β€” CacheGraphBackplane replicates invalidations between instances over the event provider (in-memory or Redis Pub/Sub) without re-publish loops
  • Declarative caching β€” [CacheResult] attribute + CacheProxy (Castle DynamicProxy) caches interface methods automatically and registers dependencies for cascade invalidation
  • Redis providers (CacheGraph.Redis) β€” distributed cache, graph storage, distributed locks, and pub/sub event invalidation with self-message filtering
  • EF Core interceptor (CacheGraph.EntityFrameworkCore) β€” automatic query caching via DbCommandInterceptor, caches SQL query results as row-column dictionaries
  • Economic cache β€” threshold-based adaptive caching that analyzes query cost (execution time + call frequency) to decide what to cache; configurable rules, TTL scaling, manual overrides, and injectable IEconomicCacheManager for monitoring dashboards
  • Metrics β€” hits, misses, sets, removes, invalidations, operation times, graph stats
  • Thread-safe β€” all in-memory providers use lock-based writes and snapshot reads
  • Benchmarks β€” BenchmarkDotNet suite comparing CacheGraph vs manual cache-aside vs FusionCache
  • Multi-target β€” .NET 8, .NET 9, and .NET 10
  • Zero external dependencies (Core) β€” only the .NET BCL

πŸš€ Quick Start

Manual setup

using CacheGraph.Core;
using CacheGraph.Core.Implementaciones;
using CacheGraph.Core.Interfaces;
using CacheGraph.Core.Modelos;

var cacheGraph = new CacheGraph(
    graphProvider: new MemoryGraphProvider(),
    cacheProvider: new MemoryCacheProvider(),
    eventProvider: new MemoryEventProvider(),
    logger: new ConsoleLogger(),
    telemetryProvider: new NoOpTelemetryProvider(),
    distributedLock: new MemoryDistributedLock(),
    options: new GraphOptions
    {
        DefaultTtl = TimeSpan.FromMinutes(30),
        LockTimeout = TimeSpan.FromSeconds(30),
        LockRetryCount = 3,
    });

// Cache-aside with entity dependency
var product = await cacheGraph.GetOrSetAsync(
    "product:42",
    factory: () => _db.GetProductAsync(42),
    dependencies: new[] { "entity:Product:42" },
    ttl: TimeSpan.FromMinutes(5));

// When the entity changes, invalidate it β†’ product:42 is removed automatically
await cacheGraph.InvalidateEntityAsync<Product>("42");

// Invalidate all Product cache entries (by type)
await cacheGraph.InvalidateEntityTypeAsync<Product>();

// Invalidate a collection of entities
await cacheGraph.InvalidateCollectionAsync<Product>(new[] { "42", "43", "44" });

// Predicate-based invalidation
await cacheGraph.InvalidateByPredicateAsync<Product>(p => p.Discontinued);

// Query-based invalidation
await cacheGraph.InvalidateByQueryAsync<Product>(q => q.Where(p => p.Price < 10));

Dependency Injection setup

// Program.cs
builder.Services.AddCacheGraph(options =>
{
    options.DefaultTtl = TimeSpan.FromMinutes(30);
});

// With Microsoft.Extensions.Logging
builder.Services.AddCacheGraphWithLogging(options =>
{
    options.DefaultTtl = TimeSpan.FromMinutes(30);
});

// Fluent builder
builder.Services.AddCacheGraph(builder => builder
    .UseMemoryCache()
    .UseMicrosoftLogging()
    .WithDefaultTtl(TimeSpan.FromMinutes(30))
);

Using IMemoryCache or IDistributedCache as the cache provider

// Wrap IMemoryCache as ICacheProvider
var memoryCache = new MemoryCache(new MemoryCacheOptions());
var cacheProvider = new MemoryCacheProviderAdapter(memoryCache);

// Wrap IDistributedCache as ICacheProvider (uses JSON serialization)
var distributedCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
var cacheProvider = new DistributedCacheProviderAdapter(
    distributedCache,
    new JsonCacheSerializer());

Declarative caching with attributes (CacheGraph.Attributes)

using CacheGraph.Attributes;

public interface IProductService
{
    [CacheResult("products:{id}", 300, "entity:Product:{id}")]
    Task<Product> GetByIdAsync(int id);
}

// Manual wiring
var proxy = CacheProxy.Create<IProductService>(new ProductService(), cacheGraph);

// Or via DI
services.AddCacheGraph();
services.AddCacheGraphProxied<IProductService, ProductService>();

Keys support {paramName} placeholders; dependencies can be declared as templates and are also auto-detected from id/ids parameters (entity:{Type}:{value}). Cache hits skip the target; cascade invalidation works transparently.

Cross-instance backplane (CacheGraph.Backplane)

// Same event provider shared across instances (Redis Pub/Sub with CacheGraph.Redis)
services.AddCacheGraph();
services.AddCacheGraphBackplane();

// At startup β€” start replicating remote invalidations
var backplane = provider.GetRequiredService<CacheGraphBackplane>();
await backplane.StartAsync();

Invalidation events carry the affected keys, so every instance removes the same entries from its local cache and graph. Replication is silent β€” no re-publish loops. With Redis, messages from the publishing instance are filtered out automatically.

EF Core automatic query caching

// Program.cs β€” register with default options (caches everything)
builder.Services.AddCacheGraphEntityFrameworkCore(options =>
{
    options.DefaultQueryTtl = TimeSpan.FromMinutes(5);
});

// Add to DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connStr)
           .AddCacheGraphInterceptor()); // <-- registers DbCommandInterceptor

Economic cache (threshold-based adaptive caching)

Instead of caching every query, the economic cache analyzes execution time and call frequency to decide intelligently what to cache:

// Program.cs
builder.Services.AddCacheGraphEntityFrameworkCore(options =>
{
    options.EconomicCache.Enabled = true;           // activate economic mode
    options.EconomicCache.AlwaysCacheAboveDuration = TimeSpan.FromSeconds(10);
    options.EconomicCache.NeverCacheBelowDuration = TimeSpan.FromMilliseconds(500);
    options.EconomicCache.ScaleTtlByDuration = true; // slower queries β†’ longer TTL
});

Threshold rules (defaults):

Zone Duration Condition Action
Always > 10 s Any frequency Cache
High 1 s – 10 s > 10 calls/hour Cache
Medium 500 ms – 1 s > 10 calls/hour Cache
Fast 0 – 500 ms > 1000 calls/hour Cache
Fast 0 – 500 ms ≀ 1000 calls/hour Skip
Pending Any < 3 samples Cache (conservative)

Inject IEconomicCacheManager into a controller to build a monitoring dashboard:

public class CacheMetricsController : ControllerBase
{
    private readonly IEconomicCacheManager _economy;

    [HttpGet("slowest")]
    public IActionResult GetSlowest([FromQuery] int top = 10)
        => Ok(_economy.GetTopSlowestQueries(top));

    [HttpGet("profiles")]
    public IActionResult GetAll()
        => Ok(_economy.GetAllProfiles());

    [HttpPost("overrides/force-cache/{key}")]
    public IActionResult ForceCache(string key)
    {
        _economy.ForceCache(key);
        return Ok();
    }

    [HttpPost("thresholds")]
    public IActionResult UpdateThresholds([FromBody] EconomicCacheOptions opts)
    {
        _economy.UpdateThresholds(o =>
        {
            o.AlwaysCacheAboveDuration = opts.AlwaysCacheAboveDuration;
            o.NeverCacheBelowDuration = opts.NeverCacheBelowDuration;
        });
        return Ok();
    }
}

Benchmarks (CacheGraph.Benchmarks)

dotnet run --project CacheGraph.Benchmarks -c Release -- --filter '*GetOrSetBenchmarks*'

Covers hot/cold GetOrSetAsync throughput, cascade invalidation cost at 10/100/1000 dependents, and a head-to-head comparison against manual cache-aside and FusionCache.

EF Core Query Caching Benchmark

Real-world comparison of EF Core + SQLite with and without CacheGraph's query interceptor. ~300K master records, 5 Level2 tables (1K rows each), 3 Level3 tables (500 rows each), 12 diverse queries, 10 iterations each. Both projects share identical schema, DbContext, data generator, and queries β€” only Program.cs and .csproj differ.

Only ~20 lines of code separate the baseline from the cached version (4 using directives + 8 lines of DI registration + 8 lines to inject interceptors via a custom DbContextFactory).

Results
Query No Cache (ms) With CacheGraph (ms) Speedup
Q1 SingleById 17.3 15.4 1.1Γ—
Q2 Filter+Count 8.8 0.5 17.6Γ—
Q3 Pagination (LIKE+Skip) 6.7 1.1 6.1Γ—
Q4 JOIN 2-table 3.7 1.4 2.6Γ—
Q5 GROUP BY SUM/AVG 1,579.7 0.1 15,797Γ—
Q6 Include→ThenInclude 2.3 1.4 1.6×
Q7 3x Include 0.6 0.4 1.5Γ—
Q8 EXISTS Subquery 61.8 1.1 56.2Γ—
Q9 TOP 100 ORDER BY 200.8 1.0 200.8Γ—
Q10 JOIN 3-table 1.8 1.8 1.0Γ—
Q11 GROUP BY HAVING 0.6 0.6 1.0Γ—
Q12 OR Filter 4.7 1.5 3.1Γ—
TOTAL 1,888.8 26.3 71.8Γ—

Aggregate and full-scan queries (Q5, Q8, Q9) benefit most β€” those go from hundreds or thousands of milliseconds to sub-millisecond responses. Queries that are already < 2 ms on small tables (Q10, Q11) see no measurable difference, confirming that the cache adds negligible overhead.

Run it yourself
# Baseline (no cache)
dotnet run --project CacheGraph.Benchmarks.EfCore/NoCache -- 10

# With CacheGraph
dotnet run --project CacheGraph.Benchmarks.EfCore/WithCache -- 10

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      ICacheGraph                         β”‚
β”‚                   (main faΓ§ade)                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ ICache     β”‚ IGraph       β”‚ IDistributed β”‚ IEvent         β”‚
β”‚ Provider   β”‚ Provider     β”‚ Lock         β”‚ Provider       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ ITelemetryProvider  β”‚  ICacheGraphLogger                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Edge direction: cache-key β†’ entity (the cache key depends on the entity).

product:42  ──────►  entity:Product:42  ◄──────  products:featured
   (dep)              (entity)                  (dep)

cart:user:42  ──────►  entity:User:42
   (dep)              (entity)

When entity:Product:42 is invalidated, the graph traverses incoming edges to find product:42 and products:featured, removing both from the cache.

πŸ“Š Interfaces

Interface Purpose
ICacheGraph Main faΓ§ade β€” cache-aside, invalidation, graph queries
ICacheProvider Key-value storage (get, set, remove, exists)
IGraphProvider Dependency graph (nodes, edges, traversal)
IDistributedLock Distributed lock for stampede protection
IEventProvider Publish/subscribe event system
ITelemetryProvider Activity tracking and performance telemetry
ICacheGraphLogger Logging abstraction
ICacheSerializer Serialization for distributed cache adapters
IEconomicCacheManager Economic cache management β€” metrics, thresholds, overrides

βš™οΈ Configuration

var options = new GraphOptions
{
    DefaultTtl       = TimeSpan.FromHours(1),   // Default cache entry TTL
    LockTimeout     = TimeSpan.FromSeconds(30), // Max wait to acquire lock
    LockRetryCount  = 3,                        // Lock acquisition retries
    LockRetryDelay  = TimeSpan.FromMilliseconds(100),
    EnableValidation = true,                     // Input validation
};

πŸ§ͺ Tests

dotnet test --framework net10.0

331 tests with xUnit + FluentAssertions, covering:

  • Unit tests for all providers, models, and the main CacheGraph faΓ§ade
  • DI integration tests
  • Cache adapter tests (IMemoryCache, IDistributedCache)
  • Concurrency stress tests (stampede protection, concurrent invalidation, concurrent edge addition, metrics accuracy under load)
  • Backplane tests (cross-instance invalidation replication, no re-publish loops)
  • Redis Pub/Sub tests (channel subscription, self-message filtering, unsubscribe)
  • Declarative caching tests (key templates, auto-dependency detection, invalidation)
  • EF Core interceptor tests (caching, parameter normalization, command suppression)
  • Economic cache tests (threshold zones, TTL scaling, force overrides, configuration)

🎯 Demo

dotnet run --project CacheGraph.Demo

πŸ“„ License

MIT License β€” see LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET 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 (2)

Showing the top 2 NuGet packages that depend on CacheGraph.Extensions:

Package Downloads
CacheGraph.Redis

Redis providers for CacheGraph β€” distributed cache, graph storage, distributed locks, and pub/sub event invalidation

CacheGraph.EntityFrameworkCore

Automatic query caching, economic thresholds, and cascade invalidation for Entity Framework Core via CacheGraph dependency graph

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 54 8/21/2026
1.0.0 115 8/6/2026