IronAlpine.Framework.Caching 3.1.1

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

IronAlpine.Framework.Caching

Redis-backed query caching for the IronAlpine mediator pipeline, a typed cache service, and runtime cache settings (enable/disable a key, override its TTL) stored in the service database.

  • Package id: IronAlpine.Framework.Caching (3.1.1, net10.0)
  • Depends on: Kernel, Mediator, Data, StackExchange.Redis, Microsoft.Extensions.Caching.Memory

Installation

<PackageReference Include="IronAlpine.Framework.Caching" Version="3.1.1" />

Registration

using IronAlpine.Caching;              // UseCachingBehavior
using IronAlpine.Caching.Extensions;   // AddIronAlpineCachingRedis

services.AddIronAlpineData<OrdersDbContext>(configuration, cfg =>
    cfg.ConfigureDbContext((sp, options) => options.UseSqlServer(connectionString)));

services.AddIronAlpineCachingRedis<OrdersDbContext>(
    configureRedis: redis => redis.InstanceName = "orders",
    configureEfCore: efCore => efCore.CacheSettingsSchema = "cache");

services.AddIronAlpineMediator(cfg =>
{
    cfg.RegisterServicesFromAssembly(applicationAssembly);
    cfg.UseLoggingBehavior();
    cfg.UseCachingBehavior();   // before validation and transaction behaviours
    // ...
});

AddIronAlpineCachingRedis<TDbContext> registers:

Service Lifetime Notes
IConnectionMultiplexer singleton ConnectionMultiplexer.Connect(...) on first resolve (synchronous)
ICacheService → RedisCacheService singleton JSON (JsonSerializerDefaults.Web) string values
ICacheSettingsProvider scoped reads CacheSetting rows, memory-cached for 5 minutes
ICacheSettingsService scoped CRUD + listing for cache settings (for an admin API)
hosted service singleton subscribes to the invalidation channel and clears the in-memory settings cache
IModelContributor singleton adds the CacheSettings table to TDbContext
IMemoryCache - AddMemoryCache()

TDbContext must be registered through AddIronAlpineData (the model contributor only runs on contexts configured by the Data package). Add a migration after registering caching: the CacheSettings table becomes part of the model.

Configuration

{
  "IronAlpine": {
    "Caching": {
      "Redis": {
        "ConnectionString": "redis:6379,password=...",
        "InstanceName": "orders",
        "DefaultDatabase": 0,
        "AbortOnConnectFail": false,
        "ConnectRetry": 5,
        "ConnectTimeoutMilliseconds": 5000,
        "EnableTtlJitter": true,
        "MaxTtlJitterSeconds": 30,
        "CacheSettingsInvalidationChannel": "ironalpine:caching:settings:invalidate",
        "EFCore": {
          "CacheSettingsTableName": "CacheSettings",
          "CacheSettingsSchema": null,
          "CacheKeyMaxLength": 256
        }
      }
    }
  }
}
Section Class Validation
IronAlpine:Caching:Redis RedisOptions validated on start: ConnectionString required and parsable, InstanceName and channel non-empty, MaxTtlJitterSeconds > 0 when jitter is on, ConnectRetry >= 0, ConnectTimeoutMilliseconds >= 1000
IronAlpine:Caching:Redis:EFCore CachingRedisEfCoreOptions - (dbo schema is treated as default schema)

InstanceName is also the Redis client name and the key prefix: every key is stored as {InstanceName}:{key}. Give each service its own instance name unless services are meant to share entries.

Cacheable queries

using IronAlpine.Caching.Contracts;
using IronAlpine.Caching.Dtos;

[CacheDefinition("Order summary", CacheKey = "order-summary:{OrderId}", DefaultExpirationSeconds = 600,
    Description = "Order header with totals")]
public sealed record GetOrderSummaryQuery(Guid OrderId) : IRequest<Result<OrderSummaryDto>>, ICacheableQuery
{
    public string CacheKey => $"order-summary:{OrderId}";
    public TimeSpan? Expiration => TimeSpan.FromMinutes(10);
}

CachingBehavior<TRequest, TResponse> applies when TRequest implements ICacheableQuery and TResponse is Result<T> (or derives from it). For each request it:

  1. resolves the override for request.CacheKey from ICacheSettingsProvider (see override resolution); an override with IsEnabled = false bypasses the cache;
  2. reads {InstanceName}:{CacheKey} from Redis; a hit returns Result.Success(payload) without calling the handler;
  3. on a miss, takes an optional distributed lock (IDistributedLock, if registered, cache:{key}, 3 s timeout) and a per-key in-process semaphore (reference counted: removed as soon as no request holds or waits for the key), re-reads the cache, then calls the handler;
  4. stores the value of a successful result whose value is not null; failures are never cached.

Value-type payloads (Result<int>, Result<bool>, Result<Guid>, structs) are read as Nullable<T> so a miss (null) is distinguished from a cached 0 / false / Guid.Empty. A custom ICacheService must therefore return default (null for T?) on a miss and accept GetAsync<T?> for values written with SetAsync<T>.

TTL precedence: override ExpirationSeconds → request.Expiration → no expiry (null means the key never expires). With EnableTtlJitter a random 0..MaxTtlJitterSeconds seconds is added to every TTL.

Redis read/write errors and lock errors are logged as warnings and the handler result is returned (the cache never breaks a request).

Invalidation

