Dragonfire.Caching.Memory 8.3.3

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

Dragonfire.Caching

Composable, production-ready caching for .NET 8. Start with an in-memory provider and swap in Redis, a hybrid tier, distributed tag invalidation, or Protobuf serialization — each as a separate NuGet package with no mandatory dependencies on things you don't use.


The problem this solves

In large codebases, caching tends to rot. It starts as a few _memoryCache.TryGetValue calls scattered across services. Over time you get:

  • Stale data bugs — a write somewhere doesn't know which cache keys to evict, so users see old data until TTL expires.
  • Cache stampedes — 200 requests all miss at the same time, all hit the database simultaneously, all try to write the same key back.
  • Untestable services — caching logic is baked into service methods. You can't test the business logic without also testing the cache.
  • Scattered key strings"user:42" is typed as a raw string in a dozen places. One typo, one missed update, and invalidation breaks silently.
  • Provider lock-in — you chose IMemoryCache on day one. Now you need Redis. Everything has to change.
  • No observability — you have no idea what your hit rate is, which operations are cold, or how large the cache has grown.

Dragonfire.Caching addresses all of these:

Problem Solution
Stale data Tag-based invalidation — one InvalidateByTagAsync("user:42") evicts every related key
Cache stampedes CacheLockManager — per-key SemaphoreSlim prevents concurrent factory execution
Untestable services ICacheService / ICacheProvider are interfaces — mock them freely
Scattered key strings [Cache(KeyTemplate = "user:{id}")] or fluent builder — key format lives next to the method
Provider lock-in Swap providers by changing one AddDragonfire* call in Program.cs
No observability CacheStatistics per provider + OpenTelemetry counters via System.Diagnostics.Metrics

Packages

Install only what you need. Every package targets net8.0.

Dragonfire.Caching                         ← always required (core)
Dragonfire.Caching.Memory                  ← in-process IMemoryCache provider
Dragonfire.Caching.Distributed             ← any IDistributedCache backend
Dragonfire.Caching.Hybrid                  ← L1 memory + L2 distributed, auto-promote
Dragonfire.Caching.Redis                   ← Redis-backed tag index (distributed invalidation)
Dragonfire.Caching.Serialization.Protobuf  ← swap JSON for Protobuf
Dragonfire.Caching.Generator               ← compile-time proxy generator (replaces DispatchProxy)
Dragonfire.Caching.Grpc                    ← cache-aside interceptors for gRPC client + server
dotnet add package Dragonfire.Caching
dotnet add package Dragonfire.Caching.Memory

Quick start

In-memory (single node)

// Program.cs
builder.Services.AddDragonfireMemoryCache(
    configureMemory: o => o.SizeLimit = 50_000);
// Anywhere in your app
public class OrderService(ICacheService cache)
{
    public Task<Order?> GetAsync(int id) =>
        cache.GetOrAddAsync(
            key: $"order:{id}",
            factory: () => _db.Orders.FindAsync(id).AsTask(),
            configureOptions: o => o.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5));
}

Redis-backed distributed cache

// Register StackExchange.Redis first (your choice of package)
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

// Then the provider
builder.Services.AddDragonfireDistributedCache();

Hybrid — L1 memory + L2 Redis

Reads hit L1 first. On L1 miss the value is fetched from Redis and promoted to L1. Writes go to both tiers in parallel.

builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

builder.Services.AddDragonfireHybridCache(
    configureMemory: o => o.SizeLimit = 10_000);

Configuration-driven caching (CacheExecutor)

For large teams, keeping TTLs and tag policies out of C# attributes and in configuration means you can tune caching without a redeploy.

appsettings.json

{
  "Caching": {
    "DefaultTtlSeconds": 300,
    "Operations": {
      "Order.GetById": {
        "TtlSeconds": 600,
        "Tags": ["order:{Id}"]
      },
      "Order.GetByUser": {
        "TtlSeconds": 120,
        "Tags": ["user:{UserId}:orders"]
      },
      "Order.Update": {
        "InvalidatesTags": ["order:{Id}", "user:{UserId}:orders"]
      }
    }
  }
}

Program.cs

builder.Services
    .AddDragonfireMemoryCache()
    .AddDragonfireCaching(builder.Configuration, configure: b =>
        b.UseQueuedInvalidation(o =>
        {
            o.Capacity      = 10_000;  // bounded queue
            o.DropWhenFull  = false;   // back-pressure (default)
            o.ConsumerCount = 2;       // parallel invalidation workers
        }));

