EdiHybridCache 0.5.6

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

๐Ÿš€ EdiHybridCache - Event Driven Cache Invalidation with Hybrid Cache

The .NET Hybrid Cache Library โ€” Blazing Fast, Battle-Tested, Enterprise-Ready

.NET Coverage Build Publish NuGet Package License


Why EdiHybridCache?

Hybrid caching combines the speed of in-process memory (L1) with the durability and sharing of Redis (L2). EdiHybridCache takes this further with:

  • โšก L1: IMemoryCache โ€” microsecond reads, zero network
  • ๐Ÿ“ก L2: Redis โ€” shared across instances, persistent
  • ๐Ÿง  Event-driven invalidation: RabbitMQ fanout โ€” invalidate all L1s instantly
  • ๐Ÿ›ก๏ธ Anti-stampede: Per-key async locking โ€” only one request hits Redis
  • ๐Ÿ” Resilience: Polly retries with exponential backoff + jitter
  • ๐Ÿ“ฆ Compression: GZip for large values
  • ๐Ÿ”’ Secure by design: CWE-409, CWE-502, CWE-754, CWE-295, CWE-770 mitigated

๐Ÿ“Š Performance

Metric Value Proof
GetAsync L1 Hit 2.23 ฮผs, 72 B allocated Benchmark
SetAsync (100B) 55.0 ฮผs, 1.7 KB allocated Benchmark
Throughput (standalone) 15,164 req/s @ 5,000 VUs k6 Load Test
Throughput (Aspire) 14,063 req/s @ 5,000 VUs k6 Load Test
Failures 0.00% @ 2.5M requests k6 Load Test
Code Coverage 90.57% line, 83.33% branch Coverage
CRAP Score Reduced up to 69% Complexity

๐Ÿ“ฆ Installation

dotnet add package EdiHybridCache

Or reference the project directly:

<ProjectReference Include="..\src\EdiHybridCache\EdiHybridCache.csproj" />

๐Ÿ” Verifying Package Signature (Sigstore)

Every NuGet release is signed with Sigstore using a private key stored as a GitHub Actions secret. The public key (cosign.pub) is available in the repository and in every workflow artifact.

  1. Download the .sig file for the release version from the workflow artifacts and the public key from the repo root.
  2. Verify with cosign:
cosign verify-blob \
  --key cosign.pub \
  --signature EdiHybridCache.X.Y.Z.nupkg.sig \
  EdiHybridCache.X.Y.Z.nupkg

Replace X.Y.Z with the actual version number. Verification succeeds if the package was signed by the trusted private key.


๐Ÿ”ง Quick Start

1. Register in DI

// Program.cs
builder.Services.AddEdiHybridCache(builder.Configuration);

2. Configure appsettings.json

{
  "EdiHybridCache": {
    "RedisConnectionString": "localhost:6379",
    "RabbitMqHost": "localhost",
    "L1TtlSeconds": 300,
    "DefaultL2TtlSeconds": 3600,
    "EnableCompression": true
  }
}

3. Inject and Use

public class MyService
{
    private readonly IHybridCache _cache;

    public MyService(IHybridCache cache) => _cache = cache;

    public async Task<string?> GetUserAsync(int id)
    {
        var key = $"user:{id}";
        return await _cache.GetAsync<string>(key);
    }

    public async Task SetUserAsync(int id, string data)
    {
        var key = $"user:{id}";
        await _cache.SetAsync(key, data, TimeSpan.FromMinutes(30));
    }

    public async Task RemoveUserAsync(int id)
    {
        var key = $"user:{id}";
        await _cache.RemoveAsync(key);
    }
}

4. Start the Invalidation Subscriber (optional)

using (var scope = app.Services.CreateScope())
{
    await scope.ServiceProvider.UseEdiHybridCacheSubscriberAsync();
}