public sealed class ShipOrderCommandHandler(IRepository<Order> orders, IUnitOfWork unitOfWork, ICacheService cache)
    : IRequestHandler<ShipOrderCommand, Result>
{
    public async Task<Result> Handle(ShipOrderCommand request, CancellationToken cancellationToken)
    {
        // ... mutate and save
        await unitOfWork.SaveChangesAsync(cancellationToken);
        await cache.RemoveAsync($"order-summary:{request.OrderId}", cancellationToken);
        return Result.Success();
    }
}

ICacheService: GetAsync<T>, SetAsync<T>(key, value, expiry), RemoveAsync, ExistsAsync. Keys are exact; there is no prefix or wildcard removal (RemoveAsync("orders:*") deletes a key literally named orders:*). Design keys so that every write knows the exact keys it invalidates, or keep TTLs short.

Cache settings (runtime overrides)

ICacheSettingsService backs an admin API:

Method Result
GetDefinitionsAsync all known cache definitions (see below), cached in memory for 10 minutes
GetPagedAsync(CacheSettingsFilter) definitions merged with overrides; filter by key/display name and IsEnabled, sort by cacheKey, displayName, isEnabled, defaultExpirationSeconds, overrideExpirationSeconds
GetAllAsync, GetByKeyAsync(cacheKey) same data, unpaged / single
UpsertAsync(CacheSettingUpsertDto(CacheKey, ExpirationSeconds, IsEnabled)) saves the row, clears local caches and publishes on the invalidation channel
DeleteAsync(cacheKey) removes the override
[HttpPut]
public async Task<IActionResult> Upsert(CacheSettingUpsertDto request, ICacheSettingsService settings, CancellationToken ct)
    => (await settings.UpsertAsync(request, ct)).IsSuccess ? Ok() : BadRequest();

CacheSettingsQueryParams (Page, Size, SortBy, SortDirection, CacheKey, IsEnabled) binds from a query string and converts with ToFilter().

Definitions are discovered by reflection: every non-abstract ICacheableQuery class contributes CacheDefinitionAttribute.CacheKey when set, otherwise the CacheKey of an instance created through its parameterless constructor. Scanned assemblies are CachingRedisEfCoreOptions.DefinitionAssemblies plus the loaded assemblies accepted by CachingRedisEfCoreOptions.DefinitionAssemblyFilter (default: name ends with .Application). Both are code-only options:

services.AddIronAlpineCachingRedis<OrdersDbContext>(configureEfCore: efCore =>
{
    efCore.DefinitionAssemblies.Add(typeof(GetOrderSummaryQuery).Assembly);
    efCore.DefinitionAssemblyFilter = _ => false;   // only the explicit assemblies
});

Override resolution

For a concrete key such as order-summary:42 the behaviour uses the first match of:

  1. an override saved under the exact key (order-summary:42);
  2. an override saved under the request type's CacheDefinitionAttribute.CacheKey;
  3. an override whose key is a template (order-summary:{OrderId}) that matches the concrete key. Each {name} placeholder matches one or more characters; the rest is compared literally, case-insensitively.

Up to 3.0.4 only the exact key was used, so overrides saved against a template never applied.

Metrics

Since 3.1.0 the caching behavior counts cache.hit, cache.miss (the handler ran) and cache.bypassed (a runtime override disabled caching) on the meter IronAlpine.Caching. The only tag is cache.definition: the [CacheDefinition(CacheKey = ...)] key, or the query type name when the attribute is missing. The concrete key is never a tag (it contains ids). A request that finds the value after waiting for the lock counts as a hit. AddIronAlpineTelemetry (Observability) listens to this meter; see its README for PromQL.

Extension points

  • IDistributedLock (TryAcquireAsync(resourceId, timeout, ct) returning an IAsyncDisposable lease or null): register an implementation to serialise cache misses across instances. None is shipped.
  • ICacheService: replace with another store (services.AddSingleton<ICacheService, MyCache>() before AddIronAlpineCachingRedis, which uses TryAdd).
  • ICacheSettingsProvider: replace to read overrides from configuration or another store.

Pitfalls

  • Up to 3.0.4 value-type payloads (Result<int>, Result<bool>, Result<Guid>) were broken: a miss returned default(T) and was treated as a hit. Fixed in 3.0.5 (see above).
  • Payloads round-trip through System.Text.Json. Types without a usable constructor or with private setters (for example smart enumerations or domain entities) do not deserialize correctly. Cache flat DTOs.
  • Expiration => null and no override means the entry never expires.
  • Key spaces should still be bounded in Redis (for example avoid keys containing free text search terms); the in-process per-key semaphores are released when unused (since 3.0.5).
  • Results are shared by everyone who sends the same key. Include the user, tenant or permission scope in the key for user-specific data.
  • GetAsync / SetAsync ignore the cancellation token (StackExchange.Redis does not take one).
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
3.1.1 173 10/21/2026
3.0.9 211 10/15/2026
3.0.8 114 10/13/2026
3.0.7 95 10/13/2026
3.0.6 105 10/9/2026
3.0.5 180 10/5/2026
3.0.4 192 10/2/2026
3.0.3 165 9/30/2026
3.0.2 956 7/22/2026
3.0.1 128 7/22/2026
3.0.0 320 7/20/2026
2.0.3 119 7/20/2026
2.0.2 147 7/13/2026
2.0.1 149 7/13/2026
2.0.0 144 7/13/2026
2.0.0-preview.2 77 7/13/2026
2.0.0-preview.1 87 7/6/2026

IronAlpine framework release. See the package README and repository documentation for details.