Define operations (once, as constants)

public static class OrderOps
{
    public static readonly CacheOperation GetById   = new("Order.GetById");
    public static readonly CacheOperation GetByUser = new("Order.GetByUser");
    public static readonly CacheOperation Update    = new("Order.Update");
}

Use in a service

public class OrderService(CacheExecutor cache)
{
    // Cache result. Tags registered from config ("order:99").
    public Task<Order> GetByIdAsync(int id) =>
        cache.GetOrCreateAsync(
            OrderOps.GetById,
            parameters: new { Id = id },
            factory: () => _db.FindAsync(id));

    // Execute action, then enqueue tag invalidation asynchronously.
    public Task UpdateAsync(int id, int userId, OrderDto dto) =>
        cache.ExecuteAndInvalidateAsync(
            OrderOps.Update,
            parameters: new { Id = id, UserId = userId },
            action: () => _db.UpdateAsync(id, dto));
}

The InvalidationWorker background service processes the queue and evicts all keys tagged order:99 and user:7:orders — without the service needing to know what those keys are.


Attribute-based caching (decorator pattern)

Register your service with the proxy decorator. No base class, no interface changes needed.

// Program.cs
builder.Services.AddDragonFireCachedService<IOrderService, OrderService>();
public class OrderService : IOrderService
{
    // Cache with a template — only {id} in the key.
    [Cache(KeyTemplate = "order:{id}", AbsoluteExpirationSeconds = 600)]
    public virtual Task<Order?> GetByIdAsync(int id) =>
        _db.Orders.FindAsync(id).AsTask();

    // Use tags for group invalidation.
    [Cache(
        KeyTemplate = "user:{userId}:orders:{status}",
        SlidingExpirationSeconds = 120,
        Tags = ["user:{userId}:orders"])]
    public virtual Task<List<Order>> GetByUserAsync(int userId, OrderStatus status) =>
        _db.Orders.Where(o => o.UserId == userId && o.Status == status).ToListAsync();

    // Evict by key pattern and tag after the write completes.
    [CacheInvalidate("order:{id}:*")]
    [CacheInvalidate(Tag = "user:{userId}:orders")]
    public virtual Task UpdateAsync(int id, int userId, OrderDto dto) =>
        _db.UpdateAsync(id, dto);
}

Important: Methods must be virtual (or on an interface) for the proxy to intercept them.


Multiple parameters — which go into the key?

You have four options, from most explicit to least.

Name exactly the placeholders you want. Everything else is ignored.

// Method: GetOrdersAsync(int userId, OrderStatus status, bool includeDeleted, string sortBy)
// Key:    "orders:42:Active"  — includeDeleted and sortBy are irrelevant to the cache result

[Cache(KeyTemplate = "orders:{userId}:{status}", AbsoluteExpirationSeconds = 300)]
public virtual Task<List<Order>> GetOrdersAsync(
    int userId, OrderStatus status, bool includeDeleted, string sortBy) { ... }

You can also use positional placeholders ({0}, {1}, ...):

[Cache(KeyTemplate = "orders:{0}:{1}", AbsoluteExpirationSeconds = 300)]
public virtual Task<List<Order>> GetOrdersAsync(int userId, OrderStatus status, ...) { ... }

Option 2 — [CacheKey] attribute (selective auto-generation)

Mark the parameters that matter. Only marked ones appear in the auto-generated key. Untagged ones (including CancellationToken, audit flags, etc.) are excluded.

[Cache(AbsoluteExpirationSeconds = 300)]
public virtual Task<List<Order>> GetOrdersAsync(
    [CacheKey] int userId,           // ✅ in key
    [CacheKey] OrderStatus status,   // ✅ in key
    bool includeDeleted,             // ❌ excluded (another param has [CacheKey])
    CancellationToken ct)            // ❌ excluded
// → key: "OrderService.GetOrdersAsync(userId=42,status=Active)"

You can also rename a parameter in the key:

[Cache(AbsoluteExpirationSeconds = 300)]
public virtual Task<Order?> GetByIdAsync([CacheKey("id")] int orderId, string expand)
// → key: "OrderService.GetByIdAsync(id=99)"

Option 3 — All parameters (no template, no [CacheKey])

