Yükleniyor...

ASP.NET Core'da ETag Caching: If-None-Match ve 304 Not Modified

ASP.NET Core API'lerinde ETag tabanlı conditional GET kullanın; yanıt için fingerprint üretin, If-None-Match header'ını kontrol edin ve veri değişmediyse 304 döndürün.

Bir GET endpoint’i, alttaki kaynak değişmemiş olsa bile çoğu zaman aynı temsili tekrar tekrar döndürür. Her istekte JSON yanıtının tamamını göndermek gereksiz ağ trafiği oluşturur. Bu durum özellikle büyük yanıtlar, mobil istemciler veya sık yenilenen ekranlarda daha belirgin hale gelir.

HTTP bunun için basit bir çözüm sunar: ETag ve If-None-Match. Sunucu mevcut temsil için bir fingerprint üretir ve bunu ETag response header’ında gönderir. Sonraki istekte istemci bu değeri If-None-Match ile sunucuya geri gönderir. Temsil hâlâ aynıysa sunucu response body’yi yeniden göndermek yerine 304 Not Modified döndürür.


Amaç


İstek Akışı

  1. İstemci GET /api/reservations/42 isteği gönderir.
  2. Sunucu kaynağı yükler ve serialize eder.
  3. Sunucu serialize edilmiş temsil üzerinden bir ETag üretir.
  4. Yanıt ETag: "..." header’ını içerir.
  5. İstemci yanıtı ETag ile birlikte saklar.
  6. Sonraki istekte If-None-Match: "..." gönderir.
  7. ETag hâlâ eşleşiyorsa sunucu response body olmadan 304 döndürür.

Örnek Entity


public sealed class Reservation
{
    public int Id { get; set; }
    public string Code { get; set; } = "";
    public string Country { get; set; } = "";
    public string Status { get; set; } = "";
    public DateTime CreatedAt { get; set; }
}

Response DTO

ETag’i doğrudan EF Core entity’sinden üretmek yerine API’nin istemciye gerçekten gönderdiği temsil üzerinden üretmek daha kullanışlıdır. Böylece validator, istemcinin aldığı verinin kendisine bağlı olur.


public sealed record ReservationResponse(
    int Id,
    string Code,
    string Country,
    string Status,
    DateTime CreatedAt);

Adım 1: ETag Helper Oluşturun

Güçlü bir ETag, istemciye döndürülecek UTF-8 byte’larının tam olarak kendisinden üretilebilir. SHA-256, serialize edilmiş temsil değiştiğinde değişen kompakt bir fingerprint sağlar.


using System.Security.Cryptography;

public static class ETagHelper
{
    public static string Create(ReadOnlySpan<byte> content)
    {
        var hash = SHA256.HashData(content);

        return $"\"{Convert.ToHexString(hash)}\"";
    }

    public static bool Matches(
        string? ifNoneMatch,
        string currentEtag)
    {
        if (string.IsNullOrWhiteSpace(ifNoneMatch))
            return false;

        foreach (var raw in ifNoneMatch.Split(
            ',',
            StringSplitOptions.RemoveEmptyEntries |
            StringSplitOptions.TrimEntries))
        {
            if (raw == "*")
                return true;

            var candidate = Normalize(raw);
            var current = Normalize(currentEtag);

            if (string.Equals(
                candidate,
                current,
                StringComparison.Ordinal))
            {
                return true;
            }
        }

        return false;
    }

    private static string Normalize(string value)
    {
        if (value.StartsWith(
            "W/",
            StringComparison.OrdinalIgnoreCase))
        {
            return value[2..];
        }

        return value;
    }
}

Tırnak İşaretleri Neden Önemlidir?

Entity Tag değerleri normalde tırnak işaretleri içinde gönderilir. Bu nedenle üretilen header şu şekilde görünür:


ETag: "8B87A05CDA844D65..."

İstemci bu değeri değiştirmeden geri göndermelidir:


If-None-Match: "8B87A05CDA844D65..."

Adım 2: Controller’a Conditional GET Ekleyin

Endpoint rezervasyonu yükler, response modeline map eder ve yalnızca bir kez serialize eder. Aynı byte’lar hem ETag hesaplamak hem de HTTP yanıtını oluşturmak için kullanılır.


using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

[ApiController]
[Route("api/reservations")]
public sealed class ReservationsController : ControllerBase
{
    private static readonly JsonSerializerOptions JsonOptions =
        new(JsonSerializerDefaults.Web);

    private readonly AppDbContext _db;

    public ReservationsController(AppDbContext db)
    {
        _db = db;
    }

    [HttpGet("{id:int}")]
    public async Task<IActionResult> Get(
        int id,
        CancellationToken cancellationToken)
    {
        var reservation = await _db.Reservations
            .AsNoTracking()
            .Where(x => x.Id == id)
            .Select(x => new ReservationResponse(
                x.Id,
                x.Code,
                x.Country,
                x.Status,
                x.CreatedAt))
            .FirstOrDefaultAsync(cancellationToken);

        if (reservation is null)
            return NotFound();

        var payload = JsonSerializer.SerializeToUtf8Bytes(
            reservation,
            JsonOptions);

        var etag = ETagHelper.Create(payload);

        Response.Headers["ETag"] = etag;
        Response.Headers["Cache-Control"] =
            "private, no-cache";

        var ifNoneMatch =
            Request.Headers["If-None-Match"].ToString();

        if (ETagHelper.Matches(ifNoneMatch, etag))
        {
            return StatusCode(
                StatusCodes.Status304NotModified);
        }

        return File(
            payload,
            "application/json; charset=utf-8");
    }
}

