ASP.NET Core 6/Cache'leme

Cache'lemek programlama dünyasında yaygın kullanılan bir şeydir. Üretilmesi maliyetli olan verilere sık erişim gerekiyorsa cache'lenmesi mantıklıdır. Bu sayede aynı verinin tekrar istenmesi durumunda en baştan tekrar üretilmesi yerine cache'lenmiş veri direkt sunulur. Üretilmesi maliyetli veri uygulamadan uygulamaya değişebilir. Bu derste 1'den belirli bir sayıya kadar olan sayıların toplamını maliyetli veri sayacağız. Şimdi Projemize Toplam.cs isimli bir sınıf ekleyelim ve içeriği şöyle olsun:

public class Toplam
{
    public async Task Endpoint(HttpContext context)
    {
        int sayi;
        int.TryParse((string)context.Request.RouteValues["count"], out sayi);
        long toplam = 0;
        for (int i = 1; i <= sayi; i++)
        {
            toplam += i;
        }
        string toplamString = $"({DateTime.Now.ToLongTimeString()}) {toplam}";
        await context.Response.WriteAsync($"({DateTime.Now.ToLongTimeString()}) 1'den {sayi} sayısına kadar olan sayıların toplamı: " + $"\n{toplamString}\n");
    }
}

Şimdi bu endpoint'i rota yapılandırmasına ekleyelim (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/sum/{count:int=1000000000}",new Toplam().Endpoint);
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});
app.Run();

Programımız /sum/<sayi> path'ına gelen isteklere Toplam endpoint'i ile karşılık vermektedir. Path'ın ikinci segmenti int'e dönüştürübilen bir metin olmalıdır. Path'ın tek segmentten oluştuğu durumda sayı değeri varsayılan olarak 1.000.000.000 alınır. Endpoint sınıfımız path'daki count segmentindeki sayıyı int'e dönüştürmekte, daha sonra 1'den bu sayıya kadar olan sayıların toplamını bulup response'a yazmaktadır. Response'a yazarken geçerli zamanı iki kere yazmaktadır.

Programı çalıştırıp sum parametresi için 10 değerini verdiğimizde sayfaya şöyle bir çıktı verilir:

(19:49:50) 1'den 10 sayisina kadar olan sayilarin toplami: 
(19:49:50) 55

Burada iki zaman damgasının da aynı olması iki verinin de sıfırdan üretildiğini, cache'lenmediğini gösterir. Sayfayı birkaç kez yenilerseniz zaman geçtikçe zaman damgasının değiştiğini göreceksiniz. Bu da verilerin tekrar hesaplandığını gösterecek.

Verinin cache'lenmesi

değiştir

Üretilmesi maliyetli bir veriyi cache'lemek için IDistributedCache servisini kullanırız. Örnek (Toplam.cs dosyası):

using Microsoft.Extensions.Caching.Distributed;

public class Toplam
{
    public async Task Endpoint(HttpContext context, IDistributedCache cache)
    {
        int sayi;
        int.TryParse((string)context.Request.RouteValues["count"], out sayi);
        string cacheKey = $"sum_{sayi}";
        string toplamString = await cache.GetStringAsync(cacheKey);
        if (toplamString == null)
        {
            long total = 0;
            for (int i = 1; i <= sayi; i++)
            {
                total += i;
            }
            toplamString = $"({DateTime.Now.ToLongTimeString()}) {total}";
            await cache.SetStringAsync(cacheKey, toplamString,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
            });
        }
        await context.Response.WriteAsync($"({DateTime.Now.ToLongTimeString()}) 1'den {sayi} sayısına kadar olan sayıların toplamı: " + $"\n{toplamString}\n");
    }
}

Bu kodumuz önceki versiyonla aynı işi yapmaktadır. Tek fark 1'den kendisine kadar olan sayıların toplamı hesaplanacak sayı cache'te varsa tekrar hesaplama yapmadan cache'ten alıp vermektedir. Eğer cache'te yoksa veriyi hesaplamakta, daha sonra cache'e yazmaktadır. Bir cache değişkeninin cache'te kalma süresi 2 dakikadır. Eğer veri cache'ten alındıysa sayfanın verdiği çıktıdaki birinci satırdaki zaman ile ikinci satırdaki zaman birbirinden farklı olacaktır. IDistributedCache arayüzündeki önemli üyeler şunlardır:

