Cachedral.Redis 1.0.2

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

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:

  1. First call GetProductAsync(15)miss. The repository runs. The result is stored for 60 seconds (absolute expiration).
  2. Later calls with the same arguments before expiry — hit. The repository does not run.
  3. After 60 seconds — miss again.
  4. If the repository throws, nothing is stored. The next call runs the method again.
  5. A null result is not stored by default (IgnoreNull = true). The next call hits the origin again.
  6. 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.
  • Guid uses format D.
  • DateTime uses round-trip o; DateTimeOffset is converted to UTC o.
  • 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 when RedisCacheOptions.DeleteCorruptValues is true, 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 MemoryCacheSizeLimit when process memory matters.
  • Invalidate with the same logical key that was used to store.
  • Monitor cachedral.errors and 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.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][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ış:

  1. İlk çağırış GetProductAsync(15)miss. Repository işləyir. Nəticə 60 saniyə (absolute) saxlanılır.
  2. Müddət bitməmiş eyni arqumentlərlə sonrakı çağırışlar — hit. Repository çağırılmır.
  3. 60 saniyədən sonra yenidən miss.
  4. Repository istisna atarsa, heç nə yazılmır; növbəti çağırış metodu yenidən işlədir.
  5. Null nəticə susmaya görə saxlanılmır (IgnoreNull = true). Növbəti çağırış origin-ə gedir.
  6. 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)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, ICacheService və 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, """null" toqquşmur.
  • Enum-lar invariant rəqəm formasındadır.
  • Guid formatı D-dir.
  • DateTime round-trip o; DateTimeOffset UTC o.
  • 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.DeleteCorruptValues standart true olanda 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ə MemoryCacheSizeLimit təyin edin.
  • Saxladığınız eyni məntiqi açarla invalidasiya edin.
  • cachedral.errors və 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 83 9/3/2026
1.0.1 87 9/2/2026
1.0.0 82 9/2/2026