HybridCache.Plus
1.0.1
dotnet add package HybridCache.Plus --version 1.0.1
NuGet\Install-Package HybridCache.Plus -Version 1.0.1
<PackageReference Include="HybridCache.Plus" Version="1.0.1" />
<PackageVersion Include="HybridCache.Plus" Version="1.0.1" />
<PackageReference Include="HybridCache.Plus" />
paket add HybridCache.Plus --version 1.0.1
#r "nuget: HybridCache.Plus, 1.0.1"
#:package HybridCache.Plus@1.0.1
#addin nuget:?package=HybridCache.Plus&version=1.0.1
#tool nuget:?package=HybridCache.Plus&version=1.0.1
<p align="center"> <img src="https://raw.githubusercontent.com/sagasta/HybridCachePlus/main/assets/icon.png" width="160" height="160" alt="HybridCache.Plus Logo" /> </p>
HybridCache.Plus
HybridCache.Plus is a high-performance extension library for Microsoft.Extensions.Caching.HybridCache in .NET 10, powered by Roslyn Source Generators.
It completely eliminates magic strings in cache keys, automates cross-interface cache invalidation via repository decorators, and delivers zero-allocation span-based key formatting with full Native AOT compatibility.
⚡ Key Features
- Strongly-Typed Contracts: Define declarative cache contracts using interfaces and attributes (
[HybridCacheKeys],[CacheTemplate]). - Zero-Allocation Execution: Formats key templates at compile time using
DefaultInterpolatedStringHandlerandReadOnlySpan<char>with zero boxing and zero heap allocations. - Automated Cross-Interface Invalidation (
[InvalidatedBy]): Reader methods declare which mutator operations (e.g.,IProductRepository.UpdateProductAsync) invalidate entries. Roslyn automatically generates decorators that intercept updates and purge keys/tags fromHybridCache. - Independent L1 + L2 TTLs: Configure distinct expiration windows for local in-process memory (
LocalTtlSeconds) and distributed caching (DistributedTtlSeconds). - Configurable TTLs & Multi-Tenant Overrides: Dynamically adjust or override TTLs at runtime via
appsettings.jsonor Dependency Injection, with hierarchical per-tenant overrides (Free vs VIP tiers). - Multi-Instance Real-Time Backplane: Synchronizes L1 invalidations across pods/replicas in real time via Redis Pub/Sub with automatic local echo cancellation.
- Multi-Tenant L2 Redis Router: Dynamically routes L2 cache operations to dedicated Redis instances per tenant while preserving HybridCache's native anti-stampede concurrency semaphores.
- Native AOT Ready: 100% free of heavy runtime reflection; fully compatible with trimming and ahead-of-time compilation.
🚀 Quick Start
1. Define Your Typed Cache Contract
using HybridCache.Plus;
[HybridCacheKeys]
public partial interface ICatalogCache
{
[CacheTemplate("tenants:{tenantId}:products:{productId}",
PolicyName = "CatalogProducts",
LocalTtlSeconds = 60,
DistributedTtlSeconds = 600,
Tags = ["tenant:{tenantId}"])]
// Invalidate automatically whenever mutations occur in your Service or Repository layer:
[InvalidatedBy<IProductService>(
nameof(IProductService.UpdateProductAsync),
nameof(IProductService.DeleteProductAsync))]
ValueTask<ProductDetailDto> GetProductAsync(string tenantId, long productId);
}
Not just for repositories! You can use [InvalidatedBy<TInterface>] with any interface across your architecture: Application Services (IProductService), Command Handlers, Domain Services, or Repositories (IProductRepository).
2. Register Services in Dependency Injection
The Roslyn Source Generator automatically generates a strongly-typed extension method for every interface referenced in [InvalidatedBy<TInterface>]:
services.Decorate{InterfaceName}WithCache();
// Standard Microsoft HybridCache registration
services.AddHybridCache();
// Register HybridCache.Plus Core
services.AddHybridCachePlus(builder =>
{
// (Optional) Configure custom policy TTLs
builder.ConfigurePolicy("CatalogProducts", p => p.LocalTtlSeconds = 120);
// (Optional) Real-time multi-instance L1 synchronization via Redis Pub/Sub
builder.UseRedisBackplane(redis =>
{
redis.ChannelName = "hybridcache:evictions";
redis.Configuration = "localhost:6379"; // or provide IConnectionMultiplexer
});
// (Optional) Multi-tenant L2 Redis routing
builder.UseMultiTenantRedisL2(tenancy =>
{
tenancy.ResolveConnectionString(tenantId =>
configuration.GetConnectionString($"Redis_{tenantId}"));
});
});
// --- Register and Decorate (Auto-Generated by Source Generator) ---
// Option A: Application / Service Layer
services.AddScoped<IProductService, ProductService>();
services.DecorateProductServiceWithCache(); // <-- Auto-generated method!
// Option B: Repository Layer
// services.AddScoped<IProductRepository, ProductRepository>();
// services.DecorateProductRepositoryWithCache(); // <-- Auto-generated method!
3. Consume in Application Code
// 1. Strongly-typed cached read
var product = await cache.GetProductAsync(
tenantId: "tenant_1",
productId: 101,
factory: async ct => await productService.LoadProductFromDatabase(tenantId, productId, ct));
// 2. Service/Repository mutation: the auto-generated decorator intercepts the call,
// executes the real business logic, purges the local cache (L1/L2), and broadcasts eviction across Redis
await productService.UpdateProductAsync("tenant_1", 101, newPrice: 49.99m);
// 3. Next read on ANY pod instantly detects invalidation and re-executes the factory
var updatedProduct = await cache.GetProductAsync("tenant_1", 101, factory);
🎯 CQRS & Clean Architecture: Commands, Queries & Complex Objects
HybridCache.Plus natively supports complex objects (Records, Classes, DTOs) in both reader contracts and mutating decorators without requiring primitive parameter lists:
1. Complex Query Objects in Cache Contracts
You can pass query objects to your cache methods and reference nested properties using dot notation or smart property matching:
public record GetProductQuery(string TenantId, long ProductId);
[HybridCacheKeys]
public partial interface ICatalogCache
{
// Option A: Explicit property navigation
[CacheTemplate("tenants:{query.TenantId}:products:{query.ProductId}", Tags = ["tenant:{query.TenantId}"])]
ValueTask<ProductDetailDto> GetProductAsync(GetProductQuery query);
// Option B: Smart property convention (Roslyn automatically maps {tenantId} -> query.TenantId)
// [CacheTemplate("tenants:{tenantId}:products:{productId}")]
// ValueTask<ProductDetailDto> GetProductAsync(GetProductQuery query);
}
2. Command Objects in Mutator Decorators
In Clean Architecture and CQRS, mutating methods typically receive command objects (e.g. UpdateProductCommand). Roslyn inspects the command's public properties at compile time and automatically resolves the cache eviction template:
public record UpdateProductCommand(string TenantId, long ProductId, string Name, decimal Price);
public interface IProductService
{
// Reader declared: [CacheTemplate("tenants:{tenantId}:products:{productId}")]
// Roslyn resolves: $"tenants:{command.TenantId}:products:{command.ProductId}"
Task UpdateProductAsync(UpdateProductCommand command);
// Invalidate by tag directly on the command object:
[InvalidatesTag("tenant:{command.TenantId}")]
Task PurgeTenantAsync(PurgeTenantCommand command);
}
The auto-generated decorator compiles into clean, allocation-free Native AOT C#:
public async Task UpdateProductAsync(UpdateProductCommand command)
{
await _inner.UpdateProductAsync(command).ConfigureAwait(false);
// Automatically resolved and emitted by Roslyn at compile time:
await _cache.RemoveAsync($"tenants:{command.TenantId}:products:{command.ProductId}", cancellationToken).ConfigureAwait(false);
}
🌐 Redis Eviction Backplane (Multi-Instance L1 Sync)
When multiple application replicas (pods) run HybridCache, an eviction on Instance A purges its local L1 and Redis L2, but Instances B, C, and D retain stale entries in their local L1 until their local TTL expires.
With HybridCache.Plus.Backplane.Redis:
- Every mutation or
Evict...Asynccall publishes a compact message (BackplaneEvictionMessage) via Redis Pub/Sub. - Reflection-free serialization using pre-compiled Native AOT
JsonSerializerContext. - Automatic echo cancellation (
OriginInstanceId == CurrentInstanceId). - Background worker (
RedisEvictionBackplaneWorker) instantly purges the local L1 cache on all receiving replicas.
🏢 Multi-Tenant L2 Router (Isolated Redis per Tenant)
Allows different tenants to reside in physically isolated Redis clusters (for compliance, data sovereignty, or performance) while preserving HybridCache anti-stampede concurrency protection:
services.AddHybridCachePlus(builder =>
{
builder.UseMultiTenantRedisL2(redis =>
{
redis.ResolveConnectionString(tenantId =>
configuration.GetConnectionString($"Redis_{tenantId}")
?? configuration.GetConnectionString("Redis_Default")!);
redis.EnableKeyPrefixTenantExtraction = true; // Extracts tenant from "tenants:{tenantId}:..." using Spans
});
});
- L1 and L2 Isolation: Physical key separation (
tenants:{tenantId}:...) prevents cross-tenant L1 cache key collisions. - Async Pass-Through: Implements
IDistributedCacheas a lightweight pass-through to avoid breaking HybridCache's native concurrency semaphores. - Zero-Allocation: Extracts tenant IDs via
ReadOnlySpan<char>or ambient context viaITenantContextAccessor(AsyncLocal).
⏱️ Configurable TTLs & Multi-Tenant Overrides
Adjust or override LocalTtlSeconds and DistributedTtlSeconds dynamically from appsettings.json or DI without recompilation, with cascading multi-tenant rules:
{
"HybridCachePlus": {
"Policies": {
"CatalogProducts": {
"LocalTtlSeconds": 60,
"DistributedTtlSeconds": 600
}
},
"Tenants": {
"tenant_vip": {
"Policies": {
"CatalogProducts": {
"LocalTtlSeconds": 10,
"DistributedTtlSeconds": 60
}
}
},
"tenant_free": {
"Policies": {
"CatalogProducts": {
"LocalTtlSeconds": 600,
"DistributedTtlSeconds": 86400
}
}
}
}
}
}
Or programmatically in AddHybridCachePlus:
services.AddHybridCachePlus(builder =>
{
builder.ConfigurePolicy("CatalogProducts", p => p.LocalTtlSeconds = 120);
builder.ConfigureTenantPolicy("tenant_vip", "CatalogProducts", p => p.LocalTtlSeconds = 10);
});
- Cascading Fallback: Tenant-specific policy ➔ Tenant default ➔ Global policy ➔ Global default ➔ Attribute values.
- Zero-Allocation Hot Path: Pre-computes
HybridCacheEntryOptionsinstances for $O(1)$ lookups during cache access.
📊 Observability & Metrics (OpenTelemetry & .NET Aspire)
HybridCache.Plus includes built-in, production-grade observability powered by System.Diagnostics.Metrics and ActivitySource. It seamlessly integrates with OpenTelemetry, Prometheus, Grafana, and the .NET Aspire Dashboard.
Emitted Metrics (Meter: "HybridCache.Plus")
| Metric Name | Type | Description | Dimensions / Tags |
|---|---|---|---|
hybridcache_plus.hits |
Counter ({hits}) |
Successful cache hits served from L1 or L2 without executing the database factory. | cache.policy, cache.tenant, cache.template |
hybridcache_plus.misses |
Counter ({misses}) |
Cache misses that triggered a factory execution against the database. | cache.policy, cache.tenant, cache.template |
hybridcache_plus.evictions |
Counter ({evictions}) |
Cache entries purged locally or remotely. | eviction.reason (DecoratorMutation, Backplane, ManualEvict), cache.tenant, cache.key |
hybridcache_plus.backplane.published |
Counter ({messages}) |
Eviction notices published across Redis Pub/Sub to invalidate remote pods. | cache.tenant, cache.key |
hybridcache_plus.backplane.received |
Counter ({messages}) |
Remote eviction notices received from other pods to purge local L1. | cache.tenant, cache.key |
hybridcache_plus.duration |
Histogram (ms) |
Execution latency of cache operations in milliseconds. | cache.operation, cache.policy, cache.tenant |
OpenTelemetry Setup
Register the meter and tracing source in your service collection using the constants in HybridCachePlusDiagnostics:
using HybridCache.Plus.Diagnostics;
services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter(HybridCachePlusDiagnostics.MeterName); // "HybridCache.Plus"
})
.WithTracing(tracing =>
{
tracing.AddSource(HybridCachePlusDiagnostics.ActivitySourceName); // "HybridCache.Plus"
});
Zero-Overhead Fast Path & Opt-Out
- Zero-Allocation Fast Path: If no metric listener or exporter is actively subscribed to the meter, metrics collection is skipped with a single boolean flag check (
counter.Enabled), ensuring 100% zero overhead in performance-critical paths. - Global Opt-Out: You can disable diagnostics completely in builder:
services.AddHybridCachePlus(builder => { builder.EnableDiagnostics(false); // Disables all metrics and tracing });
🛠️ Compile-Time Roslyn Diagnostics
HybridCache.Plus enforces best practices at compile time:
| Code | Severity | Description |
|---|---|---|
HCP001 |
Error | A key template placeholder ({param}) does not exist in the method parameter list. |
HCP002 |
Error | The interface or method specified in [InvalidatedBy] does not exist or is inaccessible. |
HCP003 |
Error | The contract method decorated with [CacheTemplate] does not return ValueTask<T> or Task<T>. |
HCP004 |
Warning | The method declares a tenantId parameter but the key template omits {tenantId}, risking cross-tenant L1 collisions. |
📦 Modular NuGet Packages
To keep dependencies strictly minimal, HybridCache.Plus is distributed across 3 independent packages:
| Package | Purpose | Dependencies |
|---|---|---|
HybridCache.Plus |
Core: Typed contracts, Source Generator, zero-allocation span formatting, automated invalidation decorators, policy registry. | Microsoft.Extensions.Caching.Hybrid |
HybridCache.Plus.Backplane.Redis |
L1 Sync: Real-time multi-pod L1 invalidation synchronization via Redis Pub/Sub. | HybridCache.Plus, StackExchange.Redis |
HybridCache.Plus.Tenancy.Redis |
L2 Multi-Tenancy: Dynamic Redis routing and isolated connection pool per tenant. | HybridCache.Plus, Microsoft.Extensions.Caching.StackExchangeRedis |
# Install lightweight core (Zero Redis dependencies)
dotnet add package HybridCache.Plus
# (Optional) For real-time multi-pod L1 invalidation backplane
dotnet add package HybridCache.Plus.Backplane.Redis
# (Optional) For dynamic multi-tenant L2 Redis routing
dotnet add package HybridCache.Plus.Tenancy.Redis
💖 Support & Sponsoring
If HybridCache.Plus has helped simplify your architecture, speed up your applications, or saved you development time, consider supporting ongoing open-source maintenance:
<p align="left"> <a href="https://buymeacoffee.com/sagasta" target="_blank"> <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="48" /> </a> </p>
📄 License
Licensed under the MIT License.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.Caching.Hybrid (>= 10.10.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on HybridCache.Plus:
| Package | Downloads |
|---|---|
|
HybridCache.Plus.Backplane.Redis
Real-time multi-instance L1 cache invalidation synchronization via Redis Pub/Sub for HybridCache.Plus in .NET 10 with Native AOT support. |
|
|
HybridCache.Plus.Tenancy.Redis
Multi-tenant dynamic Redis L2 cache routing for HybridCache.Plus in .NET 10 with Native AOT support and anti-stampede concurrency protection. |
GitHub repositories
This package is not used by any popular GitHub repositories.