Skip to main content
Version: 2.4.1

Persistence Caching — Redis

Overview

The Redis persistence caching package registers RedisCacheService as the ICacheService backing the caching repository decorators. It shares the same decorator model as the memory caching package — read operations that supply a cacheKey are served from Redis on a hit, and fall through to the underlying repository on a miss. Write operations always bypass the cache.

The same three caching repository interfaces are available regardless of which cache backend is used:

  • ICachingGraphRepository<TEntity> — EF Core
  • ICachingLinqRepository<TEntity> — EF Core or Linq2Db through the LINQ interface
  • ICachingSqlMapperRepository<TEntity> — Dapper

Use this package when you need a distributed, out-of-process cache that survives application restarts and is shared across multiple service instances.

Installation

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

This package depends on RCommon.Persistence.Caching, which is pulled in transitively. You will also need a Redis client library; StackExchange.Redis is the standard choice and is typically added as a transitive dependency through Microsoft.Extensions.Caching.StackExchangeRedis.

Configuration

1. Add Redis distributed cache

Register Redis with ASP.NET Core's distributed cache infrastructure before configuring RCommon:

builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp:";
});

2. Add Redis persistence caching to the persistence builder

Call AddRedisPersistenceCaching 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");

ef.AddRedisPersistenceCaching();
});

AddRedisPersistenceCaching registers:

  • RedisCacheService as ICacheService
  • A Func<PersistenceCachingStrategy, ICacheService> factory that resolves RedisCacheService for PersistenceCachingStrategy.Default
  • ICachingGraphRepository<>, ICachingLinqRepository<>, ICachingSqlMapperRepository<> decorators
  • CachingOptions with CachingEnabled = true and CacheDynamicallyCompiledExpressions = true

Usage

The API is identical to the memory caching package. Swap the injected interface and the configuration call; your handler code remains the same.

Injecting a caching repository

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

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

Cached reads

string key = $"products:category:{categoryId}";

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

Paged result 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();

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

Non-cached operations

Write operations and reads without a cacheKey always go to the database:

await _products.AddAsync(product, cancellationToken);
await _products.UpdateAsync(product, cancellationToken);
await _products.DeleteAsync(product, cancellationToken);

Using with Linq2Db

private readonly ICachingLinqRepository<Product> _products;

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

Using with Dapper

private readonly ICachingSqlMapperRepository<Product> _products;

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

Cache key design

Choose structured, namespaced keys to make cache management straightforward:

// Namespaced by entity, qualifier, and parameter values
string key = $"products:category:{categoryId}:page:{page}:size:{size}";

Redis keys have no automatic expiration by default when using RedisCacheService with IDistributedCache; set expiration policies on the CachingOptions or manage them through the ICacheService interface directly if your use case requires TTL control.

Invalidation strategy

Cache invalidation after a write is the responsibility of the caller. A common pattern is to delete or refresh the relevant key after mutating data:

await _products.UpdateAsync(product, cancellationToken);

// Evict the stale cache entry so the next read goes to the database
await _cacheService.DeleteAsync($"products:category:{product.CategoryId}");

Inject ICacheService directly when you need explicit cache management outside the repository decorators.

API Summary

TypePurpose
AddRedisPersistenceCaching()Extension on IPersistenceBuilder that registers RedisCacheService and caching repositories
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
RedisCacheServiceConcrete ICacheService backed by IDistributedCache with a Redis store
PersistenceCachingStrategyEnum used internally to select the ICacheService implementation
RCommonRCommon