๐Ÿ—๏ธ Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Instance A  โ”‚     โ”‚  Instance B  โ”‚     โ”‚  Instance C  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚     โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚     โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  L1   โ”‚   โ”‚     โ”‚  โ”‚  L1   โ”‚   โ”‚     โ”‚  โ”‚  L1   โ”‚   โ”‚
โ”‚  โ”‚Memory โ”‚   โ”‚     โ”‚  โ”‚Memory โ”‚   โ”‚     โ”‚  โ”‚Memory โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜   โ”‚     โ”‚  โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜   โ”‚     โ”‚  โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜   โ”‚
โ”‚      โ”‚       โ”‚     โ”‚      โ”‚       โ”‚     โ”‚      โ”‚       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”   โ”‚     โ”‚  โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”   โ”‚     โ”‚  โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  L2   โ”‚   โ”‚     โ”‚  โ”‚  L2   โ”‚   โ”‚     โ”‚  โ”‚  L2   โ”‚   โ”‚
โ”‚  โ”‚ Redis โ”‚   โ”‚     โ”‚  โ”‚ Redis โ”‚   โ”‚     โ”‚  โ”‚ Redis โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚     โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚     โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚      โ”‚       โ”‚     โ”‚      โ”‚       โ”‚     โ”‚      โ”‚       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚                    โ”‚                    โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚                    โ”‚
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚   RabbitMQ    โ”‚    โ”‚   RabbitMQ    โ”‚
          โ”‚  Exchange     โ”‚    โ”‚  Queue (each  โ”‚
          โ”‚  (Fanout)     โ”‚โ”€โ”€โ”€โ–ถโ”‚   instance)   โ”‚
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

L1 โ€” In-Process Memory

  • Provider: Microsoft.Extensions.Caching.Memory
  • Latency: 1.28 ฮผs (microseconds)
  • Allocation: 144 B per hit (dropping to ~104 B with ValueTask)
  • Scope: Per-instance, ephemeral

L2 โ€” Redis

  • Provider: StackExchange.Redis
  • Persistence: Shared across all instances
  • Resilience: Automatic retry via Polly (exponential backoff 1s โ†’ 2s โ†’ 4s + jitter)
  • Connection: Singleton via DI

Event-Driven Invalidation

  • Provider: RabbitMQ fanout exchange
  • Flow: RemoveAsync โ†’ publish event โ†’ all subscribers receive โ†’ each clears its L1
  • Graceful degradation: If RabbitMQ is unavailable, invalidation events are skipped with a warning
  • Durable: Messages are persistent; queues are auto-deleted per instance

๐Ÿ“š API Reference

ValueTask<T?> GetAsync<T>(string key, CancellationToken ct = default)

Retrieves a value from the cache.

Behavior:

  1. Check L1 (memory) โ†’ if found, return immediately (synchronous ValueTask, zero Task allocation)
  2. Acquire per-key async lock
  3. Double-check L1 (anti-stampede)
  4. Read from L2 (Redis) with Polly retry
  5. If found, populate L1 and return
  6. If not found, return null
var user = await cache.GetAsync<User>("user:42");
// Returns null if not found

Task SetAsync<T>(string key, T value, TimeSpan? ttlL2 = null, CancellationToken ct = default)

Stores a value in the cache.

Behavior:

  1. Validate key length (max 512 chars)
  2. Serialize value with System.Text.Json (camelCase)
  3. Optionally compress with GZip (threshold configurable)
  4. Write to L1 (memory) - always
  5. Write to L2 (Redis) with Polly retry
  6. Log the operation
await cache.SetAsync("user:42", user, TimeSpan.FromMinutes(30));
// Uses L1 TTL from config, L2 TTL = 30min (adjusted if below minimum)

TTL Adjustment:

  • If ttlL2 < L1TtlSeconds ร— L2TtlMultiplier, it's automatically raised to the minimum
  • This prevents race conditions where L2 expires before L1

Task RemoveAsync(string key, CancellationToken ct = default)

Removes a value from the cache and notifies other instances.

Behavior:

  1. Remove from L1 (memory)
  2. Delete from L2 (Redis) with Polly retry
  3. Publish invalidation event via RabbitMQ (best-effort)
await cache.RemoveAsync("user:42");

Task PublishInvalidationAsync(string key, CancellationToken ct = default)

Publishes an invalidation event without modifying the local cache. Useful when another service writes directly to Redis.

await cache.PublishInvalidationAsync("user:42");

void InvalidateLocal(string key)

Synchronously removes a value from L1 only. No network I/O.

if (cache is HybridCache hc)
    hc.InvalidateLocal("user:42");

๐Ÿ”’ Security

CWE CVSS Vulnerability Status
CWE-409 7.5 ZIP Bomb โ€” decompression bomb โœ… Hard cap at 100 MB, detection via leftover bytes
CWE-502 6.5 Deserialization injection โœ… Type-safe System.Text.Json + where T : class + exception logging
CWE-754 7.5 Deadlock from .GetAwaiter().GetResult() โœ… Async lazy init โ€” no blocking in constructors
CWE-295 7.4 Missing SSL/TLS for RabbitMQ โœ… Configurable via RabbitMqUseSsl, RabbitMqSslServerName, RabbitMqSslCertificatePath
CWE-770 5.3 Unbounded resource allocation โœ… Size limits โ€” max key length (512), max value size (100 MB)
CWE-312 5.9 Cleartext secrets in memory โš ๏ธ Documented โ€” operate on trusted network, HMAC/add encryption if needed
CWE-117 3.1 Log injection โœ… Structured logging via LoggerMessage.Define โ€” no string interpolation in logs