GetString(key): Belirtilen anahtarla temsil edilen cache değerini string olarak döndürür. Böyle bir cache değişkeni yoksa null döndürür.
GetStringAsync(key): GetString(key) metodunun asenkron versiyonudur.
SetString(key,value,options): Belirtilen anahtarla temsil edilecek cache değeri belirtilir. Bir DistributedCacheEntryOptions nesnesiyle ilgili cache değişkeniyle ilgili ayarlamalar yapılabilir.
SetStringAsync(key,value,options): SetStringAsync(key,value,options) metodunun asenkron versiyonudur.
Refresh(key): Anahtarla temsil edilen cache değişkeninin ömür sayacını sıfırlar.
RefreshAsync(key): Refresh(key) metodunun asenkron versiyonudur.
Remove(key): Anahtarla temsil edilen cache değişkenini siler.
RemoveAsync(key): Remove(key) metodunun asenkron versiyonudur.

Varsayılan durumda cache değişkenleri cache'te sonsuza kadar kalır. Ancak SetString() ve SetStringAsync() metotlarının üçüncü parametresine bir DistributedCacheEntryOptions nesnesi verilerek bu varsayılan durum değiştirilebilir. DistributedCacheEntryOptions sınıfının önemli özellikleri şunlardır:

AbsoluteExpiration: Tam olarak hangi zamanda cache değişkeninin öleceğini belirtiriz.
AbsoluteExpirationRelativeToNow: Cache değişkeninin ömrünü belirtiriz (ne kadar süre yaşayacak)
SlidingExpiration: Cache değişkenine ne kadar süre erişilmediği zaman silineceğini belirtmeye yarar.

Cache servisinin çalışması için aşağıdaki şekilde AddDistributedMemoryCache servisini ayağa kaldırmalıyız (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDistributedMemoryCache(opts => {
    opts.SizeLimit = 200;
});
var app = builder.Build();
app.MapGet("/sum/{count:int=1000000000}",new Toplam().Endpoint);
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});
app.Run();

Bu kodumuzda cache değişkenlerini RAM bellekte tutacağımızı söyledik. Aşağıdaki altenatif servisler vasıtasıyla cache değişkenlerini başka yerlerde de tutabiliriz:

AddDistributedMemoryCache(): Cache değerlerini RAM bellekte tutar.
AddDistributedSqlServerCache(): Cache değrlerini SQL Server'da tutar.
AddStackExchangeRedisCache(): Cache değerlerini Redis veritabanında tutar.

Örneğimizde RAM bellekte cache değişkenlerini depolamak için AddDistributedMemoryCache() servisi kullanılmıştır. Bu servis MemoryDistributedCacheOptions sınıfı ile konfigüre edilir. Bu sınıfın önemli özellikleri şunlardır:

ExpirationScanFrequency: Belirli aralıklarla cache değişkenlerinin ömrünün dolup dolmadığı kontrol edilir. Ömrü dolan bir cache değişkeni tespit edilirse cache'ten silinir. İşte buradaki "belirli aralık"ı belirtir.
SizeLimit: Cache'teki maksimum değişken sayısını belirtir. Bu sayı aşılırsa cache'ten silme yapılır.
CompactionPercentage: SizeLimit'e ulaşıldığında cache'in boyutunun ne kadar azaltılacağını belirtir.

Örneğimizde maksimum 200 değişken olabilir. Programımızı çalıştırdığımızda zaman damgalarının bir tanesinin sürekli güncel değeri gösterdiğini, diğerinin ise eğer cache'lenmiş veriye erişiyorsak sabit kaldığını görürüz.

Cache'lemenin kalıcı belleğe yapılması

değiştir

Bir önceki örneğimizde cache'lemeyi RAM belleğe yaptık. İki açıdan RAM belleğe cache'leme yapmanın dezavantajı vardır:

  1. RAM belleğe yazılan veriler geçicidir. Sunucu yeniden başladığında veriler silinir.
  2. Bazen uygulamamızı birden fazla sunucuya dağıtabiliriz. Bu durumda sunucular birbirlerinin cache'lerini kullanamazlar. Uygulamanın birden fazla sunucuya dağıldığı durumlarda cache'lemeyi düzgün bir şekilde yapabilmek için cache'lemeyi kalıcı belleğe yapmalıyız.

