Skip to main content
Version: Next

Persistence Caching — Memory

Overview

The persistence caching layer decorates existing repositories with cache-aware query overloads. Instead of hitting the database on every read, results can be stored in a cache and retrieved by a caller-supplied key. Writes (add, update, delete) are always delegated directly to the underlying repository without touching the cache.

Two in-process memory cache options are available:

  • In-memory (InMemoryCacheService) — uses IMemoryCache from Microsoft.Extensions.Caching.Memory. Best for single-node deployments.
  • Distributed memory (DistributedMemoryCacheService) — uses IDistributedCache with the in-memory implementation from Microsoft.Extensions.Caching.StackExchangeRedis. Useful for testing distributed cache code paths locally without a Redis instance.

Both options register the same set of caching repository decorators:

  • ICachingGraphRepository<TEntity> — for use with IGraphRepository<TEntity> (EF Core)
  • ICachingLinqRepository<TEntity> — for use with ILinqRepository<TEntity> (EF Core, Linq2Db)
  • ICachingSqlMapperRepository<TEntity> — for use with ISqlMapperRepository<TEntity> (Dapper)

Installation

NuGet Package
dotnet add package RCommon.Persistence.Caching.MemoryCache

This package depends on RCommon.Persistence.Caching, which is pulled in transitively.

Configuration

Call AddInMemoryPersistenceCaching or AddDistributedMemoryPersistenceCaching on the persistence builder after configuring the underlying provider:

builder.Services.AddRCommon()
.WithPersistence<EFCorePerisistenceBuilder>(ef =>
{
ef.AddDbContext<AppDbContext>(
"AppDb",
options => options.UseSqlServer(connectionString));

ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb");

// In-process memory cache
ef.AddInMemoryPersistenceCaching();

// -- OR -- distributed memory cache (useful for local testing)
// ef.AddDistributedMemoryPersistenceCaching();
});

Both methods configure CachingOptions with these defaults:

OptionDefault
CachingEnabledtrue
CacheDynamicallyCompiledExpressionstrue

Usage

Injecting a caching repository

Inject ICachingGraphRepository<TEntity> instead of IGraphRepository<TEntity>. All non-cached operations fall through to the inner repository unchanged; only the overloads that accept a cacheKey use the cache.

public class ProductQueryHandler
{
private readonly ICachingGraphRepository<Product> _products;

public ProductQueryHandler(ICachingGraphRepository<Product> products)
{
_products = products;
_products.DataStoreName = "AppDb";
}
}

Cached reads

Every ICachingGraphRepository<TEntity> overload that accepts a cacheKey checks the cache first and falls through to the database on a miss, storing the result for subsequent calls:

// Cache by a composite key
string key = $"products:category:{categoryId}";

ICollection<Product> cached = await _products.FindAsync(
cacheKey: key,
expression: p => p.CategoryId == categoryId,
token: cancellationToken);

Paged query with caching:

string pageKey = $"products:page:{pageNumber}:{pageSize}";

IPaginatedList<Product> page = await _products.FindAsync(
cacheKey: pageKey,
expression: p => p.IsActive,
orderByExpression: p => p.Name,
orderByAscending: true,
pageNumber: pageNumber,
pageSize: pageSize,
token: cancellationToken);

Using a specification:

var spec   = new ActiveProductSpec();
string key = "products:active";

ICollection<Product> active = await _products.FindAsync(
cacheKey: key,
specification: spec,
token: cancellationToken);

Paged query from a paged specification:

var spec = new PagedSpecification<Product>(
p => p.IsActive,
p => p.Name,
orderByAscending: true,
pageNumber: 1,
pageSize: 10);

IPaginatedList<Product> page = await _products.FindAsync(
cacheKey: "products:active:p1",
specification: spec,
token: cancellationToken);

Non-cached operations

All write operations and non-keyed reads delegate directly to the inner repository without interacting with the cache:

// These bypass the cache entirely
await _products.AddAsync(product, cancellationToken);
await _products.UpdateAsync(product, cancellationToken);
await _products.DeleteAsync(product, cancellationToken);

// Non-cached read (no cacheKey overload)
Product? p = await _products.FindAsync(productId, cancellationToken);

ICachingLinqRepository and ICachingSqlMapperRepository

The same caching pattern applies to ICachingLinqRepository<TEntity> (for Linq2Db or EF Core through the LINQ interface) and ICachingSqlMapperRepository<TEntity> (for Dapper):

// Linq2Db
private readonly ICachingLinqRepository<Product> _products;

ICollection<Product> cached = await _products.FindAsync(
"products:all", p => true, cancellationToken);
// Dapper
private readonly ICachingSqlMapperRepository<Product> _products;

ICollection<Product> cached = await _products.FindAsync(
"products:featured", p => p.IsFeatured, cancellationToken);

Cache key design

Cache keys are arbitrary object values. Using structured, namespaced string keys (e.g., entity:qualifier:value) makes it straightforward to reason about cache invalidation:

// Good: namespaced and parameterised
string key = $"products:category:{categoryId}:page:{page}";

// Avoid: generic keys that may collide across entity types
string key = $"{categoryId}:{page}";

Cache invalidation is the responsibility of the caller. After a write operation, remove or refresh the relevant cache entries using the underlying ICacheService if your scenario requires it.

API Summary

TypePurpose
ICachingGraphRepository<TEntity>Decorator over IGraphRepository<TEntity>; adds cacheKey overloads to FindAsync
ICachingLinqRepository<TEntity>Decorator over ILinqRepository<TEntity>; adds cacheKey overloads to FindAsync
ICachingSqlMapperRepository<TEntity>Decorator over ISqlMapperRepository<TEntity>; adds cacheKey overloads to FindAsync
AddInMemoryPersistenceCaching()Extension on IPersistenceBuilder that registers InMemoryCacheService and caching repositories
AddDistributedMemoryPersistenceCaching()Extension that registers DistributedMemoryCacheService and caching repositories
PersistenceCachingStrategyEnum used internally to select the ICacheService implementation
RCommonRCommon