๐Ÿ“ˆ Benchmark Results

BenchmarkDotNet v0.14.0, .NET 10.0.9, AMD Ryzen 7 5700U
9 benchmarks, 2 warmup, 5 iterations each
Method Mean Gen0 Gen1 Allocated
GetAsync L1 Hit 2.23 ฮผs 0.03 โ€” 72 B
GetAsync L2 Hit 6,699 ฮผs โ€” โ€” 25.2 KB
GetAsync L2 Miss 6,614 ฮผs โ€” โ€” 26.3 KB
SetAsync 100B 55.0 ฮผs 0.24 โ€” 1.7 KB
SetAsync 10KB 29.7 ฮผs 1.71 0.73 11.5 KB
SetAsync 200KB (LOH) 244 ฮผs โ€” โ€” 201.8 KB
SetAsync 10KB + compress 22.5 ฮผs 3.97 1.19 11.9 KB ๐Ÿ“‰
RemoveAsync 9.30 ฮผs 0.37 0.11 2.4 KB
InvalidateLocal 7.34 ฮผs 0.24 0.06 1.5 KB

Key takeaways:

  • GetAsync L1 Hit in 2.23 ฮผs, 72 B allocated โ€” Zero-allocation fast path via synchronous lock acquisition
  • SetAsync 10KB + compress: 45% less allocations (22 KB โ†’ 11.9 KB) after removing extra MemoryStream copy in TryDecompress
  • Zero LOH allocations on hot paths โ€” ArrayPool<byte> + struct Releaser + ReadOnlySpan
  • LoggerMessage.Define eliminated params object[] allocation, saving ~32 B per hot log call

๐Ÿงช k6 Load Test

Standalone (direct Redis connection)

15,164 req/s ยท 0% failure ยท p(95) = 226 ms ยท 5,000 VUs
1,289,184 total requests, 1,718,912 checks passed โœ…

Aspire AppHost (with DCP proxy)

14,063 req/s ยท 0% failure ยท p(95) = 225 ms ยท 5,000 VUs
1,195,692 total requests, 1,594,256 checks passed โœ…

Test scenario: Set โ†’ Get(L1) โ†’ InvalidateLocal โ†’ Get(L2) โ†’ Remove โ†’ Get(Miss) (6 requests/iteration)

Metric Standalone Aspire AppHost
Peak throughput 15,164 req/s 14,063 req/s
Avg latency 78 ms 73 ms
p(95) latency 226 ms โœ… (< 2000ms) 225 ms โœ… (< 2000ms)
HTTP failures 0.00% 0.00%
Total requests 1,289,184 1,195,692
Total checks 1,718,912 โœ“ 1,594,256 โœ“
L1 hit rate 100% 100%
L2 hit rate 100% 100%

Status Final

Metric Status
Standalone throughput 15,164 req/s ๐Ÿ”ฅ
Aspire throughput 14,063 req/s ๐Ÿ”ฅ
p(95) latency 225 ms โœ…
Failures 0.00%
p(95) < 2s threshold โœ… Passed
Memory usage ~200 MB ๐Ÿ“‰

๐Ÿ“Š Code Coverage

Metric Value
Line Coverage 90.57%
Branch Coverage 83.33%
Lines covered 222 of 245 (excluding RabbitMQ classes)

Per-Class Coverage

Class Coverage
HybridCache 100%
HybridCacheOptions 100%
CompressionHelper 100%
AsyncLock 100%
ServiceCollectionExtensions 97.87%
GetAsync state machine 97.29%
RabbitMQ classes [ExcludeFromCodeCoverage] (require infrastructure)

๐Ÿ“‰ Code Quality & Complexity

Cyclomatic Complexity Reduction

Method Before After Reduction
TryDecompress CC 8 CC 4 ๐Ÿ”ฝ 50%
DeserializeRedisValue CC 6 CC 2 ๐Ÿ”ฝ 67%
GetAsync CC 5 CC 3 ๐Ÿ”ฝ 40%
SetAsync CC 3 CC 3 โ€”
Overall CC 22 CC 12 ๐Ÿ”ฝ 45%

