RepletoryLib.Caching.Repository 1.0.0

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

RepletoryLib.Caching.Repository

Cache-aside decorator for RepletoryLib repositories with write-through caching and cross-service entity sharing.

Overview

This library provides transparent caching for IRepository<T> and ICompositeKeyRepository<T> using the decorator pattern. It is designed for microservice architectures where:

  • The owning service writes entities to both database and cache (write-through)
  • Consuming services read entities directly from the shared cache without needing any per-entity DI registration

Cache Strategy

Operation Strategy
Read Cache first → return if hit. If miss → fetch from DB → cache → return
Write/Update DB first → update cache → return
Delete DB first → invalidate cache → return

Architecture at Scale

┌─────────────────────────┐     ┌─────────────────────────┐
│  UserManagement Service │     │  CourseEnrollment Service│
│  (OWNS User entity)     │     │  (reads User from cache) │
│                         │     │                         │
│  IRepository<User>      │     │  ICacheService           │
│    └─ CachedRepository  │     │    └─ GetAsync<User>(key)│
│       └─ Repository     │     │                         │
│          └─ Database    │     │  No User DI registration │
└────────┬────────────────┘     └────────┬────────────────┘
         │ write-through                  │ read-only
         └──────────┬─────────────────────┘
                    ▼
              ┌──────────┐
              │  Redis   │
              │  (shared)│
              └──────────┘

Installation

<PackageReference Include="RepletoryLib.Caching.Repository" Version="1.0.0" />

Quick Start

1. Define Cache Keys (shared library referenced by ALL services)

// SharedLib.CacheKeys/UserCacheKeys.cs

// ICacheKeyProvider<T> implementation — used by CachedRepository in the owning service
public class UserCacheKeyProvider : ICacheKeyProvider<UserEntity>
{
    public string EntityPrefix => "user";
    public string GetEntityKey(Guid id) => $"user:{id}";
    public string GetIndexKey() => "user:ids";
    public TimeSpan? Expiry => TimeSpan.FromMinutes(30);
}

// Static helpers — used by consuming services (zero DI registration)
public static class UserCacheKeys
{
    public const string Prefix = "user";
    public static string ById(Guid id) => $"user:{id}";
    public const string IndexKey = "user:ids";
}

2. Owning Service Setup (3 lines in Program.cs)

// UserManagement service — owns UserEntity, RoleEntity, etc.
services.AddRepletoryRepositories<AppDbContext>(configuration);                         // auto-register ALL repos
services.AddRepletryCacheKeyProviders(typeof(UserCacheKeyProvider).Assembly);           // scan key providers
services.AddRepletryCaching(configuration);                                            // wrap matching repos

That's it — whether you have 5 or 50 entities, it's always 3 lines. Every DbSet<T> on AppDbContext gets an IRepository<T> (for BaseEntity types) or ICompositeKeyRepository<T>, and every entity with a matching ICacheKeyProvider<T> is automatically wrapped with CachedRepository<T>.

3. Consuming Service (reads from shared cache — NO entity registration)

// CourseEnrollment service — only registers entities it OWNS
services.AddRepletoryRepositories<CourseDbContext>(configuration);
services.AddRepletryCacheKeyProviders(typeof(CourseCacheKeyProvider).Assembly);
services.AddRepletryCaching(configuration);
public class CourseEnrollmentService(
    IRepository<CourseEnrollmentEntity> enrollmentRepo,  // cached — owns this entity
    ICacheService cache)                                  // already registered
{
    public async Task<CourseEnrollmentView> GetAsync(Guid id)
    {
        var enrollment = await enrollmentRepo.GetByIdAsync(id);

        // Read User directly from shared cache using static key helper
        var user = await cache.GetAsync<UserEntity>(UserCacheKeys.ById(enrollment!.UserId));

        return new CourseEnrollmentView
        {
            EnrollmentId = enrollment.Id,
            CourseName = enrollment.CourseName,
            UserName = user?.FullName,      // enriched from shared cache
            UserEmail = user?.Email          // no HTTP call to UserManagement
        };
    }
}

Features

Cache Key Providers

Guid-Keyed Entities (ICacheKeyProvider<T>)

For entities inheriting from BaseEntity with a Guid primary key:

