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.
- 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.
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);
}Tip
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).
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!// 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);HybridCache.Plus natively supports complex objects (Records, Classes, DTOs) in both reader contracts and mutating decorators without requiring primitive parameter lists:
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);
}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);
}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.
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).
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.
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.
| 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 |
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-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 });
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. |
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.RedisIf HybridCache.Plus has helped simplify your architecture, speed up your applications, or saved you development time, consider supporting ongoing open-source maintenance:
Licensed under the MIT License.