CRAP Score Improvement

CRAP = (CCยฒ) ร— (1 โˆ’ coverage)ยณ + CC

Method CC Coverage CRAP Before CRAP After Improvement
TryDecompress 8โ†’4 70% 13.3 5.0 ๐Ÿ”ฝ 62%
DeserializeRedisValue 6โ†’2 90% 6.4 2.0 ๐Ÿ”ฝ 69%
GetAsync 5โ†’3 95% 5.0 3.0 ๐Ÿ”ฝ 40%

Clean Code Practices

  • โœ… DRY: RedisSafeExecuteAsync<T> extracted from 3 repetitions
  • โœ… DRY: TryOverrideFromEnv / TryParseEnvInt / TryParseEnvDouble replace 8 repetitions
  • โœ… DRY: ValidateMaxSize extracted from 2 repetitions
  • โœ… Single Responsibility: Each method does one thing
  • โœ… Early Return: No else branches โ€” early exit pattern
  • โœ… Static members before instance (SA1204 compliance)
  • โœ… Zero params object[] in hot path logs (LoggerMessage.Define)
  • โœ… No magic strings โ€” all constants named

โš™๏ธ Configuration

Environment Variables

Variable Default Description
REDIS_CONNECTION โ€” Redis connection string
RABBITMQ_HOST localhost RabbitMQ host
RABBITMQ_PORT 5672 RabbitMQ port
RABBITMQ_USERNAME guest RabbitMQ username
RABBITMQ_PASSWORD guest RabbitMQ password
L1_TTL_SECONDS 300 L1 TTL (in-process memory)
DEFAULT_L2_TTL_SECONDS 3600 Default L2 TTL (Redis)
L2_TTL_MULTIPLIER 1.5 Minimum L2/L1 TTL ratio
RABBITMQ_USE_SSL false Enable SSL/TLS for RabbitMQ
RABBITMQ_SSL_SERVER_NAME โ€” RabbitMQ SSL server name
RABBITMQ_SSL_CERT_PATH โ€” RabbitMQ SSL certificate path

appsettings.json Example

{
  "EdiHybridCache": {
    "RedisConnectionString": "localhost:6379",
    "RabbitMqHost": "localhost",
    "RabbitMqPort": 5672,
    "RabbitMqUseSsl": false,
    "L1TtlSeconds": 300,
    "DefaultL2TtlSeconds": 3600,
    "L2TtlMultiplier": 1.5,
    "EnableCompression": true,
    "CompressionThresholdBytes": 4096,
    "RetryCount": 3,
    "RetryBaseDelaySeconds": 1
  }
}

๐Ÿงฐ How to Run

Docker (Redis + RabbitMQ)

Start the required infrastructure (Redis and RabbitMQ) with Docker Compose:

docker-compose up -d

This starts:

  • Redis on localhost:6379
  • RabbitMQ on localhost:5672 (AMQP) and localhost:15672 (Management UI โ€” guest/guest)

Standalone

# Build
dotnet build

# Run tests
dotnet test tests/EdiHybridCache.Tests

# Run benchmarks
dotnet run -c Release --project benchmarks/EdiHybridCache.Benchmarks

# Run the playground (Web API with Swagger) - requires Redis + RabbitMQ
# (start docker-compose first, or have Redis + RabbitMQ running locally)
dotnet run --project playground/EdiHybridCache.Playground
# Swagger UI: http://localhost:5000/swagger/index.html
# API base URL: http://localhost:5000

# Run k6 load test (while playground is running)
k6 run k6-load-test.js

The Aspire AppHost automatically provisions Redis and RabbitMQ containers, injects environment variables, and starts the Playground:

dotnet run --project src/EdiHybridCache.AppHost/EdiHybridCache.AppHost.csproj

The dashboard will be available at https://localhost:XXXXX (random port). Redis and RabbitMQ credentials are auto-generated โ€” no manual configuration needed.


๐Ÿ—๏ธ Project Structure