Bu bölümde cache'lemeyi SQL Server'a yapacağız. Bunun için öncelikle SQL Server Management Studio'yu açın ve CacheDb isimli bir veritabanı oluşturun. Daha sonra bu veritabanına bir tablo ekleyeceğiz. Ancak bu tabloda ihtiyaç duyulacak sütunları otomatik oluşturmak için bir araç kullanacağız. Visual Studio'da "Geliştirici PowerShell" pencereciğine gelin, cd komutuyla .csproj dosyasının olduğu klasöre geçin ve aşağıdaki komutu verin:

dotnet sql-cache create "Server=DESKTOP-J5SDIAM;Database=CacheDb;Trusted_Connection=True;" dbo DataCache

Bu komuta verilen parametreler SQL Server'a bağlanmak için kullanılacak connection string'i, tablonun şeması ve tablo ismidir. Tabloda Id, Value, ExpiresAtTime, SlidingExpirationInSeconds ve AbsoluteExpiration sütunları bulunmaktadır ve cache değişkeni ismini, cache değerini ve cache değişkeninin ne kadar süreyle cache'te tutulacağını belirten bilgiler içermektedir. Bu sütunları yine SQL Server Management Studio ile görebilirsiniz.

Kalıcı cache'leme servisini kullanma

değiştir

Veritabanımız hazır olduğuna göre şimdi programımıza geçebiliriz. Şimdi SQL Server'a cache'leme yapacak NuGet paketini kurmalıyız. Paketin ismi Microsoft.Extensions.Caching.SqlServer. Bu örnek için 6.0.0 versiyonunu kurmalıyız.

Şimdi SQL server için connection string'i aşağıdaki şekilde appsettings.json dosyasına ekleyelim:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "CacheConnection": "Server=DESKTOP-J5SDIAM;Database=CacheDb;Trusted_Connection=True;"
  }
}

Şimdi Program.cs dosyasını AddDistributedMemoryCache() servisi yerine AddDistributedSqlServerCache() servisini kullanacak şekilde değiştirelim:

var builder = WebApplication.CreateBuilder(args);
//builder.Services.AddDistributedMemoryCache(opts => {
// opts.SizeLimit = 200;
//});
builder.Services.AddDistributedSqlServerCache(opts => {
    opts.ConnectionString = builder.Configuration["ConnectionStrings:CacheConnection"];
    opts.SchemaName = "dbo";
    opts.TableName = "DataCache";
});
var app = builder.Build();
app.MapGet("/sum/{count:int=1000000000}",new Toplam().Endpoint);
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});
app.Run();

Burada cache'leme için SQL Server kullanacağımızı belirtik, ayrıca veritabanına bağlantı için kullanılacak string'i, veritabanında cache değişkenlerini tutacak tabloyu ve tablonun şemasını da belirttik. AddDistributedSqlServerCache() servisi SqlServerCacheOptions sınıfı aracılığıyla konfigüre edilmektedir. Bu sınıfın önemli özellikleri şunlardır:

ConnectionString: Veritabanına bağlantı için kullanılacak string.
SchemaName: Cache'leme için kullanılacak tablonun şeması.
TableName: Cache'leme için kullanılacak tablo.
ExpiredItemsDeletionInterval: Tablo hangi sıklıkla ömrü dolan değişken var mı diye kontrol edilecek? Varsayılan: 30 dakika.
DefaultSlidingExpiration: Bir cache değişkenine maksimum ne kadar süre erişilmezse cache değişkeni silinecek? Varsayılan: 20 dakika

Programı çalıştırdığınızda çalışmasında hiçbir fark olmadığını göreceksiniz. Ancak önceki versiyonun aksine ASP.NET Core yeniden başlatıldığında da cache değişkenlerine erişebildiğinizi göreceksiniz. Ayrıca uygulamanın veritabanındaki tabloya yazdığı verileri SQL Server management Studio'yu kullanarak görebilirisniz.

NOT: Cache'lemenin mantığı birden fazla kullanıcı aynı verilere erişmeye çalıştığında optimizasyon sağlamaktır. Cache'lemenin amacı kullanıcıya özgü hassas verileri tutmak değildir. Kullanıya özgü verileri tutmak için oturum servisini kullanın.