Safe when all parameters are value types or strings. Complex objects fall back to GetHashCode() which is not stable across restarts — use Option 1 or 2 for those.

[Cache(AbsoluteExpirationSeconds = 300)]
public virtual Task<Report> GenerateAsync(int year, int month, ReportType type)
// → key: "ReportService.GenerateAsync(year=2024,month=3,type=Monthly)"

Option 4 — Fluent builder (no attributes at all)

builder.Services.AddDragonFireCachedService<IOrderService, OrderService>(b => b
    .Cache(
        s => s.GetOrdersAsync(default, default, default, default),
        cacheKeyTemplate: "orders:{userId}:{status}",   // only these two in key
        expiration: TimeSpan.FromMinutes(5))
    .Invalidate(
        s => s.UpdateOrderAsync(default, default),
        cacheKeyTemplate: "orders:{userId}:*"));        // glob wipes all statuses

Summary

Approach Which params go in the key
KeyTemplate = "x:{a}:{b}" Only the placeholders you list
[CacheKey] on some params Only the marked params
[CacheKey] on no params All params (same as next row)
No template, no [CacheKey] All params (value types / strings — safe)
Manual ICacheService Whatever string you build yourself

Tag-based invalidation

Tags let you evict a group of related keys with a single call, regardless of how many variations are cached.

// Store with tags
await cache.GetOrAddWithTagsAsync(
    key: $"user:{userId}:profile",
    factory: () => _db.GetProfileAsync(userId),
    tags: [$"user:{userId}"],
    configureOptions: o => o.SlidingExpiration = TimeSpan.FromMinutes(10));

await cache.GetOrAddWithTagsAsync(
    key: $"user:{userId}:orders:pending",
    factory: () => _db.GetOrdersAsync(userId, OrderStatus.Pending),
    tags: [$"user:{userId}", "orders:pending"]);

// One call evicts ALL keys tagged "user:42" — profile and orders both gone
await cache.InvalidateByTagAsync($"user:{userId}");

For distributed scenarios (multiple nodes), replace the default in-memory tag index with Redis:

// Option A — from the caching builder
builder.Services.AddDragonfireCaching(b => b.UseRedisTagIndex());

// Option B — standalone registration (if IConnectionMultiplexer is already registered)
builder.Services.AddDragonfireRedisTagIndex();

// Option C — register multiplexer and tag index together
builder.Services.AddDragonfireRedisTagIndex("localhost:6379");

Invalidation queue parameters

The channel-based invalidation queue can be tuned for high-throughput scenarios.

builder.Services.AddDragonfireCaching(builder.Configuration, configure: b =>
    b.UseQueuedInvalidation(o =>
    {
        // null = unbounded (default). Set a number to apply back-pressure.
        o.Capacity = 50_000;

        // When at capacity:
        //   false (default) = wait for space (back-pressure, no data loss)
        //   true            = silently drop the oldest item (lossy, never blocks)
        o.DropWhenFull = false;

        // Number of parallel workers draining the queue.
        // Increase if invalidation throughput is a bottleneck.
        o.ConsumerCount = 4;
    }));

Direct invalidation (without the queue)

If you need synchronous, guaranteed invalidation before returning to the caller:

// By exact key
await cache.RemoveAsync("order:99");

// By glob pattern (all variants of an order)
await cache.RemoveByPatternAsync("order:99:*");

// By tag (all keys associated with this tag)
await cache.InvalidateByTagAsync("user:42");

Note: RemoveByPatternAsync on the DistributedCacheProvider uses an in-process key index — it works correctly when all writes go through the same provider instance. For multi-node deployments, use tag-based invalidation with RedisTagIndex instead.


Distributed locking (stampede prevention)

// The factory runs at most once per key, even under concurrent load.
// Other callers wait until the first one populates the cache.
var result = await cache.GetOrAddWithLockAsync(
    key: $"expensive-report:{date}",
    factory: () => _reportEngine.GenerateAsync(date),
    configureOptions: o => o.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
    lockTimeout: TimeSpan.FromSeconds(30));

GetOrAddWithLockAsync uses CacheLockManager — per-key SemaphoreSlim locks, in-process only. For distributed locking across multiple nodes, use a library like RedLock.net and call ICacheService.GetOrAddAsync inside your own lock.


Custom serializer

Protobuf (for generated IMessage types)

dotnet add package Dragonfire.Caching.Serialization.Protobuf
builder.Services.AddDragonfireDistributedCache(configure: b =>
    b.UseProtobufSerializer());

