Cachedral.Redis
1.0.2
dotnet add package Cachedral.Redis --version 1.0.2
NuGet\Install-Package Cachedral.Redis -Version 1.0.2
<PackageReference Include="Cachedral.Redis" Version="1.0.2" />
<PackageVersion Include="Cachedral.Redis" Version="1.0.2" />
<PackageReference Include="Cachedral.Redis" />
paket add Cachedral.Redis --version 1.0.2
#r "nuget: Cachedral.Redis, 1.0.2"
#:package Cachedral.Redis@1.0.2
#addin nuget:?package=Cachedral.Redis&version=1.0.2
#tool nuget:?package=Cachedral.Redis&version=1.0.2
Cachedral
Cachedral is an application-cache library for .NET 8, .NET 9, and .NET 10. It caches method results and explicit keys inside your services: deterministic keys, expiration, tags, stampede protection, and either in-process memory or Redis.
| Package | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Cachedral |
✅ | ✅ | ✅ |
Cachedral.AspNetCore |
✅ | ✅ | ✅ |
Cachedral.Redis |
✅ | ✅ | ✅ |
Each package is multi-targeted (net8.0;net9.0;net10.0). A single package version contains assemblies for all three frameworks. When the application restores, NuGet/MSBuild selects lib/net8.0, lib/net9.0, or lib/net10.0 to match the project's target framework.
License: MIT.
What Cachedral is
Cachedral stores application data: repository results, computed DTOs, and values you put there with ICacheService. It is not HTTP response caching, output caching, session state, or an ORM second-level cache.
- Memory is the default provider (one process).
- Redis is optional (shared across instances).
- Method caching uses
[Cachedral]. - Method invalidation uses
[CachedralInvalidate].
There is no [Cache] or [Cacheable] attribute.
Packages
| Package | What you get |
|---|---|
Cachedral |
ICacheService, CacheOptions, [Cachedral], [CachedralInvalidate], memory provider, keys, stampede protection, serializer contract |
Cachedral.AspNetCore |
AddCachedral, method interception (Castle DynamicProxy) |
Cachedral.Redis |
Redis ICacheProvider and optional distributed stampede lease |
Cachedral.AspNetCore → Cachedral
Cachedral.Redis → Cachedral + StackExchange.Redis
Cachedral does not depend on ASP.NET Core or Redis. For Redis, install Cachedral.Redis; you do not need a separate StackExchange.Redis reference unless the rest of the application already uses it.
Typical ASP.NET Core application:
dotnet add package Cachedral.AspNetCore
That package depends on Cachedral. Add Redis when instances must share cache:
dotnet add package Cachedral.AspNetCore
dotnet add package Cachedral.Redis
A worker or console that only uses ICacheService (no [Cachedral] interception) can reference Cachedral alone, and Cachedral.Redis if Redis is required.
Supported frameworks
| net8.0 | net9.0 | net10.0 | |
|---|---|---|---|
| Library assemblies | included | included | included |
| Consuming app TFM | selected automatically | selected automatically | selected automatically |
Microsoft.Extensions.* dependencies are the 8.x line so a .NET 8 application can restore. A .NET 9 or .NET 10 application still loads the matching Cachedral assembly for that TFM.
Quick start (ASP.NET Core, memory)
using Cachedral;
using Cachedral.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.AddCachedral(options =>
{
options.KeyPrefix = "Cachedral:v1";
});
builder.Services.AddScoped<ProductService>();
builder.Services.AddControllers();
builder.Services.AddCachedralMethodInterception();
var app = builder.Build();
app.MapControllers();
app.Run();
public class ProductService
{
private readonly ProductRepository _repository;
public ProductService(ProductRepository repository) => _repository = repository;
[Cachedral(60)]
public virtual Task<Product?> GetProductAsync(int id) => _repository.GetProductAsync(id);
}
Behavior:
- First call
GetProductAsync(15)— miss. The repository runs. The result is stored for 60 seconds (absolute expiration). - Later calls with the same arguments before expiry — hit. The repository does not run.
- After 60 seconds — miss again.
- If the repository throws, nothing is stored. The next call runs the method again.
- A null result is not stored by default (
IgnoreNull = true). The next call hits the origin again. - Concurrent misses for the same key share one origin call when stampede protection is enabled (default).
Call AddCachedralMethodInterception() after the application services that use [Cachedral] / [CachedralInvalidate]. Cached methods on class registrations must be virtual, and the class must not be sealed.
Redis
Use Redis when more than one process must see the same cache.
using Cachedral.AspNetCore;
using Cachedral.Redis;
builder.AddCachedral(options =>
{
options.KeyPrefix = "Cachedral:v1";
});
builder.Services.AddCachedralRedis(
builder.Configuration.GetConnectionString("Redis"));
builder.Services.AddScoped<ProductService>();
builder.Services.AddCachedralMethodInterception();
AddCachedralRedis replaces the memory ICacheProvider. One IConnectionMultiplexer is registered as a singleton; connections are not opened per request.
Example connection string (adjust host, port, password, TLS):
localhost:6379
or
myredis.example.com:6380,password=...,ssl=true,abortConnect=false
Cachedral does not require Docker. Docker is only a convenient way to run a Redis server locally.
Configuration (CacheOptions)
builder.AddCachedral(options =>
{
options.KeyPrefix = "Cachedral:v1";
options.DefaultExpiration = TimeSpan.FromMinutes(5);
options.EnableStampedeProtection = true;
options.DefaultIgnoreNull = true;
options.FailurePolicy = CacheFailurePolicy.FailFast;
options.MaxKeyLength = 512;
options.StampedeWaitTimeout = TimeSpan.FromSeconds(30);
options.MemoryCacheSizeLimit = 10_000;
options.EnableDistributedStampedeProtection = true;
options.DistributedLockTimeout = TimeSpan.FromSeconds(10);
options.LogCacheKeys = false;
});
| Property | Default | Meaning |
|---|---|---|
KeyPrefix |
Cachedral:v1 |
Prepended to every logical key |
DefaultExpiration |
5 minutes | Used when [Cachedral] does not set duration |
EnableStampedeProtection |
true |
One in-process factory per key on concurrent misses |
DefaultIgnoreNull |
true |
Default for entries that do not set IgnoreNull |
FailurePolicy |
FailFast |
Provider errors throw (FailOpen treats them as miss / no-op) |
MaxKeyLength |
512 |
Longer keys get a hashed suffix |
StampedeWaitTimeout |
30 seconds | How long a waiter waits for the shared factory (does not cancel it) |
MemoryCacheSizeLimit |
null |
Optional IMemoryCache size limit (each entry size 1) |
EnableDistributedStampedeProtection |
true |
Redis lease around origin work when Redis is registered |
DistributedLockTimeout |
10 seconds | Lease duration |
LogCacheKeys |
false |
If true, Debug logs may include a short SHA-256 prefix of a key or tag |
Serializer and provider are registered in DI, not as properties on CacheOptions.
Method-level caching ([Cachedral])
Requires Cachedral.AspNetCore and a DI proxy (see Method interception below).
The interceptor stores T, not the Task / ValueTask wrapper.
[Cachedral(60)]
[Cachedral(300, Key = "product:{id}", Tags = ["products"])]
[Cachedral(DurationSeconds = 30, SlidingExpiration = true)]
[Cachedral] // uses CacheOptions.DefaultExpiration
| Member | Meaning |
|---|---|
Cachedral() |
Duration from CacheOptions.DefaultExpiration (DurationSeconds = -1) |
Cachedral(int durationSeconds) |
Absolute seconds (> 0) unless sliding is enabled |
DurationSeconds |
-1 = default; 0 is invalid |
Key |
Optional template, e.g. product:{id} (parameter names) |
SlidingExpiration |
When true, duration is a sliding window |
IgnoreNull |
Default true — null results are not stored |
Tags |
Tag index for RemoveByTagAsync |
Supported return types: Task<T>, Task<T?>, ValueTask<T>, ValueTask<T?>.
Not supported on [Cachedral]: Task, ValueTask, void, synchronous returns (InvalidOperationException).
Generic methods are supported. Automatic keys include generic arguments, so GetAsync<Product>(1) and GetAsync<Order>(1) do not share an entry.
CancellationToken parameters are omitted from automatic keys. A waiting caller’s token does not cancel shared origin work for other callers.
Exceptions are never cached.
Self-invocation (this.GetProductAsync(...) inside the same instance) is not intercepted. Call through the injected (proxied) service.
Interfaces: register AddCachedScoped<IProductService, ProductService>() (or singleton/transient) so an interface proxy is used. Methods on the interface that you cache should be the ones callers invoke.
Method interception
Cachedral.AspNetCore uses Castle DynamicProxy.
| Registration style | Requirement |
|---|---|
Concrete class (AddScoped<ProductService>()) |
Cached methods virtual; type not sealed; public constructor |
Interface (AddCachedScoped<IFoo, Foo>()) |
Callers must use IFoo |
Recommended sequence:
builder.AddCachedral();
builder.Services.AddScoped<ProductService>();
builder.Services.AddCachedralMethodInterception();
builder.AddCachedral()/builder.Services.AddCachedral()registers memory cache,ICacheService, and interceptor types.builder.Services.AddCachedral()does not wrap services registered after that call.AddCachedralMethodInterception()wraps already registered services that have[Cachedral]or[CachedralInvalidate].
builder.AddCachedral() also rewrites the collection when the host is built. If [Cachedral] still does not run, call AddCachedralMethodInterception() after those services are registered.
Generic host:
host.UseCachedral();
Helpers that register a proxy immediately:
AddCachedScoped<T>()/AddCachedSingleton<T>()/AddCachedTransient<T>()AddCachedScoped<TService, TImplementation>()and the singleton/transient overloads
More detail: docs/aspnet-core.md, docs/architecture/method-interception.md.
Cache keys
Logical keys are passed through ICacheKeyGenerator.Normalize, which prepends KeyPrefix unless it is already present.
product:15 is stored as Cachedral:v1:product:15 with the default prefix.
Automatic keys
When [Cachedral] has no Key, the generator includes declaring type (Type.FullName), method name, generic arguments, parameter types and names, and formatted values. CancellationToken is omitted.
Keys are deterministic. They do not use object.GetHashCode().
- Strings are JSON-quoted so
null,"", and"null"cannot collide. - Enums use invariant numeric form.
Guiduses formatD.DateTimeuses round-tripo;DateTimeOffsetis converted to UTCo.- Collections, dictionaries, and other objects are canonicalized, then hashed (SHA-256).
- Keys longer than
MaxKeyLength(default 512) keep a prefix fragment and append the full SHA-256 hex digest.
Custom templates
[Cachedral(300, Key = "product:{id}")]
public virtual Task<Product?> GetProductAsync(int id) => _repository.GetProductAsync(id);
Tokens are parameter names. Unknown names, empty {}, and unmatched { throw ArgumentException. They do not fall back to a shared key.
Invalidate with the same logical template ([CachedralInvalidate("product:{id}")]) or RemoveAsync("product:15"). If the method used an automatic key, invalidating product:{id} will not remove that entry.
Full formatting tables: docs/cache-keys.md.
Manual API (ICacheService)
ICacheService is a singleton. Inject it into scoped services as usual. Logical keys are normalized with KeyPrefix.
ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
ValueTask SetAsync<T>(string key, T value, CacheEntryOptions? options = null, CancellationToken cancellationToken = default);
ValueTask SetAsync<T>(string key, T value, TimeSpan expiration, CancellationToken cancellationToken = default);
ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);
ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default);
ValueTask<bool> ExistsAsync(string key, CancellationToken cancellationToken = default);
ValueTask<T?> GetOrCreateAsync<T>(
string key,
Func<CancellationToken, Task<T?>> factory,
CacheEntryOptions? options = null,
CancellationToken cancellationToken = default);
ValueTask<T?> GetOrCreateAsync<T>(
string key,
Func<CancellationToken, Task<T?>> factory,
TimeSpan expiration,
CancellationToken cancellationToken = default);
Missing keys: GetAsync returns default and does not throw. A cached null and a miss both look like default; use ExistsAsync when that distinction matters. RemoveByTagAsync throws ArgumentException for null, empty, or whitespace tags. Missing tags are not an error.
public class PriceService(ICacheService cache)
{
public ValueTask<decimal?> GetPriceAsync(int id, CancellationToken cancellationToken) =>
cache.GetOrCreateAsync(
$"price:{id}",
_ => LoadAsync(id),
TimeSpan.FromMinutes(5),
cancellationToken);
private static Task<decimal?> LoadAsync(int id) => Task.FromResult<decimal?>(12.5m);
}
GetOrCreateAsync
On miss, the factory runs and the result is stored (unless it is null and IgnoreNull is true). Factory exceptions and cancellation are not cached. Concurrent misses for the same key share one factory when stampede protection is enabled. The factory is not given the caller’s CancellationToken; that token only cancels this caller’s wait.
Expiration
[Cachedral(60)] is absolute 60 seconds from the write.
[Cachedral(DurationSeconds = 300, SlidingExpiration = true)]
await cache.SetAsync(key, value, CacheEntryOptions.Sliding(TimeSpan.FromMinutes(2)));
await cache.SetAsync(key, value, CacheEntryOptions.Absolute(TimeSpan.FromMinutes(10)));
| Provider | Absolute | Sliding |
|---|---|---|
| Memory | AbsoluteExpirationRelativeToNow |
SlidingExpiration |
| Redis | key TTL | TTL refreshed with EXPIRE on hit |
When [Cachedral] omits duration, CacheOptions.DefaultExpiration is used.
Stampede protection
A stampede is many concurrent misses for one key all hitting the origin.
In-process (EnableStampedeProtection, default true):
- Hits do not enter this path.
- Only one factory runs per key while others wait.
- Waiters receive the same result.
- Different keys run concurrently.
- One caller cancelling does not cancel the shared factory.
- Exceptions and cancellations are not stored.
- Per-key wait state is removed after completion.
Redis lease (EnableDistributedStampedeProtection, default true, requires Cachedral.Redis): a best-effort single-resource lease around origin execution. It is not Redlock and not a guarantee of exactly-once execution across a cluster.
StampedeWaitTimeout limits how long a waiter blocks; it does not abort the shared factory. DistributedLockTimeout is the Redis lease length. RedisCacheOptions.LockAcquireTimeout / LockRetryDelay control how long Redis retries acquiring the lease before running without it.
Invalidation and tags
Exact key:
await cache.RemoveAsync("product:15");
Missing keys are not an error.
Tags:
[Cachedral(300, Key = "product:{id}", Tags = ["products", "catalog"])]
await cache.RemoveByTagAsync("products");
CacheEntryOptions.Tags on SetAsync / GetOrCreateAsync works the same way. Empty and whitespace tags are ignored. Comparisons are ordinal (case-sensitive). Memory uses an in-memory index. Redis uses sets — not KEYS and not a full keyspace scan.
[CachedralInvalidate]
Cannot be combined with [Cachedral] on the same method. Multiple invalidate attributes on one method are allowed.
[CachedralInvalidate("product:{id}")]
public virtual Task UpdateProductAsync(int id) => _repository.UpdateAsync(id);
[CachedralInvalidate(Tag = "products")]
public virtual Task InvalidateCatalogAsync() => Task.CompletedTask;
| Member | Meaning |
|---|---|
constructor (string key) |
Key template (prefix applied like [Cachedral]) |
Key |
Key template |
Tag |
One tag |
Tags |
Additional tags |
Timing |
Default CacheInvalidationTiming.AfterSuccess |
With AfterSuccess, if the method throws or is cancelled, keys stay. CacheInvalidationTiming.BeforeInvocation invalidates before the method runs.
Serialization
ICacheSerializer: Serialize<T>, TryDeserialize<T>. Default implementation: SystemTextJsonCacheSerializer (System.Text.Json).
- Memory stores CLR objects. The serializer is not used.
- Redis stores bytes. There is no polymorphic
$type/ type-name handling. Corrupt payloads are treated as a miss (deleted whenRedisCacheOptions.DeleteCorruptValuesistrue, default).
Replace the serializer:
services.AddCachedralSerializer<MySerializer>();
MySerializer must implement ICacheSerializer. Implementations are synchronous and CPU-bound; do not wrap them in Task.Run. Deserialize only to the requested T.
Failure policy
Default: CacheFailurePolicy.FailFast — Redis down, serialization failures, and similar provider errors propagate.
CacheFailurePolicy.FailOpen: reads behave as misses, writes are skipped, factories and intercepted methods still run. Watch cachedral.errors.
options.FailurePolicy = CacheFailurePolicy.FailOpen;
Null values
Default IgnoreNull / DefaultIgnoreNull is true: null factory or method results are not stored. Set IgnoreNull = false on [Cachedral] or CacheEntryOptions to cache null (negative caching). GetAsync still returns default for both miss and cached null; use ExistsAsync to tell them apart.
Exceptions
Exceptions from factories and intercepted methods are never written to the cache. With FailFast, provider errors throw. With FailOpen, they are logged and treated as miss / no-op. After a failed shared factory, the next miss may run the origin again.
Cancellation
GetAsync, SetAsync, RemoveAsync, and ExistsAsync pass CancellationToken to the provider. For GetOrCreateAsync, the token cancels waiting, not the shared factory. Intercepted origin work is started so that cancelling one waiter does not abort in-flight work for others.
Logging
Hits, misses, and sets are logged at Debug, not Information. Values, payloads, secrets, connection strings, and full method arguments are not logged. When LogCacheKeys is true, Debug logs may include a short SHA-256 prefix of a key or tag.
Metrics
Meter and activity source name: Cachedral (CachedralInstrumentation).
Instruments include cachedral.hits, cachedral.misses, cachedral.errors, cachedral.stampede.waits, cachedral.factory.duration, get/set/remove durations, serialization errors, and lock counters. Some instruments use a provider dimension (memory, redis, other). Cache keys and user ids are not used as metric tags.
DI and lifetimes
| Service | Lifetime |
|---|---|
ICacheService, ICacheProvider, ICacheKeyGenerator, ICacheSerializer, stampede protector, interceptor |
Singleton |
| Your application services | Unchanged (scoped stays scoped) |
Redis options (RedisCacheOptions)
Passed to AddCachedralRedis(connectionString, configure):
| Property | Default | Meaning |
|---|---|---|
ConnectionString |
argument | StackExchange.Redis configuration string (not logged) |
Database |
-1 |
Connection default |
LockAcquireTimeout |
2 seconds | Retry window when acquiring a stampede lease |
LockRetryDelay |
40 ms | Delay between lease attempts |
DeleteCorruptValues |
true |
Delete unreadable payloads |
Prefer TLS in production. Do not put passwords in source control.
AOT and trimming
Method interception uses runtime proxies and is not Native AOT compatible. Do not enable PublishAot on applications that rely on [Cachedral] interception.
| Area | Status |
|---|---|
ICacheService + memory, no interception |
More trim-friendly; not claimed as a complete Native AOT product |
| Redis + default reflection JSON | Not AOT-safe for arbitrary T unless a trim-safe ICacheSerializer is supplied |
| Method interception | Not AOT compatible |
Production notes
- Use stable key templates for data you will invalidate.
- Always set a finite expiration. The cache is not the source of truth.
- Prefer few, coarse tags. A unique tag per user can grow without bound.
- Memory for a single instance; Redis for multiple instances.
- Do not put unbounded user input into keys without hashing or length limits.
- Set
MemoryCacheSizeLimitwhen process memory matters. - Invalidate with the same logical key that was used to store.
- Monitor
cachedral.errorsand Redis connectivity. - Do not cache secrets in a shared Redis if that store is not isolated.
- Keep stampede protection enabled for expensive origin calls.
- Default failure policy is FailFast. Switch to FailOpen only if the application must keep serving when cache infrastructure fails.
Common problems
| Symptom | Typical cause |
|---|---|
| Method always hits the origin | Missing AddCachedralMethodInterception() after the service, non-virtual method, sealed class, or self-invocation |
| Invalidate does nothing | Template does not match the stored logical key (automatic key vs product:{id}) |
| Memory works, other instances do not | Redis not registered (AddCachedralRedis) |
| Provider exceptions in production | Default FailFast; Redis unavailable |
| Unexpected origin load on Redis | Distributed lease is best-effort, not exactly-once |
| Native AOT publish fails | Castle interception |
Sample
This repository includes samples/Cachedral.Sample: a Product catalog Web API. Memory is used when ConnectionStrings:Redis is empty; Redis is used when that string is set.
dotnet run --project samples/Cachedral.Sample --framework net8.0
dotnet run --project samples/Cachedral.Sample --framework net9.0
dotnet run --project samples/Cachedral.Sample --framework net10.0
See samples/Cachedral.Sample/README.md.
Further documentation
| Document | Topic |
|---|---|
| docs/aspnet-core.md | DI, interception, lifetimes |
| docs/configuration.md | Options and failure policy |
| docs/cache-keys.md | Key generation |
| docs/stampede-protection.md | In-process and Redis lease |
| docs/invalidation.md | Keys, tags, [CachedralInvalidate] |
| docs/serialization.md | ICacheSerializer |
| docs/redis.md | Redis provider |
| docs/observability.md | Logging and metrics |
| docs/troubleshooting.md | Misses, DI, Redis, expiration |
| docs/architecture/overview.md | Design |
License
MIT. See LICENSE.
Cachedral — Azərbaycan dilində
Cachedral .NET 8, .NET 9 və .NET 10 üçün tətbiq keşi kitabxanasıdır. Servislərin içində metod nəticələrini və açıq açarları saxlayır: deterministik açarlar, expiration, teqlər, stampede qoruması, prosesdaxili Memory və ya Redis.
| Paket | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Cachedral |
✅ | ✅ | ✅ |
Cachedral.AspNetCore |
✅ | ✅ | ✅ |
Cachedral.Redis |
✅ | ✅ | ✅ |
Hər paket çoxhədəflidir (net8.0;net9.0;net10.0). Eyni paket versiyasında hər üç framework üçün assembly var. Restore zamanı NuGet/MSBuild tətbiqin TFM-inə uyğun lib/net8.0, lib/net9.0 və ya lib/net10.0 seçir.
Lisenziya: MIT.
Cachedral nədir
Cachedral tətbiq məlumatını saxlayır: repository nəticələri, DTO-lar, ICacheService ilə yazdığınız dəyərlər. HTTP response cache, output cache, session və ya ORM ikinci səviyyə keşi deyil.
- Memory susmaya görə provayderdir (bir proses).
- Redis istəyə bağlıdır (instansiyalar arasında paylaşılan keş).
- Metod keşi:
[Cachedral]. - Metod invalidasiyası:
[CachedralInvalidate].
[Cache] və [Cacheable] atributu yoxdur.
Paketlər
| Paket | Nə verir |
|---|---|
Cachedral |
ICacheService, CacheOptions, [Cachedral], [CachedralInvalidate], memory provayder, açarlar, stampede qoruması, serializer müqaviləsi |
Cachedral.AspNetCore |
AddCachedral, metod interception (Castle DynamicProxy) |
Cachedral.Redis |
Redis ICacheProvider və istəyə bağlı paylanmış stampede lease |
Cachedral.AspNetCore → Cachedral
Cachedral.Redis → Cachedral + StackExchange.Redis
Cachedral ASP.NET Core və Redis-dən asılı deyil. Redis üçün Cachedral.Redis kifayətdir; tətbiqin başqa yerdə StackExchange.Redis-ə ehtiyacı yoxdursa ayrıca əlavə etməyə ehtiyac yoxdur.
Tipik ASP.NET Core tətbiqi:
dotnet add package Cachedral.AspNetCore
Bu paket Cachedral-i asılılıq kimi gətirir. Instansiyalar keşi paylaşmalıdırsa:
dotnet add package Cachedral.AspNetCore
dotnet add package Cachedral.Redis
Yalnız ICacheService istifadə edən (interception olmayan) worker və ya console Cachedral götürə bilər; Redis lazımdırsa Cachedral.Redis də əlavə olunur.
Dəstəklənən framework-lər
| net8.0 | net9.0 | net10.0 | |
|---|---|---|---|
| Kitabxana assembly-ləri | daxildir | daxildir | daxildir |
| Tətbiqin TFM-i | avtomatik seçilir | avtomatik seçilir | avtomatik seçilir |
Microsoft.Extensions.* asılılıqları 8.x xəttindədir ki, .NET 8 tətbiqi restore edə bilsin. .NET 9 və .NET 10 tətbiqləri həmin TFM-ə uyğun Cachedral assembly-sini yükləyir.
Tez başlanğıc (ASP.NET Core, Memory)
using Cachedral;
using Cachedral.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.AddCachedral(options =>
{
options.KeyPrefix = "Cachedral:v1";
});
builder.Services.AddScoped<ProductService>();
builder.Services.AddControllers();
builder.Services.AddCachedralMethodInterception();
var app = builder.Build();
app.MapControllers();
app.Run();
public class ProductService
{
private readonly ProductRepository _repository;
public ProductService(ProductRepository repository) => _repository = repository;
[Cachedral(60)]
public virtual Task<Product?> GetProductAsync(int id) => _repository.GetProductAsync(id);
}
Davranış:
- İlk çağırış
GetProductAsync(15)— miss. Repository işləyir. Nəticə 60 saniyə (absolute) saxlanılır. - Müddət bitməmiş eyni arqumentlərlə sonrakı çağırışlar — hit. Repository çağırılmır.
- 60 saniyədən sonra yenidən miss.
- Repository istisna atarsa, heç nə yazılmır; növbəti çağırış metodu yenidən işlədir.
- Null nəticə susmaya görə saxlanılmır (
IgnoreNull = true). Növbəti çağırış origin-ə gedir. - Eyni açar üçün paralel miss-lər stampede qoruması açıq olanda bir origin çağırışını paylaşır.
AddCachedralMethodInterception()-i [Cachedral] / [CachedralInvalidate] istifadə edən servislərdən sonra çağırın. Sinif qeydiyyatında keşlənən metodlar virtual olmalıdır; sinif sealed olmamalıdır.
Redis
Birdən çox proses eyni keşi görməlidirsə Redis istifadə edin.
using Cachedral.AspNetCore;
using Cachedral.Redis;
builder.AddCachedral(options =>
{
options.KeyPrefix = "Cachedral:v1";
});
builder.Services.AddCachedralRedis(
builder.Configuration.GetConnectionString("Redis"));
builder.Services.AddScoped<ProductService>();
builder.Services.AddCachedralMethodInterception();
AddCachedralRedis memory ICacheProvider-i əvəz edir. IConnectionMultiplexer singleton-dur; hər sorğuda yeni bağlantı açılmır.
Nümunə connection string (host, port, parol, TLS öz mühitinizə uyğunlaşdırın):
localhost:6379
və ya
myredis.example.com:6380,password=...,ssl=true,abortConnect=false
Cachedral-in işləməsi üçün Docker vacib deyil. Docker yalnız lokal Redis serverini qaldırmaq üçündür.
Konfiqurasiya (CacheOptions)
builder.AddCachedral(options =>
{
options.KeyPrefix = "Cachedral:v1";
options.DefaultExpiration = TimeSpan.FromMinutes(5);
options.EnableStampedeProtection = true;
options.DefaultIgnoreNull = true;
options.FailurePolicy = CacheFailurePolicy.FailFast;
options.MaxKeyLength = 512;
options.StampedeWaitTimeout = TimeSpan.FromSeconds(30);
options.MemoryCacheSizeLimit = 10_000;
options.EnableDistributedStampedeProtection = true;
options.DistributedLockTimeout = TimeSpan.FromSeconds(10);
options.LogCacheKeys = false;
});
| Xüsusiyyət | Standart | Mənası |
|---|---|---|
KeyPrefix |
Cachedral:v1 |
Hər məntiqi açarın əvvəlinə əlavə olunur |
DefaultExpiration |
5 dəqiqə | [Cachedral] müddət verməyəndə |
EnableStampedeProtection |
true |
Eyni açar üçün prosesdaxili bir factory |
DefaultIgnoreNull |
true |
IgnoreNull təyin olunmayan yazılar üçün |
FailurePolicy |
FailFast |
Provayder xətası atılır (FailOpen miss / no-op kimi davam edir) |
MaxKeyLength |
512 |
Daha uzun açarlar hash suffix alır |
StampedeWaitTimeout |
30 saniyə | Gözləyənin paylaşılan factory-ni gözləmə müddəti (factory ləğv olunmur) |
MemoryCacheSizeLimit |
null |
İstəyə bağlı IMemoryCache ölçü limiti (hər yazı ölçüsü 1) |
EnableDistributedStampedeProtection |
true |
Redis qeyd olunanda origin ətrafında lease |
DistributedLockTimeout |
10 saniyə | Lease müddəti |
LogCacheKeys |
false |
true olanda Debug-da açarın qısa SHA-256 prefiksi ola bilər |
Serializer və provayder CacheOptions xüsusiyyəti deyil; DI-də qeyd olunur.
Metod səviyyəsində keş ([Cachedral])
Cachedral.AspNetCore və DI proxy tələb olunur (aşağıda Metod interception).
Interceptor T saxlayır, Task / ValueTask örtüyünü yox.
[Cachedral(60)]
[Cachedral(300, Key = "product:{id}", Tags = ["products"])]
[Cachedral(DurationSeconds = 30, SlidingExpiration = true)]
[Cachedral] // CacheOptions.DefaultExpiration
| Üzv | Mənası |
|---|---|
Cachedral() |
Müddət CacheOptions.DefaultExpiration-dandır (DurationSeconds = -1) |
Cachedral(int durationSeconds) |
Saniyə (> 0); sliding yoxdursa absolute |
DurationSeconds |
-1 = standart; 0 etibarsızdır |
Key |
Şablon, məsələn product:{id} (parametr adları) |
SlidingExpiration |
true olanda müddət pəncərədir |
IgnoreNull |
Standart true — null saxlanılmır |
Tags |
RemoveByTagAsync üçün |
Dəstəklənən qayıdış tipləri: Task<T>, Task<T?>, ValueTask<T>, ValueTask<T?>.
[Cachedral] üçün dəstəklənmir: Task, ValueTask, void, sinxron qayıdış (InvalidOperationException).
Generic metodlar dəstəklənir. Avtomatik açara generic arqumentlər daxildir; GetAsync<Product>(1) və GetAsync<Order>(1) eyni yazını paylaşmır.
CancellationToken parametrləri avtomatik açardan çıxarılır. Gözləyən çağırışın token-i başqaları üçün paylaşılan origin işini ləğv etmir.
İstisnalar heç vaxt keşlənmir.
Self-invocation (this.GetProductAsync(...) eyni instansiyanın içində) interception olunmur. Çağırışı inject olunmuş (proxy) servis üzərindən edin.
İnterfeyslər: AddCachedScoped<IProductService, ProductService>() (və ya singleton/transient) ilə interface proxy qeyd olunur. Keşləmək istədiyiniz metodlar çağırışın getdiyi interfeysdə olmalıdır.
Metod interception
Cachedral.AspNetCore Castle DynamicProxy istifadə edir.
| Qeydiyyat | Tələb |
|---|---|
Konkret sinif (AddScoped<ProductService>()) |
Keşlənən metodlar virtual; tip sealed deyil; ictimai konstruktor |
İnterfeys (AddCachedScoped<IFoo, Foo>()) |
Çağırışlar IFoo üzərindən olmalıdır |
Tövsiyə olunan ardıcıllıq:
builder.AddCachedral();
builder.Services.AddScoped<ProductService>();
builder.Services.AddCachedralMethodInterception();
builder.AddCachedral()/builder.Services.AddCachedral()memory keşi,ICacheServicevə interceptor tiplərini qeyd edir.- Təkcə
builder.Services.AddCachedral()sonra qeyd olunan servisləri sarmır. AddCachedralMethodInterception()artıq qeyd olunmuş,[Cachedral]və ya[CachedralInvalidate]olan servisləri sarmalayır.
builder.AddCachedral() host qurulanda da kolleksiyanı yenidən yaza bilər. [Cachedral] yenə də işləmirsə, həmin servislər qeyd olunduqdan sonra AddCachedralMethodInterception() çağırın.
Generic host:
host.UseCachedral();
Proxy-ni dərhal qeyd edən köməkçilər:
AddCachedScoped<T>()/AddCachedSingleton<T>()/AddCachedTransient<T>()AddCachedScoped<TService, TImplementation>()və singleton/transient overload-ları
Ətraflı: docs/aspnet-core.md, docs/architecture/method-interception.md.
Keş açarları
Məntiqi açarlar ICacheKeyGenerator.Normalize-dən keçir; KeyPrefix artıq yoxdursa əlavə olunur.
Standart prefikslə product:15 saxlanılır: Cachedral:v1:product:15.
Avtomatik açarlar
[Cachedral]-də Key yoxdursa generator elan edən tipi (Type.FullName), metod adını, generic arqumentləri, parametr tiplərini, adlarını və formatlanmış dəyərləri daxil edir. CancellationToken çıxarılır.
Açarlar deterministikdir. object.GetHashCode() istifadə olunmur.
- Sətirlər JSON-quoted-dir;
null,""və"null"toqquşmur. - Enum-lar invariant rəqəm formasındadır.
GuidformatıD-dir.DateTimeround-tripo;DateTimeOffsetUTCo.- Kolleksiyalar, lüğətlər və digər obyektlər kanonikləşdirilir, sonra SHA-256 ilə hash olunur.
MaxKeyLength-dən (standart 512) uzun açarların sonuna tam SHA-256 hex əlavə olunur.
Xüsusi şablonlar
[Cachedral(300, Key = "product:{id}")]
public virtual Task<Product?> GetProductAsync(int id) => _repository.GetProductAsync(id);
Token-lər parametr adlarıdır. Naməlum ad, boş {} və uyğunsuz { ArgumentException atır; ümumi açara düşmür.
Invalidasiyanı eyni məntiqi şablonla edin ([CachedralInvalidate("product:{id}")]) və ya RemoveAsync("product:15"). Metod avtomatik açar istifadə edibsə, product:{id} silmək o yazını silməz.
Tam cədvəllər: docs/cache-keys.md.
Əl API (ICacheService)
ICacheService singleton-dur. Scoped servisə inject etmək olar. Məntiqi açarlar KeyPrefix ilə normallaşır.
ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
ValueTask SetAsync<T>(string key, T value, CacheEntryOptions? options = null, CancellationToken cancellationToken = default);
ValueTask SetAsync<T>(string key, T value, TimeSpan expiration, CancellationToken cancellationToken = default);
ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);
ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default);
ValueTask<bool> ExistsAsync(string key, CancellationToken cancellationToken = default);
ValueTask<T?> GetOrCreateAsync<T>(
string key,
Func<CancellationToken, Task<T?>> factory,
CacheEntryOptions? options = null,
CancellationToken cancellationToken = default);
ValueTask<T?> GetOrCreateAsync<T>(
string key,
Func<CancellationToken, Task<T?>> factory,
TimeSpan expiration,
CancellationToken cancellationToken = default);
Çatışmayan açar: GetAsync default qaytarır, atış yoxdur. Keşlənmiş null və miss hər ikisi default ola bilər; fərq lazımdırsa ExistsAsync istifadə edin. RemoveByTagAsync null, boş və ya yalnız boşluq olan teq üçün ArgumentException atır. Mövcud olmayan teq xəta deyil.
public class PriceService(ICacheService cache)
{
public ValueTask<decimal?> GetPriceAsync(int id, CancellationToken cancellationToken) =>
cache.GetOrCreateAsync(
$"price:{id}",
_ => LoadAsync(id),
TimeSpan.FromMinutes(5),
cancellationToken);
private static Task<decimal?> LoadAsync(int id) => Task.FromResult<decimal?>(12.5m);
}
GetOrCreateAsync
Miss-də factory işləyir və nəticə yazılır (null və IgnoreNull = true istisna). Factory istisnası və ləğv keşlənmir. Eyni açar üçün paralel miss-lər stampede qoruması açıq olanda bir factory paylaşır. Factory-yə çağıranın CancellationToken-i verilmir; token yalnız bu çağırışın gözləməsini ləğv edir.
Expiration
[Cachedral(60)] yazı anından absolute 60 saniyədir.
[Cachedral(DurationSeconds = 300, SlidingExpiration = true)]
await cache.SetAsync(key, value, CacheEntryOptions.Sliding(TimeSpan.FromMinutes(2)));
await cache.SetAsync(key, value, CacheEntryOptions.Absolute(TimeSpan.FromMinutes(10)));
| Provayder | Absolute | Sliding |
|---|---|---|
| Memory | AbsoluteExpirationRelativeToNow |
SlidingExpiration |
| Redis | açarın TTL-i | hit-də EXPIRE ilə TTL yenilənir |
[Cachedral] müddət verməyəndə CacheOptions.DefaultExpiration işləyir.
Stampede qoruması
Stampede: eyni açar üçün eyni anda çox miss-in hamısının origin-ə getməsi.
Prosesdaxili (EnableStampedeProtection, standart true):
- Hit bu yola girmir.
- Eyni anda yalnız bir factory işləyir, digərləri gözləyir.
- Gözləyənlər eyni nəticəni alır.
- Fərqli açarlar paralel işləyir.
- Bir çağırışın ləğvi paylaşılan factory-ni dayandırmır.
- İstisna və ləğv keşə yazılmır.
- Açar üzrə gözləmə vəziyyəti bitəndə silinir.
Redis lease (EnableDistributedStampedeProtection, standart true, Cachedral.Redis lazımdır): origin ətrafında best-effort tək-resurs lease. Redlock deyil və klasterdə exactly-once zəmanəti deyil.
StampedeWaitTimeout gözləməni məhdudlaşdırır, paylaşılan factory-ni dayandırmır. DistributedLockTimeout Redis lease müddətidir. RedisCacheOptions.LockAcquireTimeout / LockRetryDelay lease alma cəhdlərinin nə qədər davam etdiyini təyin edir; vaxt bitəndə factory lease olmadan da işləyə bilər.
Invalidasiya və teqlər
Dəqiq açar:
await cache.RemoveAsync("product:15");
Mövcud olmayan açar xəta deyil.
Teqlər:
[Cachedral(300, Key = "product:{id}", Tags = ["products", "catalog"])]
await cache.RemoveByTagAsync("products");
SetAsync / GetOrCreateAsync üzərində CacheEntryOptions.Tags eyni işi görür. Boş və yalnız boşluq olan teqlər nəzərə alınmır. Müqayisə ordinal-dır (böyük/kiçik hərf fərqlidir). Memory in-memory indeks istifadə edir. Redis set istifadə edir — KEYS və tam keyspace scan yoxdur.
[CachedralInvalidate]
Eyni metodda [Cachedral] ilə birləşdirilə bilməz. Bir metodda bir neçə invalidate atributu ola bilər.
[CachedralInvalidate("product:{id}")]
public virtual Task UpdateProductAsync(int id) => _repository.UpdateAsync(id);
[CachedralInvalidate(Tag = "products")]
public virtual Task InvalidateCatalogAsync() => Task.CompletedTask;
| Üzv | Mənası |
|---|---|
konstruktor (string key) |
Açar şablonu ([Cachedral] kimi prefiks tətbiq olunur) |
Key |
Açar şablonu |
Tag |
Bir teq |
Tags |
Əlavə teqlər |
Timing |
Standart CacheInvalidationTiming.AfterSuccess |
AfterSuccess olanda metod atarsa və ya ləğv olunarsa, açarlar qalır. CacheInvalidationTiming.BeforeInvocation metod işləməmişdən əvvəl silir.
Serializasiya
ICacheSerializer: Serialize<T>, TryDeserialize<T>. Standart: SystemTextJsonCacheSerializer (System.Text.Json).
- Memory CLR obyektini birbaşa saxlayır. Serializer işləmir.
- Redis bayt saxlayır. Polimorfik
$type/ tip adı yoxdur. Korlanmış payload miss sayılır (RedisCacheOptions.DeleteCorruptValuesstandarttrueolanda silinir).
Serializer-i dəyişmək:
services.AddCachedralSerializer<MySerializer>();
MySerializer ICacheSerializer implementasiya etməlidir. Implementasiya sinxron və CPU-bound-dur; Task.Run ilə sarmayın. Yalnız tələb olunan T-yə deserialize edin.
Failure policy
Standart: CacheFailurePolicy.FailFast — Redis əlçatan olmayanda, serializasiya xətasında və oxşar provayder xətalarında istisna yayılır.
CacheFailurePolicy.FailOpen: oxu miss kimi, yazı atlanır, factory və intercepted metodlar işləməyə davam edir. cachedral.errors izləyin.
options.FailurePolicy = CacheFailurePolicy.FailOpen;
Null dəyərlər
Standart IgnoreNull / DefaultIgnoreNull true-dur: null factory və ya metod nəticəsi saxlanılmır. Mənfi keş üçün [Cachedral] və ya CacheEntryOptions üzərində IgnoreNull = false qoyun. GetAsync həm miss, həm keşlənmiş null üçün default qaytara bilər; fərq üçün ExistsAsync istifadə edin.
İstisnalar
Factory və intercepted metod istisnaları keşə yazılmır. FailFast-də provayder xətası atılır. FailOpen-də loglanır və miss / no-op kimi davam edir. Uğursuz paylaşılan factory-dən sonra növbəti miss origin-i yenidən işlədə bilər.
Ləğv (cancellation)
GetAsync, SetAsync, RemoveAsync, ExistsAsync token-i provayderə ötürür. GetOrCreateAsync-də token gözləməni ləğv edir, paylaşılan factory-ni yox. Intercepted origin işi elə başlanır ki, bir gözləyənin ləğvi başqalarının işini kəsməsin.
Loglama
Hit, miss və set Debug səviyyəsində yazılır, Information-da yox. Dəyərlər, payload, sirlər, connection string və tam metod arqumentləri log olunmur. LogCacheKeys = true olanda Debug-da açar və ya teqin qısa SHA-256 prefiksi ola bilər.
Metriklər
Meter və activity source adı: Cachedral (CachedralInstrumentation).
Alətlər: cachedral.hits, cachedral.misses, cachedral.errors, cachedral.stampede.waits, cachedral.factory.duration, get/set/remove müddətləri, serializasiya xətaları, lock sayğacları. Bəzi alətlərdə provider ölçüsü var (memory, redis, other). Keş açarı və istifadəçi id-si metric teqi kimi istifadə olunmur.
DI və lifetime
| Servis | Lifetime |
|---|---|
ICacheService, ICacheProvider, ICacheKeyGenerator, ICacheSerializer, stampede protector, interceptor |
Singleton |
| Tətbiq servisləri | Dəyişmir (scoped qalır scoped) |
Redis seçimləri (RedisCacheOptions)
AddCachedralRedis(connectionString, configure):
| Xüsusiyyət | Standart | Mənası |
|---|---|---|
ConnectionString |
arqument | StackExchange.Redis konfiqurasiya sətri (log olunmur) |
Database |
-1 |
Bağlantının standart bazası |
LockAcquireTimeout |
2 saniyə | Stampede lease alma cəhdlərinin pəncərəsi |
LockRetryDelay |
40 ms | Cəhdlər arası gözləmə |
DeleteCorruptValues |
true |
Oxunmayan payload silinir |
İstehsalda TLS üstünlük verin. Parolları mənbə koduna yazmayın.
AOT və trimming
Metod interception runtime proxy istifadə edir və Native AOT ilə uyğun deyil. [Cachedral] interception-ə bel bağlayan tətbiqlərdə PublishAot yandırmayın.
| Sahə | Vəziyyət |
|---|---|
ICacheService + memory, interception yox |
Daha trim-dostudur; tam Native AOT məhsulu kimi iddia olunmur |
| Redis + standart reflection JSON | İxtiyari T üçün AOT-safe deyil; trim-safe ICacheSerializer vermək olar |
| Metod interception | AOT uyğun deyil |
İstehsal qeydləri
- Invalidasiya edəcəyiniz məlumat üçün sabit açar şablonları istifadə edin.
- Həmişə sonlu expiration qoyun. Keş həqiqət mənbəyi deyil.
- Az və qaba teqlər seçin. Hər istifadəçiyə unikal teq limitsiz böyüyə bilər.
- Tək instansiya: Memory; çox instansiya: Redis.
- Limitsiz istifadəçi mətnini açara qoymaqdan çəkinin.
- Proses yaddaşı vacibdirsə
MemoryCacheSizeLimittəyin edin. - Saxladığınız eyni məntiqi açarla invalidasiya edin.
cachedral.errorsvə Redis bağlantısını izləyin.- İzolyasiya olunmayan paylaşılan Redis-də sirləri keşləməyin.
- Bahalı origin üçün stampede qorumasını açıq saxlayın.
- Standart failure policy FailFast-dir. Keş infrastrukturu düşəndə tətbiq işləməyə davam etməlidirsə FailOpen seçin.
Tez-tez rast gəlinən problemlər
| Əlamət | Tipik səbəb |
|---|---|
| Metod həmişə origin-ə gedir | Servisdən sonra AddCachedralMethodInterception() yoxdur, metod virtual deyil, sinif sealed-dir və ya self-invocation |
| Invalidasiya heç nə silmir | Şablon saxlanan məntiqi açarla uyğun gəlmir (avtomatik açar və product:{id}) |
| Memory işləyir, digər instansiyalar yox | AddCachedralRedis çağırılmayıb |
| İstehsalda provayder istisnaları | Standart FailFast; Redis əlçatan deyil |
| Redis-də gözlənilməz origin yükü | Paylanmış lease best-effort-dur, exactly-once deyil |
| Native AOT publish uğursuz olur | Castle interception |
Nümunə
Repozitoriyada samples/Cachedral.Sample var: Product kataloqu Web API. ConnectionStrings:Redis boşdursa Memory, doludursa Redis işləyir.
dotnet run --project samples/Cachedral.Sample --framework net8.0
dotnet run --project samples/Cachedral.Sample --framework net9.0
dotnet run --project samples/Cachedral.Sample --framework net10.0
Baxın: samples/Cachedral.Sample/README.md.
Əlavə sənədlər
| Sənəd | Mövzu |
|---|---|
| docs/aspnet-core.md | DI, interception, lifetime |
| docs/configuration.md | Seçimlər və failure policy |
| docs/cache-keys.md | Açar generasiyası |
| docs/stampede-protection.md | Prosesdaxili və Redis lease |
| docs/invalidation.md | Açarlar, teqlər, [CachedralInvalidate] |
| docs/serialization.md | ICacheSerializer |
| docs/redis.md | Redis provayderi |
| docs/observability.md | Log və metriklər |
| docs/troubleshooting.md | Miss, DI, Redis, expiration |
| docs/architecture/overview.md | Dizayn |
Lisenziya
MIT. Baxın: LICENSE.
| Product | Versions 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 is compatible. 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 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
- Cachedral (>= 1.0.2)
- StackExchange.Redis (>= 2.9.32)
-
net8.0
- Cachedral (>= 1.0.2)
- StackExchange.Redis (>= 2.9.32)
-
net9.0
- Cachedral (>= 1.0.2)
- StackExchange.Redis (>= 2.9.32)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.