IronAlpine.Framework.Caching
3.0.8
See the version list below for details.
dotnet add package IronAlpine.Framework.Caching --version 3.0.8
NuGet\Install-Package IronAlpine.Framework.Caching -Version 3.0.8
<PackageReference Include="IronAlpine.Framework.Caching" Version="3.0.8" />
<PackageVersion Include="IronAlpine.Framework.Caching" Version="3.0.8" />
<PackageReference Include="IronAlpine.Framework.Caching" />
paket add IronAlpine.Framework.Caching --version 3.0.8
#r "nuget: IronAlpine.Framework.Caching, 3.0.8"
#:package IronAlpine.Framework.Caching@3.0.8
#addin nuget:?package=IronAlpine.Framework.Caching&version=3.0.8
#tool nuget:?package=IronAlpine.Framework.Caching&version=3.0.8
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.0.8,net10.0) - Depends on: Kernel, Mediator, Data,
StackExchange.Redis,Microsoft.Extensions.Caching.Memory
Installation
<PackageReference Include="IronAlpine.Framework.Caching" Version="3.0.8" />
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:
- resolves the override for
request.CacheKeyfromICacheSettingsProvider(see override resolution); an override withIsEnabled = falsebypasses the cache; - reads
{InstanceName}:{CacheKey}from Redis; a hit returnsResult.Success(payload)without calling the handler; - 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; - 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:
- an override saved under the exact key (
order-summary:42); - an override saved under the request type's
CacheDefinitionAttribute.CacheKey; - 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.
Extension points
IDistributedLock(TryAcquireAsync(resourceId, timeout, ct)returning anIAsyncDisposablelease ornull): register an implementation to serialise cache misses across instances. None is shipped.ICacheService: replace with another store (services.AddSingleton<ICacheService, MyCache>()beforeAddIronAlpineCachingRedis, which usesTryAdd).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 returneddefault(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 => nulland 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/SetAsyncignore the cancellation token (StackExchange.Redis does not take one).
| 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
- IronAlpine.Framework.Data (>= 3.0.8)
- IronAlpine.Framework.Kernel (>= 3.0.8)
- IronAlpine.Framework.Mediator (>= 3.0.8)
- Microsoft.EntityFrameworkCore (>= 10.0.10)
- Microsoft.Extensions.Caching.Memory (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
- StackExchange.Redis (>= 2.8.37)
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 | 219 | 10/21/2026 |
| 3.0.9 | 211 | 10/15/2026 |
| 3.0.8 | 117 | 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 | 120 | 7/20/2026 |
| 2.0.2 | 148 | 7/13/2026 |
| 2.0.1 | 150 | 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.