Custom JSON options

builder.Services.AddDragonfireMemoryCache(configureCaching: b =>
    b.UseJsonSerializer(new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        Converters = { new JsonStringEnumConverter() }
    }));

Bring your own serializer

Implement ICacheSerializer and register it:

public class MessagePackCacheSerializer : ICacheSerializer
{
    public byte[] Serialize<T>(T value) => MessagePackSerializer.Serialize(value);
    public T Deserialize<T>(byte[] bytes) => MessagePackSerializer.Deserialize<T>(bytes);
}

builder.Services.AddDragonfireMemoryCache(configureCaching: b =>
    b.UseSerializer<MessagePackCacheSerializer>());

Custom key strategy

By default, auto-generated keys look like OrderService.GetByIdAsync(id=99). Replace the strategy globally:

public class PrefixedKeyStrategy : DefaultCacheKeyStrategy
{
    private readonly string _prefix;
    public PrefixedKeyStrategy(string prefix) => _prefix = prefix;

    public override string GenerateKey(MethodInfo method, object?[] arguments, string? keyTemplate = null)
        => $"{_prefix}:{base.GenerateKey(method, arguments, keyTemplate)}";
}

builder.Services.AddSingleton<ICacheKeyStrategy>(new PrefixedKeyStrategy("myapp"));

Custom tag index

// Replace the default in-memory tag index with any implementation:
builder.Services.AddDragonfireCaching(b =>
    b.UseTagIndex<MyCustomTagIndex>());

Custom cache provider

Implement ICacheProvider and register it:

public class DynamoDbCacheProvider : ICacheProvider { ... }

builder.Services
    .AddDragonfireCaching()
    .AddDragonfireCacheProvider<DynamoDbCacheProvider>();

Expiration options

// Absolute — expires at a fixed point in time
o.AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(2);

// Absolute relative to now — most common
o.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);

// Sliding — expiration resets on every cache hit
o.SlidingExpiration = TimeSpan.FromMinutes(10);

// Convenience factory methods
var opts = CacheEntryOptions.Absolute(TimeSpan.FromMinutes(5));
var opts = CacheEntryOptions.Sliding(TimeSpan.FromMinutes(10));
var opts = CacheEntryOptions.NeverExpire;     // Priority = NeverRemove (memory only)

// Tags + expiration together
await cache.SetAsync("key", value, o =>
{
    o.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
    o.Tags.Add("user:42");
    o.Priority = CacheItemPriority.High;      // memory provider only
});

Bulk operations

// Read many keys at once
IDictionary<string, User?> users = await provider.GetMultipleAsync<User>(
    ["user:1", "user:2", "user:3"]);

// Write many keys at once
await provider.SetMultipleAsync(
    new Dictionary<string, User>
    {
        ["user:1"] = user1,
        ["user:2"] = user2,
    },
    CacheEntryOptions.Absolute(TimeSpan.FromMinutes(10)));

Observability

Cache statistics

public class CacheDiagnosticsController(ICacheService cache) : ControllerBase
{
    [HttpGet("cache/stats")]
    public IActionResult Stats()
    {
        var s = cache.GetStatistics();
        return Ok(new
        {
            provider   = cache.ProviderName,
            hitRatio   = s.HitRatio.ToString("P1"),   // "94.3%"
            hits       = s.TotalHits,
            misses     = s.TotalMisses,
            sets       = s.TotalSets,
            removals   = s.TotalRemovals,
            entryCount = s.CurrentEntryCount,
            sinceUtc   = s.LastReset
        });
    }
}

OpenTelemetry metrics

The library records four counters under the Dragonfire.Caching meter:

Metric name Tags Description
dragonfire.cache.hits provider Total cache hits
dragonfire.cache.misses provider Total cache misses
dragonfire.cache.sets provider Total cache sets
dragonfire.cache.removals provider Total cache removals
// Prometheus via OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithMetrics(m => m
        .AddMeter("Dragonfire.Caching")
        .AddPrometheusExporter());

Caching gRPC calls

Dragonfire.Caching integrates with gRPC two different ways. Pick whichever fits your codebase — they are not mutually exclusive.

Option B — wrap the proto client behind your own interface (zero new packages)