public interface ICacheKeyProvider<T> where T : BaseEntity
{
    string EntityPrefix { get; }          // e.g. "user"
    string GetEntityKey(Guid id);         // e.g. "user:{id}"
    string GetIndexKey();                 // e.g. "user:ids"
    TimeSpan? Expiry { get; }            // null = use DefaultExpiryMinutes from options
}
Composite-Keyed Entities (ICompositeKeyCacheKeyProvider<T>)

For entities with composite primary keys:

public interface ICompositeKeyCacheKeyProvider<T> where T : class
{
    string EntityPrefix { get; }
    string GetEntityKey(object[] keyValues);
    string GetIndexKey();
    string SerializeKeyValues(object[] keyValues);     // for index storage
    object[] DeserializeKeyValues(string serialized);  // for index retrieval
    TimeSpan? Expiry { get; }
}

Example implementation:

public class OrderItemCacheKeyProvider : ICompositeKeyCacheKeyProvider<OrderItemEntity>
{
    public string EntityPrefix => "order_item";
    public string GetEntityKey(object[] keyValues) => $"order_item:{keyValues[0]}:{keyValues[1]}";
    public string GetIndexKey() => "order_item:ids";
    public string SerializeKeyValues(object[] keyValues) => $"{keyValues[0]}:{keyValues[1]}";
    public object[] DeserializeKeyValues(string serialized)
    {
        var parts = serialized.Split(':');
        return [Guid.Parse(parts[0]), Guid.Parse(parts[1])];
    }
    public TimeSpan? Expiry => TimeSpan.FromMinutes(15);
}

ID Index Pattern

Each cached entity type maintains an ID index — a single cache key storing all entity IDs for that type:

Cache Key: "user:ids"
Value:     [guid1, guid2, guid3, ...]

Cache Key: "user:{guid1}"
Value:     { Id: guid1, FullName: "Alice", Email: "alice@example.com", ... }

Cache Key: "user:{guid2}"
Value:     { Id: guid2, FullName: "Bob", Email: "bob@example.com", ... }

The index enables GetAllAsync, FindAsync, pagination, and streaming without hitting the database.

Extension Methods on ICacheService

These methods are available to any service that has ICacheService registered — no per-entity registration needed:

Get All Entities
var users = await cache.GetAllFromIndexAsync<UserEntity>(
    UserCacheKeys.IndexKey, UserCacheKeys.ById);
Find with Filter
var activeUsers = await cache.FindFromIndexAsync<UserEntity>(
    UserCacheKeys.IndexKey, UserCacheKeys.ById,
    u => u.IsActive);
Pagination
var page = await cache.GetPagedFromIndexAsync<UserEntity>(
    UserCacheKeys.IndexKey, UserCacheKeys.ById,
    page: 2, pageSize: 20);

// page.Items          — entities on this page
// page.TotalCount     — total entities in index
// page.TotalPages     — computed total pages
// page.HasNextPage    — true if more pages after this one
// page.HasPreviousPage — true if pages before this one
Paginated + Filtered
var activePage = await cache.FindPagedFromIndexAsync<UserEntity>(
    UserCacheKeys.IndexKey, UserCacheKeys.ById,
    u => u.IsActive,
    page: 1, pageSize: 50);
Streaming in Batches
await foreach (var batch in cache.StreamFromIndexAsync<UserEntity>(
    UserCacheKeys.IndexKey, UserCacheKeys.ById, batchSize: 100))
{
    foreach (var user in batch.Items)
        ProcessUser(user);

    Console.WriteLine(
        $"Batch {batch.BatchNumber}/{batch.TotalBatches}, " +
        $"processed {batch.ProcessedCount}/{batch.TotalCount}, " +
        $"has more: {batch.HasMore}");
}
Check Entity Existence
var exists = await cache.EntityExistsAsync(UserCacheKeys.ById(userId));
Composite Key Extensions

All the above methods have composite-key equivalents:

// Get all composite-keyed entities
var items = await cache.GetAllFromCompositeIndexAsync<OrderItemEntity>(
    OrderItemCacheKeys.IndexKey, OrderItemCacheKeys.BySerializedKey);

// Paginated
var page = await cache.GetPagedFromCompositeIndexAsync<OrderItemEntity>(
    OrderItemCacheKeys.IndexKey, OrderItemCacheKeys.BySerializedKey,
    page: 1, pageSize: 50);

// Stream
await foreach (var batch in cache.StreamFromCompositeIndexAsync<OrderItemEntity>(
    OrderItemCacheKeys.IndexKey, OrderItemCacheKeys.BySerializedKey,
    batchSize: 100))
{
    // process batch.Items
}