EdiHybridCache/
โ”œโ”€โ”€ src/EdiHybridCache/           # ๐Ÿ“š Library source
โ”‚   โ”œโ”€โ”€ Cache/
โ”‚   โ”‚   โ”œโ”€โ”€ HybridCache.cs        # Core implementation
โ”‚   โ”‚   โ”œโ”€โ”€ IHybridCache.cs       # Public interface
โ”‚   โ”‚   โ”œโ”€โ”€ HybridCacheOptions.cs # Configuration options
โ”‚   โ”‚   โ”œโ”€โ”€ AsyncLock.cs          # Per-key async locking
โ”‚   โ”‚   โ”œโ”€โ”€ CacheMetrics.cs       # OpenTelemetry metrics
โ”‚   โ”‚   โ”œโ”€โ”€ CompressionHelper.cs  # GZip compression (ArrayPool)
โ”‚   โ”‚   โ”œโ”€โ”€ Constants.cs          # Central constants
โ”‚   โ”‚   โ””โ”€โ”€ Invalidation/
โ”‚   โ”‚       โ”œโ”€โ”€ ICacheInvalidationPublisher.cs
โ”‚   โ”‚       โ”œโ”€โ”€ ICacheInvalidationSubscriber.cs
โ”‚   โ”‚       โ”œโ”€โ”€ RabbitMqInvalidationPublisher.cs
โ”‚   โ”‚       โ””โ”€โ”€ RabbitMqInvalidationSubscriber.cs
โ”‚   โ”œโ”€โ”€ Configuration/
โ”‚   โ”‚   โ””โ”€โ”€ HybridCacheServiceCollectionExtensions.cs
โ”‚   โ””โ”€โ”€ EdiHybridCache.csproj     # NuGet package
โ”œโ”€โ”€ src/EdiHybridCache.AppHost/  # ๐Ÿš€ Aspire orchestration
โ”‚   โ”œโ”€โ”€ Program.cs               # AppHost entry point
โ”‚   โ”œโ”€โ”€ AppHostConstants.cs      # Resource names & env vars
โ”‚   โ””โ”€โ”€ EdiHybridCache.AppHost.csproj
โ”œโ”€โ”€ tests/                        # โœ… Unit tests (26/26 passing)
โ”‚   โ””โ”€โ”€ EdiHybridCache.Tests/
โ”œโ”€โ”€ benchmarks/                   # โšก Performance benchmarks
โ”‚   โ””โ”€โ”€ EdiHybridCache.Benchmarks/
โ”œโ”€โ”€ playground/                   # ๐ŸŽฎ Sample Web API (Swagger)
โ”‚   โ””โ”€โ”€ EdiHybridCache.Playground/
โ”œโ”€โ”€ k6-load-test.js               # ๐Ÿ“Š Load testing script
โ””โ”€โ”€ README.md

๐Ÿง  Anti-Stampede (Cache Stampede Protection)

When a popular key expires in L1 and multiple requests arrive simultaneously, only one request hits Redis:

using (await _asyncLock.LockAsync(key, cancellationToken))
{
    // Double-check: if another thread already populated L1, return it
    if (_memoryCache.TryGetValue(key, out cached))
        return cached;

    // Only ONE request reaches Redis
    var redisValue = await _redisDb.StringGetAsync(key);
}

๐Ÿ” Resilience

  • Redis retries: Automatic Polly retry policy (configurable count + exponential backoff + jitter)
  • RabbitMQ retries: Separate Polly retry policy for publisher; background reconnection with exponential backoff (1s โ†’ 60s max) for subscriber
  • Graceful degradation: If RabbitMQ is down, cache continues operating; invalidation events are skipped with a warning; subscriber retries in background
  • Timeouts: Configurable RedisOperationTimeoutSeconds (default: 5s)
  • Connection tuning: AbortOnConnectFail=false, SyncTimeout=5s, KeepAlive=60s, ReconnectRetryPolicy for StackExchange.Redis

๐Ÿ”’ Security Features

  • Key length validation: Max 512 characters (ArgumentException)
  • Value size cap: Max 100 MB (LogWarning + skip)
  • ZIP bomb protection: Hard cap on decompression buffer doubling; leftover byte detection
  • Cache poisoning prevention: TypeNameHandling is not supported by System.Text.Json; JsonException is caught and logged with "Possible cache poisoning"
  • Deadlock prevention: No .GetAwaiter().GetResult() in constructors (CWE-754)
  • SSL/TLS: Configurable for RabbitMQ connections
  • Log injection prevention: Structured logging via LoggerMessage.Define โ€” no params object[] on hot paths

โš–๏ธ License

MIT License โ€” Free to use, modify, distribute, and incorporate into any project (commercial or not). No attribution required, though appreciated.

Copyright ยฉ 2026 Valdomiro Galo ยท ORCID

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
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
0.5.6 111 7/31/2026
0.5.5 100 7/31/2026
0.5.4 104 7/31/2026
0.5.0 96 7/31/2026
0.4.0 111 7/31/2026
0.3.0 103 7/31/2026