Response'ları cache'leme

değiştir

Şimdiye kadar hep bir response'un içindeki birtakım verileri cache'ledik. Bu bölümde response'un tamamını bir bütün halinde cache'leyeceğiz. Örnek (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
app.MapGet("/sum/{count:int=1000000000}",new Toplam().Endpoint);
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});
app.Run();

UseResponseCaching() middleware'inin diğer middleware ve endpoint'ler tarafından oluşturulan response'ları cache'leyebilmesi için request pipeline'da ilk tanımlanan middleware'lerden birisi olması gerekmektedir. Ayrıca UseResponseCaching() middleware'i cache'leme yapabilmesi için bellek veya SQL Server tablosuna ihtiyaç duymaz. O yüzden koddan AddDistributedSqlServerCache() servis çağrısı kaldırılmıştır.

Şimdi Toplam.cs dosyasının içeriğini şöyle değiştirelim:

using Microsoft.Extensions.Caching.Distributed;

public class Toplam
{
    public async Task Endpoint(HttpContext context)
    {
        LinkGenerator generator=context.RequestServices.GetRequiredService<LinkGenerator>();
        int sayi;
        int.TryParse((string)context.Request.RouteValues["count"], out sayi);
        long toplam = 0;
        for (int i = 1; i <= sayi; i++)
        {
            toplam += i;
        }
        string toplamString = $"({DateTime.Now.ToLongTimeString()}) {toplam}";
        context.Response.Headers["Cache-Control"] = "public, max-age=120";
        string url = generator.GetPathByRouteValues(context, null, new { count = sayi });
        context.Response.ContentType = "text/html";
        string html = @$"
<!DOCTYPE html>
<html>

<head>
  <meta charset=""utf-8"">
  <meta name=""viewport"" content=""width=device-width, initial-scale=1"">
  <title>Cache'leme</title>
</head>

<body>
  <div>({DateTime.Now.ToLongTimeString()}) {sayi} sayısı için toplam:</div>
  <div>{toplamString}</div>
  <div><a href=""{url}"">Sayfayı Yenile</a></div>
</body>

</html>";
        await context.Response.WriteAsync(html);
    }
}

Evet, kod çok karmaşık gibi gözüküyor. Ancak bizin için burada önemli olan

context.Response.Headers["Cache-Control"] = "public, max-age=120";

satırı. Response caching middleware'i sadece "Cache-Control" header'ını ve bu header'ın içinde "public" direktifini içeren response'ları cache'ler. max-age direktifi saniye cinsinden ne kadar süreyle response'un cache'leneceğini belirtir.

Response caching'i etkinleştirmek basittir, sadece "Cache-Control" header'ını ayarlamak yeterlidir. Ancak çalıştığını görmek için dikkat etmemiz gereken şeyler var. F5 tuşuna basmamız, tarayıcının araç çubuğundaki yenile butonuna basmamız veya adres çubuğuna tıklayıp enter tuşuna basmamız response caching'in çalışmamasına yol açar. Response caching'in tek çalışabileceği senaryo bir link üzerinden ilgili talebin yapılmasıdır. Zaten bu yüzden oluşturulan çıktıya LinkGenerator servisini kullanarak link ekledik. Bu linke tıkladığımızda aynı URL'ye talep yapılır, ancak veri cache'lenmiş response'tan gelir. Bunu, response'taki zamanın değişmemesinden anlayabiliriz. İlk yaptığınız talepten sonra 2 dakika geçtikten sonra tekrar talep yapmanız durumunda belirtilen zamanın değiştiğini göreceksiniz.

Cache-Control header'ının direktiflerindeki public direktifi ilgili cache'in kullanıcıya özgü olmadığını belirtir. Diğer bir deyişle bir kullanıcı ilgili URL'ye talepte bulunduğunda 2 dakika boyunca aynı URL'ye link yoluyla talepte bulunan diğer kullanıcılar da cache'lenmiş aynı içeriği görecektir.

Response caching middleware'i response'un değişip değişmediğini umursamaz. Aynu URL'e link yoluyla talep geldiğinde cache'lenmiş response'u sunar.