Skip to main content

IHybridRequestCache

Uygulama katmanının cache arayüzü. HybridCache’i doğrudan inject etmek yerine bu interface kullanılır — multi-instance broadcast hook’u ve domain-friendly imza için. Tanım: src/BuildingBlocks/BuildingBlocks.Caching/IHybridRequestCache.cs.
public interface IHybridRequestCache
{
    // Cache'te varsa döner, yoksa factory'yi çağırıp doldurur (stampede-safe).
    ValueTask<T> GetOrCreateAsync<T>(
        string key,
        Func<CancellationToken, ValueTask<T>> factory,
        HybridCacheEntryOptions? options = null,
        IReadOnlyCollection<string>? tags = null,
        CancellationToken cancellationToken = default);

    // Tek key'i hem L1 hem L2'den siler (diğer instance'ların L1'ini ETKİLEMEZ).
    ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);

    // Tag'e bağlı tüm entry'leri invalidate eder + broadcast (BroadcastTagInvalidation açıksa).
    ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default);
    ValueTask RemoveByTagsAsync(IReadOnlyCollection<string> tags, CancellationToken cancellationToken = default);

    // Broadcast'ten gelen istekleri uygular — sadece local, tekrar broadcast YAPMAZ (loop önleme).
    ValueTask RemoveByTagLocallyAsync(string tag, CancellationToken cancellationToken = default);
}
RemoveByTagAsync vs RemoveByTagLocallyAsync farkı için: Invalidation ve Multi-Instance.

Stampede koruması

HybridCache.GetOrCreateAsync aynı key için paralel gelen istekleri tek factory çağrısında birleştirir: cache miss anında ilk istek factory’yi (DB sorgusu) çalıştırır, diğerleri sonucu bekler. Bu sayede cold cache + yüksek trafik durumunda DB’ye yüzlerce eşzamanlı sorgu gitmez. Davranış HybridCache infrastructure’ından gelir; ek kod gerektirmez.

Kayıt sırası KRİTİK

HybridCache, L2 backend’ini (IDistributedCache, Redis) kayıt anında keşfeder. Bu yüzden Redis önce register edilmiş olmalı:
// Program.cs — bu sırayla olmalı
builder.Services.AddRedisCache(builder.Configuration);   // 1) IDistributedCache (Redis)
builder.Services.AddCache(builder.Configuration);        // 2) IHybridRequestCache + CacheOptions + noop broadcaster
// ...
builder.Services.AddHybridCache(options =>               // 3) HybridCache (L2'yi keşfeder)
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        LocalCacheExpiration = cacheConfig.L1.Ttl,   // Cache:L1:Ttl (1 dk)
        Expiration           = cacheConfig.L2.Ttl    // Cache:L2:Ttl (5 dk)
    };
});
Sıra bozulursa (AddHybridCache önce, AddRedisCache sonra) HybridCache IDistributedCache’i göremez ve L1-only mode’a düşer — Redis hiçbir zaman dolmaz, multi-instance’da her node ayrı veri tutar. AddCache metodu (BuildingBlocks.Caching/DependencyInjection.cs) bilerek HybridCache register etmez; bu sorumluluk Program.cs’e bırakılır çünkü L2 backend önce hazır olmalı.
AddCache ne register eder:
public static IServiceCollection AddCache(this IServiceCollection services, IConfiguration configuration)
{
    services.Configure<CacheOptions>(o => configuration.GetSection("Cache").Bind(o));
    services.AddMemoryCache();
    services.AddSingleton<IHybridRequestCache, HybridRequestCache>();
    // Default: noop. Multi-instance'da Application katmanı bunu MassTransit impl'i ile değiştirir.
    services.TryAddSingleton<IRemoteTagBroadcaster, NoopRemoteTagBroadcaster>();
    return services;
}

Spec tabanlı cache — CachedRepository<T>

CachedRepository<T> (src/.../Infrastructure.EFCore/Repositories/CachedRepository.cs), IReadRepository<T> üzerinde bir HybridCache decorator’üdür. Read path’i spec cache açıksa otomatik cache’ler:
public async Task<List<T>> ListAsync(ISpecification<T> spec, CancellationToken ct = default)
{
    if (CacheDisabled(spec))
        return await _source.ListAsync(spec, ct);   // raw EF Core

    var key = $"{spec.CacheKey}:List";
    return await _cache.GetOrCreateAsync(
        key,
        async c => await _source.ListAsync(spec, c),
        BuildOptions(spec),   // TTL: spec.WithCacheTtl ?? CacheOptions.L2.Ttl
        TagsOf(spec),         // spec.WithTags(...)
        ct);
}
Spec’te cache’i şu fluent zincirle açarsın (Ardalis EnableCache + projeye özel WithCacheTtl/WithTags):
public sealed class PublicFaqsPagedSpecification : Specification<Faq>
{
    public const string CacheTag = "faqs";

    public PublicFaqsPagedSpecification(PublicFaqsFilter filter)
    {
        Query.Where(f => f.DeletedAt == null);
        Query.Where(f => f.IsActive);
        // ... filtre + sıralama + paging ...
        Query.AsNoTracking();

        // EFCore-layer cache. Tüm sayfa/sort/search varyantları aynı tag altında
        // toplanır → tek RemoveByTagAsync("faqs") hepsini lazy invalidate eder.
        Query.EnableCache(
                nameof(PublicFaqsPagedSpecification),
                filter.Page, filter.PageSize, filter.SearchTerm)
            .WithCacheTtl(TimeSpan.FromMinutes(5))
            .WithTags(CacheTag);
    }
}
WithCacheTtl adı bilinçli seçilmiştir — Ardalis’in WithTimeToLive’i ile ambiguous reference olmasın diye. TTL/tag bilgisi spec instance’ına ConditionalWeakTable üzerinden attach edilir (SpecificationExtensions); spec GC olunca metadata da serbest kalır.
EnableCache(name, args...) zinciri spec’in CacheKey + CacheEnabled property’lerini set eder (Ardalis 9.x). args cache key’in parçası olur — yani her sayfa/arama varyantı ayrı entry, ama hepsi aynı faqs tag’i altında.

Cache ne zaman devre dışı?

CachedRepository.CacheDisabled(spec) üç koşuldan biri sağlanırsa raw EF Core’a düşer:
private bool CacheDisabled<TSpec>(TSpec spec) where TSpec : ISpecification<T>
    => !_options.CurrentValue.Enabled       // global Cache:Enabled = false (kill-switch)
       || !spec.CacheEnabled                 // spec EnableCache çağırmamış
       || string.IsNullOrWhiteSpace(spec.CacheKey);
KoşulSonuç
Cache:Enabled = falseTüm spec cache bypass (emergency kill-switch). Auth/UserContext/JWKS/PKCE cache’lerini etkilemez.
Spec EnableCache çağırmamışO query cache’lenmez (örn. admin spec’leri — anlık değişiklik görünsün).
CacheKey boşCache’lenmez.
Ayrıca AnyAsync ve spec’siz çağrılar (ListAsync(), GetByIdAsync(...)) bilerek passthrough’tur — cache istiyorsan CompositeSpecification + EnableCache kullan.

İlgili

Invalidation

Tag düşürme: domain event handler’da RemoveByTagAsync.

Multi-Instance Senkron

RemoveByTagLocallyAsync + echo guard.

Specification

Ardalis.Specification ve cache zinciri.

Cache Mimarisi

Genel bakış ve konfigürasyon.