The compile-time generator (Dragonfire.Caching.Generator) only needs an interface that implements ICacheable. Because proto-generated clients are sealed and you can't add [Cache] to them, you write a thin wrapper that is attribute-decorated, and let the generator produce the proxy:

using Dragonfire.Caching.Abstractions;
using Dragonfire.Caching.Attributes;
using Order;   // proto-generated namespace

public interface IOrderClient : ICacheable
{
    [Cache(SlidingExpirationSeconds = 300, KeyTemplate = "order:{tenantId}:{orderId}",
           Tags = new[] { "tenant:{tenantId}" })]
    Task<OrderReply> GetOrderAsync(string tenantId, string orderId);

    [CacheInvalidate("order:{tenantId}:*")]
    Task UpdateOrderAsync(string tenantId, OrderReply order);
}

public sealed class OrderClient(OrderService.OrderServiceClient inner)
    : IOrderClient, ICacheable
{
    public Task<OrderReply> GetOrderAsync(string tenantId, string orderId) =>
        inner.GetOrderAsync(new GetOrderRequest { TenantId = tenantId, OrderId = orderId })
             .ResponseAsync;

    public Task UpdateOrderAsync(string tenantId, OrderReply order) =>
        inner.UpdateOrderAsync(new UpdateOrderRequest { TenantId = tenantId, Order = order })
             .ResponseAsync;
}

Registration is unchanged from any other ICacheable:

builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(o =>
    o.Address = new Uri("https://order-service:5001"));
builder.Services.AddScoped<IOrderClient, OrderClient>();

builder.Services.AddDragonfireMemoryCache().AddDragonfireCaching();
builder.Services.AddDragonfireGeneratedCaching();   // wraps OrderClient automatically

Use this when:

  • You already have a domain layer and the gRPC client is just one of several backends.
  • You want full attribute-driven semantics (templates, tags, multiple [CacheInvalidate]).
  • You don't want a runtime gRPC interceptor in the call path.

Option D — Dragonfire.Caching.Grpc interceptors (no wrapper required)

When you can't or won't write a wrapper — for example you call the proto client directly throughout your code — install Dragonfire.Caching.Grpc. It ships two Grpc.Core.Interceptors.Interceptor subclasses that read scalar fields from the proto request via descriptor reflection (same approach as Dragonfire.Logging.Grpc) and apply cache-aside / invalidation rules registered in DI.

Client side — cache outbound unary calls:

using Dragonfire.Caching.Grpc.Configuration;
using Dragonfire.Caching.Grpc.Extensions;
using Dragonfire.Caching.Grpc.Interceptors;

builder.Services.AddDragonfireMemoryCache().AddDragonfireCaching();

builder.Services.AddDragonfireGrpcClientCaching(options =>
{
    options.Cache(new GrpcCacheRule
    {
        FullMethod        = "/order.OrderService/GetOrder",
        KeyTemplate       = "order:{tenantId}:{orderId}",
        SlidingExpiration = TimeSpan.FromMinutes(5),
        Tags              = new[] { "tenant:{tenantId}" }
    });

    options.Invalidate(new GrpcInvalidateRule
    {
        FullMethod  = "/order.OrderService/UpdateOrder",
        KeyPatterns = new[] { "order:{tenantId}:*" },
        Tags        = new[] { "tenant:{tenantId}" }
    });
});

builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(o =>
        o.Address = new Uri("https://order-service:5001"))
    .AddInterceptor<DragonfireClientCachingInterceptor>();

Server side — short-circuit inbound unary handlers:

builder.Services.AddDragonfireGrpcServerCaching(options =>
{
    options.Cache(new GrpcCacheRule
    {
        FullMethod        = "/order.OrderService/GetOrder",
        KeyTemplate       = "order:{tenantId}:{orderId}",
        SlidingExpiration = TimeSpan.FromMinutes(5)
    });
});

builder.Services.AddGrpc(o =>
    o.Interceptors.Add<DragonfireServerCachingInterceptor>());

Behaviour:

  • Unary calls only. Streaming (client-streaming, server-streaming, bidi) passes through untouched — caching streamed payloads is not safe in a generic way.
  • Cache keys are built by the registered ICacheKeyStrategy. Templates use proto JSON field names (lowerCamelCase), e.g. {tenantId} for proto field tenant_id. With no template, the auto-key is Service.Method(field=value,...).
  • IncludeFields narrows which scalar fields participate in the key — useful when the request carries a correlation ID or auth token that would make every key unique.
  • Tags templates are also resolved from the request, so a single tag like "tenant:{tenantId}" flushes everything for one tenant.

