HybridCache.Plus.Tenancy.Redis 1.0.1

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

<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

CI NuGet .NET Native AOT License: MIT Buy Me A Coffee

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

  1. Strongly-Typed Contracts: Define declarative cache contracts using interfaces and attributes ([HybridCacheKeys], [CacheTemplate]).
  2. Zero-Allocation Execution: Formats key templates at compile time using DefaultInterpolatedStringHandler and ReadOnlySpan<char> with zero boxing and zero heap allocations.
  3. 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 from HybridCache.
  4. Independent L1 + L2 TTLs: Configure distinct expiration windows for local in-process memory (LocalTtlSeconds) and distributed caching (DistributedTtlSeconds).
  5. Configurable TTLs & Multi-Tenant Overrides: Dynamically adjust or override TTLs at runtime via appsettings.json or Dependency Injection, with hierarchical per-tenant overrides (Free vs VIP tiers).
  6. Multi-Instance Real-Time Backplane: Synchronizes L1 invalidations across pods/replicas in real time via Redis Pub/Sub with automatic local echo cancellation.
  7. 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.
  8. 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...Async call 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 IDistributedCache as a lightweight pass-through to avoid breaking HybridCache's native concurrency semaphores.
  • Zero-Allocation: Extracts tenant IDs via ReadOnlySpan<char> or ambient context via ITenantContextAccessor (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 HybridCacheEntryOptions instances 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 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.1 81 9/11/2026
1.0.0 84 9/11/2026