İlk İstek

İlk istekte henüz If-None-Match header’ı bulunmaz:


GET /api/reservations/42 HTTP/1.1
Host: api.example.com

API, JSON temsilini ETag değeriyle birlikte döndürür:


HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: private, no-cache
ETag: "8B87A05CDA844D65..."

{
  "id": 42,
  "code": "RSV-0042",
  "country": "TR",
  "status": "active",
  "createdAt": "2026-09-16T10:30:00Z"
}

If-None-Match ile İkinci İstek

İstemci daha önce aldığı ETag değerini sunucuya geri gönderir:


GET /api/reservations/42 HTTP/1.1
Host: api.example.com
If-None-Match: "8B87A05CDA844D65..."

Rezervasyon değişmemişse sunucu şu yanıtı döndürür:


HTTP/1.1 304 Not Modified
ETag: "8B87A05CDA844D65..."
Cache-Control: private, no-cache

JSON body gönderilmez. İstemci daha önce cache’lediği temsili kullanmaya devam edebilir.


Kaynak Değişirse Ne Olur?

Rezervasyon durumunun active değerinden completed değerine değiştiğini düşünün. Serialize edilen JSON da değişeceği için SHA-256 farklı bir ETag üretir.

Eski If-None-Match değeri artık eşleşmez. API bu nedenle güncellenmiş body ve yeni ETag ile normal bir 200 OK yanıtı döndürür.


HTTP/1.1 200 OK
ETag: "0F7C49B2D5A81E31..."

{
  "id": 42,
  "code": "RSV-0042",
  "country": "TR",
  "status": "completed",
  "createdAt": "2026-09-16T10:30:00Z"
}

curl ile Hızlı Test

Önce kaynağı isteyin ve dönen ETag değerini not edin:


curl -i \
  "https://localhost:5001/api/reservations/42"

Ardından aynı ETag değerini If-None-Match ile gönderin:


curl -i \
  "https://localhost:5001/api/reservations/42" \
  -H 'If-None-Match: "8B87A05CDA844D65..."'

Temsil değişmemişse ikinci isteğin JSON payload olmadan 304 Not Modified döndürmesi gerekir.


Cache-Control: private, no-cache Ne Anlama Gelir?

no-cache direktifi “bu yanıtı hiçbir zaman saklama” anlamına gelmez. Saklanan bir yanıtın tekrar kullanılmadan önce sunucuyla doğrulanması gerektiğini belirtir. Bu nedenle ETag tabanlı conditional request’lerle birlikte kullanılması oldukça uygundur.

private, yanıtın ortak bir cache yerine istemciye ait özel bir cache için tasarlandığını belirtir. Herkese açık kaynaklarda uygulamanın ihtiyaçlarına göre farklı cache politikaları kullanılabilir.


Hash Tabanlı ETag Kullanıldığında Veritabanı Sorgusu Yine Çalışır

Bu örnek, değişmemiş bir response body’nin tekrar ağ üzerinden gönderilmesini önler. Ancak sunucu hash’i hesaplayabilmek için yine de veritabanını sorgular, DTO’yu oluşturur ve serialize eder.

Küçük API’lerde bu yaklaşım çoğu zaman tamamen yeterlidir. Daha yüksek trafikli sistemlerde ETag’i doğrudan bir kaynak sürümünden üretebilirsiniz; örneğin veritabanındaki rowversion, bir versiyon numarası veya temsil değiştiğinde değişen başka bir değer kullanılabilir.

Böyle bir yaklaşım doğrulama işlemini daha ucuz hale getirebilir; çünkü istemcinin zaten güncel sürüme sahip olup olmadığını anlamak için tüm yanıtı serialize etmek gerekmeyebilir.


ETag Yalnızca Tarayıcı Cache’i İçin Değildir

ETag’ler, istekler arasında bir validator saklayabilen tüm HTTP istemcileri için kullanılabilir. Tarayıcılar, mobil uygulamalar, masaüstü uygulamaları, API istemcileri ve reverse proxy’ler conditional request mekanizmasından yararlanabilir.

İstemcinin yapması gereken tek şey, ETag’i cache’lediği temsil ile birlikte saklamak ve aynı kaynağı yeniden isterken If-None-Match üzerinden geri göndermektir.


Yaygın İyileştirmeler


TL;DR

  • Mevcut yanıtı temsil eden bir ETag üretin.
  • Bu değeri ETag response header’ında döndürün.
  • İstemci değeri sonraki istekte If-None-Match ile geri göndersin.
  • Değerler eşleşiyorsa response body olmadan 304 Not Modified döndürün.
  • Kaynak değişmişse yeni body ve yeni ETag ile 200 OK döndürün.