Use this when:

  • You don't own the call sites (libraries, generated clients passed around).
  • You want server-side caching for read-heavy RPCs without changing handler code.
  • You want consistent rules driven from configuration rather than attributes.

Migration guide — from IMemoryCache / IDistributedCache

Before:

if (!_cache.TryGetValue($"user:{id}", out User user))
{
    user = await _db.Users.FindAsync(id);
    _cache.Set($"user:{id}", user, TimeSpan.FromMinutes(5));
}
return user;

After:

return await _cache.GetOrAddAsync(
    $"user:{id}",
    () => _db.Users.FindAsync(id).AsTask(),
    o => o.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5));

To swap providers later, change only Program.cs — nothing in your services changes.


Architecture overview

┌─────────────────────────────────────────────────────────────┐
│  Your application                                           │
│                                                             │
│  ICacheService ──────────────────────────────────────────┐  │
│       │                                                  │  │
│  CacheExecutor (config-driven, with queued invalidation) │  │
│       │                                                  │  │
│  CachingProxy<T> (attribute or fluent decorator)         │  │
└───────┼──────────────────────────────────────────────────┼──┘
        │                                                  │
        ▼                                                  ▼
  ICacheProvider                                      ITagIndex
  ├─ MemoryCacheProvider     (Dragonfire.Caching.Memory)
  ├─ DistributedCacheProvider (Dragonfire.Caching.Distributed)
  └─ HybridCacheProvider     (Dragonfire.Caching.Hybrid)
        │                         ├─ InMemoryTagIndex  (core)
        ▼                         └─ RedisTagIndex     (Dragonfire.Caching.Redis)
  ICacheSerializer
  ├─ SystemTextJsonSerializer (core, default)
  └─ ProtobufSerializer       (Dragonfire.Caching.Serialization.Protobuf)

  IInvalidationQueue ──► InvalidationWorker (BackgroundService)
  └─ ChannelInvalidationQueue (configurable capacity + consumers)

Full example

// Program.cs — hybrid provider, Redis tags, queued invalidation
builder.Services
    .AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379")
    .AddDragonfireHybridCache(
        configureMemory: o => o.SizeLimit = 10_000,
        configureCaching: b => b
            .UseQueuedInvalidation(o => { o.Capacity = 20_000; o.ConsumerCount = 2; })
            .UseRedisTagIndex())
    .AddDragonfireCaching(builder.Configuration);   // binds CacheSettings from appsettings

// Register a service with the proxy decorator
builder.Services.AddDragonFireCachedService<IOrderService, OrderService>();
// OrderService.cs
public class OrderService : IOrderService
{
    private readonly AppDbContext _db;
    public OrderService(AppDbContext db) => _db = db;

    [Cache(
        KeyTemplate = "order:{id}",
        AbsoluteExpirationSeconds = 600,
        Tags = ["order:{id}", "user:{userId}:orders"])]
    public virtual Task<Order?> GetByIdAsync(int id, int userId) =>
        _db.Orders.FindAsync(id).AsTask();

    [Cache(
        KeyTemplate = "user:{userId}:orders:{status}",
        SlidingExpirationSeconds = 120,
        Tags = ["user:{userId}:orders"])]
    public virtual Task<List<Order>> GetByUserAsync(
        [CacheKey] int userId,
        [CacheKey] OrderStatus status,
        bool includeArchived,           // not in cache key
        CancellationToken ct = default) =>
        _db.Orders
            .Where(o => o.UserId == userId && o.Status == status)
            .ToListAsync(ct);

    [CacheInvalidate("order:{id}:*")]
    [CacheInvalidate(Tag = "user:{userId}:orders")]
    public virtual async Task UpdateAsync(int id, int userId, OrderDto dto)
    {
        await _db.UpdateOrderAsync(id, dto);
    }
}
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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 (1)

Showing the top 1 NuGet packages that depend on Dragonfire.Caching.Memory:

Package Downloads
Dragonfire.Caching.Hybrid

Hybrid (L1 memory + L2 distributed) cache provider for Dragonfire.Caching. Automatically promotes L2 hits to L1.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.3.3 114 5/2/2026
8.3.2 116 5/1/2026
8.0.1 123 4/11/2026
8.0.0 117 4/10/2026