CachedRepository Method Strategies

The CachedRepository<T> decorator wraps IRepository<T> transparently:

Method Strategy
GetByIdAsync Cache first → miss: DB → cache → add to index
GetAllAsync Read index → resolve each from cache → batch-fetch misses from DB → cache misses. Empty index triggers full rebuild
FindAsync If index ≤ max: resolve all + in-memory filter. Else: fallback to DB
AddAsync DB → cache entity → add ID to index
AddRangeAsync DB → cache each → add IDs to index
UpdateAsync DB → overwrite in cache
UpdateRangeAsync DB → overwrite each in cache
SoftDeleteAsync DB → remove from cache → remove from index
HardDeleteAsync DB → remove from cache → remove from index
SoftDeleteRangeAsync DB → remove each from cache & index
HardDeleteRangeAsync DB → remove each from cache & index
ExistsAsync Pass through to DB (lightweight query)
CountAsync Pass through to DB (lightweight query)
Query() Pass through to inner (IQueryable, not cacheable)

Distributed Locking

When IDistributedLockService is registered (e.g., via RepletoryLib.Caching.Redis), index mutations are protected with a distributed lock to prevent race conditions across multiple instances:

AddToIndex:    lock("lock:user:ids") → read index → add ID → write → release
RemoveFromIndex: lock("lock:user:ids") → read index → remove ID → write → release

When IDistributedLockService is not registered, index updates use simple read-modify-write without locking.

Configuration

{
  "Caching": {
    "Repository": {
      "DefaultExpiryMinutes": 60,
      "EnableIndexKey": true,
      "MaxEntitiesForInMemoryFilter": 10000,
      "FallbackToDbForFind": true
    }
  }
}
Property Type Default Description
DefaultExpiryMinutes int 60 Fallback TTL when ICacheKeyProvider.Expiry is null
EnableIndexKey bool true Maintain ID index for GetAll/Find resolution
MaxEntitiesForInMemoryFilter int 10000 FindAsync safety limit before falling back to DB
FallbackToDbForFind bool true If index count exceeds max, FindAsync hits DB instead

DI Registration

// 1. Auto-register all repositories from DbContext
services.AddRepletoryRepositories<AppDbContext>(configuration);

// 2. Scan assembly for ICacheKeyProvider<T> implementations
services.AddRepletryCacheKeyProviders(typeof(UserCacheKeyProvider).Assembly);

// 3. Auto-wrap all matching repos with cached decorators
services.AddRepletryCaching(configuration);

Explicit Per-Entity Registration

// Register a specific entity's cached repository
services.AddRepletryCachedRepository<UserEntity>(configuration);

// Register a composite-key entity's cached repository
services.AddRepletryCachedCompositeKeyRepository<OrderItemEntity>(configuration);

Models

PagedCacheResult<T>

public class PagedCacheResult<T>
{
    public IReadOnlyList<T> Items { get; set; }
    public int TotalCount { get; set; }
    public int Page { get; set; }
    public int PageSize { get; set; }
    public int TotalPages { get; }          // computed
    public bool HasNextPage { get; }        // computed
    public bool HasPreviousPage { get; }    // computed
}

StreamBatch<T>

public class StreamBatch<T>
{
    public IReadOnlyList<T> Items { get; set; }
    public int BatchNumber { get; set; }
    public int TotalBatches { get; set; }
    public int TotalCount { get; set; }
    public int ProcessedCount { get; set; }  // cumulative items yielded so far
    public bool HasMore { get; }             // computed
}

Dependencies

Package Purpose
RepletoryLib.Caching.Abstractions ICacheService, IDistributedLockService
RepletoryLib.Data.EntityFramework IRepository<T>, ICompositeKeyRepository<T>, BaseEntity
Microsoft.Extensions.DependencyInjection.Abstractions DI container abstractions
Microsoft.Extensions.Logging.Abstractions Logging abstractions
Microsoft.Extensions.Options.ConfigurationExtensions Options pattern

Cross-Service Usage Summary

Role Registration How to Read
Owning service AddRepletoryRepositories + AddRepletryCacheKeyProviders + AddRepletryCaching Inject IRepository<T> (transparently cached)
Consuming service None (for entities it doesn't own) Inject ICacheService + use static key helpers or extension methods
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

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
1.0.0 74 3/2/2026