Mert Özen Her Satırda Daha İleri
Backend ASP.NET Core MVC ve Razor Pages — #15

MVC'de Request'ler Arası State: TempData, Session, Cookie ve Query String — Hangisi Ne Zaman?

Mert Özen 15 Kas 2026 14 dk 6 görüntülenme
MVC'de Request'ler Arası State: TempData, Session, Cookie ve Query String — Hangisi Ne Zaman?

Filtre için query string, tercih için onaylı cookie, anonim sepet için Data Protection ile korunan cookie, çok adımlı form için Redis'li session, flash mesaj için TempData. Bookstore.Web'i iki instance'ta çalıştırıp bellek içi session'ın formu sessizce sıfırladığını, ayrı key ring'in sepeti boşalttığını ve çerez onayının TempData'yı nasıl kapattığını gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.404, runtime 10.0.15), Microsoft.Extensions.Caching.StackExchangeRedis 10.0.15 ve Redis 8.10.3 ile derlenip denenmiştir. Sepet ve kurumsal sipariş formu .NET 11 (SDK 11.0.100) üzerinde, paketin 11.0.0 sürümüyle de aynı sonuçları verdi.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) MVC tarafı Bookstore.Web, yoğun dönemde iki instance'a çıkarıldı. Önlerinde bir load balancer var ve sticky session (bir kullanıcının isteklerini hep aynı instance'a gönderme) kapalı. Session, template'teki gibi AddDistributedMemoryCache ile tutuluyor. Okulun satın alma sorumlusu Nevin Hanım üç adımlı kurumsal sipariş formunun ilk adımını dolduruyor; ikinci adımın isteği öbür instance'a gidiyor. Ne olur: hata mı alır, form mu sıfırlanır, yoksa hiçbir şey fark etmez mi?

İkinci soru: son adımda sipariş özetini bir sonraki sayfada göstermek için TempData["Draft"] = draft; yazdın, draft da bir class. Kod derlenir mi, çalışır mı?

State'i Nereye Koyabilirsin?

HTTP stateless; iki istek arasında bir şey taşımak istiyorsan onu ya tarayıcıya ya da sunucuya koyarsın. MVC'de seçenekler:

  • Query string ve route: herkesin görebileceği, paylaşılabilir adres. Filtre, sıralama, sayfa numarası.
  • Hidden field: formla birlikte gidip gelen değer. Kullanıcı değiştirebilir.
  • Cookie: tarayıcıda durur, her istekte sunucuya gider. Data Protection ile şifrelenip imzalanabilir.
  • TempData: bir sonraki isteğe kadar yaşayan küçük değerler. Varsayılan olarak bir cookie'de.
  • Session: tarayıcıda yalnız bir id, verinin kendisi sunucu tarafındaki bir IDistributedCache'te.
  • Veritabanı: kalıcı olan her şey, örneğin gönderilmiş sipariş.

Bu yazıda Bookstore.Web'e dördünü ekliyoruz: anonim kullanıcının sepeti, üç adımlı kurumsal sipariş formu, flash mesajlar ve katalog sıralaması tercihi. Kod Bookstore reposunun src/Bookstore.Web projesinde; dotnet run --project src/Bookstore.Web ile açılıyor.

TempData: Flash Mesajın Yeri

Formlar yazısında Post-Redirect-Get için TempData["StatusMessage"] kullanmıştık. Hata sayfaları yazısında da DomainExceptionHandler stok uyarısını TempData'ya yazıyordu. Varsayılan provider CookieTempDataProvider: değerleri serialize ediyor, Data Protection ile şifreliyor, Base64Url ile kodluyor ve gerekirse birkaç cookie'ye bölüyor. Sepete ekleme sonrasındaki gerçek yanıt:

HTTP/1.1 302 Found
Location: /books/1/dune
Set-Cookie: bookstore_cart=CfDJ8FswMoxR_8FKk...; max-age=2592000; path=/; samesite=lax; httponly
Set-Cookie: .AspNetCore.Mvc.CookieTempDataProvider=CfDJ8FswMoxR_8FKk...; path=/; samesite=lax; httponly

CfDJ8 ile başlayan her değer Data Protection çıktısı (değerleri kısalttım). TempData'daki bir değer okunduğu isteğin sonunda silinmek üzere işaretleniyor. Kurumsal siparişin onay sayfası bunu kullanıyor:

[HttpGet("done")]
public IActionResult Done() => View(nameof(Done), TempData["OrderNumber"] as string);

Aynı sayfayı iki kez açınca:

<h1>Order IO-1001 was sent for approval</h1>
<h1>Your order was sent for approval</h1>

İlk istek numarayı okudu ve sildi; yenilenen sayfa genel mesajı gösteriyor. TempData.Peek("OrderNumber") ile denediğimde iki istekte de numara geldi, ama TempData cookie'si başka sayfaları gezdikten sonra da tarayıcıda kaldı; Peek değeri silinmekten korur ve onu bir sonraki normal okumaya kadar taşır. Keep() da okunmuş değerleri bir istek daha tutar. İkisini de bilinçli kullan, yoksa eski bir mesaj yanlış sayfada çıkar.

View(nameof(Done), ...) yazmamın nedeni ayrı bir tuzak: ilk hâli View(TempData["OrderNumber"] as string) idi. View(string) overload'u parametreyi view adı sayıyor; sonuç The view 'IO-1001' was not found. hatası.

Session'ı zaten kullanan bir uygulama TempData'yı session'a da taşıyabilir: AddControllersWithViews().AddSessionStateTempDataProvider(). Microsoft'un dokümanı, TempData'ya 500 byte'tan fazla veri koyuyorsan session provider'ın her istekte veriyi gidip getirmekten kurtardığını yazıyor. Bookstore.Web cookie provider'da kalıyor: mesajlar küçük ve session yalnız sipariş formunda açık. Bu bölümde resmî belgelerden yararlanılmıştır.

Sepet, kullanıcının giriş yapmadan biriktirdiği ve günlerce durmasını beklediği bir veri. Session'ın 30 dakikalık ömrüne uymuyor; veritabanına yazmak için de henüz bir kullanıcı yok. Bookstore.Web sepeti "kitap id'si:adet" çiftleri olarak bir cookie'de tutuyor:

// Anonim sepet tarayıcıda: "bookId:quantity" çiftleri, Data Protection ile şifreli ve imzalı
public sealed class CookieCartStore(IDataProtectionProvider provider, ILogger<CookieCartStore> logger) : ICartStore
{
    public const string CookieName = "bookstore_cart";
    private readonly IDataProtector protector = provider.CreateProtector("Bookstore.Web.Cart.v1");

    public IReadOnlyList<CartLine> Read(HttpContext context)
    {
        string? value = context.Request.Cookies[CookieName];
        if (string.IsNullOrEmpty(value))
        {
            return [];
        }

        try
        {
            return protector.Unprotect(value)
                .Split(',', StringSplitOptions.RemoveEmptyEntries)
                .Select(pair => pair.Split(':'))
                .Select(parts => new CartLine(int.Parse(parts[0]), int.Parse(parts[1])))
                .ToList();
        }
        catch (CryptographicException ex)
        {
            // Değiştirilmiş cookie ya da bu instance'ta olmayan bir key: boş sepetle devam et
            logger.LogWarning(ex, "Cart cookie could not be read");
            return [];
        }
    }

    public void Write(HttpContext context, IReadOnlyList<CartLine> lines)
    {
        string payload = string.Join(',', lines.Select(l => $"{l.BookId}:{l.Quantity}"));
        context.Response.Cookies.Append(CookieName, protector.Protect(payload), new CookieOptions
        {
            HttpOnly = true,
            Secure = context.Request.IsHttps,
            SameSite = SameSiteMode.Lax,
            MaxAge = TimeSpan.FromDays(30),
            IsEssential = true // sepet, kullanıcının istediği şeyin kendisi
        });
    }
}

CreateProtector'a verilen purpose string'i (Bookstore.Web.Cart.v1) önemli: aynı key ring'le korunan TempData ya da antiforgery cookie'sinin değeri sepet cookie'si olarak açılamıyor. Sonundaki v1, format değişirse eski cookie'leri ayırmak için.

Cookie'de fiyat yok, yalnız id ve adet var. Fiyat her istekte katalogdan okunuyor; böylece kullanıcı cookie'yi açamasa bile fiyatı değiştiremez, fiyat değişirse sepet güncel kalır. İki kitapla sepet:

<li>2 × Dune · 978.00 TRY</li>
<li>1 × Foundation · 319.00 TRY</li>
<p class="price">Total 1297.00 TRY</p>

Cookie değeri iki kitapla 134, stoğu olan 19 kitabın hepsiyle 240 karakter. Tarayıcıların çoğu bir cookie'yi yaklaşık 4096 byte ile sınırlıyor ve cookie her istekte, CSS ve resim istekleri dahil, sunucuya gidiyor. "Id ve adet" formatı bu sınıra rahatça sığıyor; kitap başlığını ya da açıklamasını cookie'ye koysaydık sığmazdı.

Cookie'nin son karakterlerinden birini değiştirip /cart'ı açtığımda sepet boş geldi ve log'a şu düştü:

warn: Bookstore.Web.Ordering.CookieCartStore[0] ... Cart cookie could not be read System.Security.Cryptography.CryptographicException: The payload was invalid.

Data Protection değeri şifrelemekle kalmıyor, imzalıyor da; değiştirilmiş bir cookie sessizce yanlış veri vermiyor, açılmıyor.

Kurumsal Sipariş Formu: Session

Kurumsal siparişin üç adımı var: kurum bilgisi, kitaplar ve adetler (en az 20 kopya), %10 kurumsal indirimle özet. Adımlar arasındaki taslak, kullanıcı formu bitirene kadar yaşaması gereken, birkaç yüz byte'lık bir veri. Session tam bu iş için:

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.Name = ".Bookstore.Session";
    options.Cookie.Path = "/institutional-orders"; // yalnız formun istekleri taşısın (ve store'a gitsin)
    options.Cookie.IsEssential = true; // kurumsal sipariş formu bu cookie olmadan çalışmaz
});

// Session yalnız kullanıldığı yerde: UseSession'dan geçen her istek store'daki kaydını tazeliyor
app.UseWhen(context => context.Request.Path.StartsWithSegments("/institutional-orders"), wizard => wizard.UseSession());

IdleTimeout'un varsayılanı 20 dakika ve her istekte yeniden başlıyor; uzun bir kitap listesini dolduran kullanıcı için 30 dakika verdim. Session'a yalnız string, int ve byte[] yazılabiliyor, object'leri JSON'a çevirmek sana kalıyor:

public static class SessionJsonExtensions
{
    public static T? GetJson<T>(this ISession session, string key) =>
        session.GetString(key) is { } json ? JsonSerializer.Deserialize<T>(json) : default;

    public static void SetJson<T>(this ISession session, string key, T value) =>
        session.SetString(key, JsonSerializer.Serialize(value));
}

Controller her adımda taslağı yüklüyor, ilk adım eksikse kullanıcıyı başa gönderiyor:

private async Task<InstitutionalOrderDraft> LoadDraftAsync(CancellationToken cancellationToken)
{
    // Session'ı asenkron yükle; yoksa ilk erişim onu senkron yükler
    await HttpContext.Session.LoadAsync(cancellationToken);
    return HttpContext.Session.GetJson<InstitutionalOrderDraft>(DraftKey) ?? new InstitutionalOrderDraft();
}

[HttpGet("books")]
public async Task<IActionResult> Books(CancellationToken cancellationToken)
{
    InstitutionalOrderDraft draft = await LoadDraftAsync(cancellationToken);
    if (draft.InstitutionName is null)
    {
        return RedirectToAction(nameof(Institution)); // 1. adım eksik (ya da session gitti)
    }

    return View(new BooksStep { Quantities = draft.Quantities, Catalog = await catalog.ListAsync(null, cancellationToken) });
}

LoadAsync çağrısı kozmetik değil. Çağırmazsan session ilk GetString'te store'dan senkron okunuyor; Redis gibi ağ üzerindeki bir store'da bu, request thread'ini I/O beklerken bloke etmek demek. Formun gerçek akışı:

HTTP/1.1 302 Found
Location: /institutional-orders/new/books
Set-Cookie: .Bookstore.Session=CfDJ8FswMoxR%2F8F...; path=/institutional-orders; samesite=lax; httponly

<li>Institutional orders need at least 20 copies (now 12).</li>

<p>Ankara Science High School / Library · nevin@school.example.com</p>
<li>10 × Dune</li>
<li>10 × The Hobbit</li>
<li>5 × 1984</li>
<p>Subtotal 9584.00 TRY · Institutional discount -958.40 TRY · Total 8625.60 TRY</p>

Gönderimden sonra action taslağı Session.Remove ile siliyor ve /review'a dönen bir istek 1. adıma gönderiliyor. Form tekrar gönderilemiyor.

Çok Instance: Redis ve Ortak Key Ring

Önce Tahmin Et'teki senaryoyu kurdum: aynı uygulamanın iki kopyası 52215 ve 52216 portlarında, aynı cookie jar'la iki instance arasında gidip geliyorum.

15-state-two-instances-tr.png

Bellek içi session ve ortak key ring ile, 1. adım A'da:

GET books on A -> 200
GET books on B -> 302 http://127.0.0.1:52216/institutional-orders/new/institution
cart on B: <li>2 × Dune · 978.00 TRY</li>

İlk sorunun cevabı: hata yok, log'da uyarı yok, form sessizce başa dönüyor. B, session cookie'sini açabiliyor ama o id için kendi belleğinde veri bulamıyor ve boş bir session başlatıyor. Sepet ise B'de dolu; verinin kendisi cookie'de.

İki instance'ın key ring'i ayrıysa (iki ayrı makine ya da container gibi) sepet de gidiyor. B'nin log'u:

warn: Bookstore.Web.Ordering.CookieCartStore[0] ... Cart cookie could not be read System.Security.Cryptography.CryptographicException: The key {fc3f6d2a-...} was not found in the key ring.
fail: Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery[7] ... An exception was thrown while deserializing the token.
warn: Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider[3] ... The temp data cookie .AspNetCore.Mvc.CookieTempDataProvider could not be loaded.
warn: Microsoft.AspNetCore.Session.SessionMiddleware[7] Error unprotecting the session cookie.

Sepet, antiforgery token'ı, TempData ve session cookie'si: hepsi aynı key ring'e bağlı. Çözümün iki parçası var. Birincisi, session verisini paylaşılan bir store'a koymak:

string? redis = builder.Configuration.GetConnectionString("Redis");
if (string.IsNullOrEmpty(redis))
{
    builder.Services.AddDistributedMemoryCache(); // tek instance: veri bu process'in belleğinde
}
else
{
    builder.Services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = redis;
        options.InstanceName = "bookstore-web:";
    });
}

// Her instance diğerlerinin cookie'lerini okuyabilmeli: aynı application name, ortak key ring
IDataProtectionBuilder dataProtection = builder.Services.AddDataProtection().SetApplicationName("Bookstore.Web");
if (builder.Configuration["DataProtection:KeysPath"] is { Length: > 0 } keysPath)
{
    dataProtection.PersistKeysToFileSystem(new DirectoryInfo(keysPath));
}

İkincisi o key ring. Bu denemede iki instance'a aynı klasörü verdim; production'da key'leri Redis'te ya da blob storage'da tutup at-rest şifrelemek gerekiyor. Ayrıntısı ASP.NET Core Güvenliği serisinin Data Protection yazısında. Kendi Redis container'ımla (ConnectionStrings__Redis=127.0.0.1:63790) aynı deneme:

step 2 on B -> 302 http://127.0.0.1:52216/institutional-orders/new/review
<p>Ankara Science High School / Library · nevin@school.example.com</p>
<li>10 × Dune</li>
<li>10 × Foundation</li>

1. adım A'da, 2. adım B'de, özet yine A'da; form kaldığı yerden devam ediyor. Redis'teki kayıt:

$ redis-cli HKEYS bookstore-web:8e542909...
absexp
data
sldexp
$ redis-cli HGET bookstore-web:8e542909... sldexp
18000000000
$ redis-cli TTL bookstore-web:8e542909...
1800
$ redis-cli HGET bookstore-web:8e542909... data
.....>..q.V.....(..#..InstitutionalOrder.Draft....{"InstitutionName":"Ankara Science High School","UnitName":"Library","ContactEmail":"nevin

Kayıt bir Redis hash'i: sldexp 30 dakikanın tick karşılığı, TTL (kaydın Redis'te kalan ömrü) 1800 saniye. Dikkat çeken satır sonuncusu: cookie'deki session id şifreli, ama Redis'teki veri düz JSON. Session'a koyduğun her şey Redis'e erişebilen herkese açık. Distributed cache'in serialization ve key tasarımı Caching serisinin IDistributedCache ve Redis yazısında.

UseWhen ile session'ı yalnız forma açmamın nedenini de ölçtüm. Session middleware, isteğin sonunda session'a hiç dokunulmamış olsa bile store'daki kaydı tazelemeye (RefreshAsync) gidiyor. UseSession() pipeline'ın genelindeyken Redis'i durdurduğumda katalog sayfası 5,6 saniyede açıldı ve log'da "Error closing the session" vardı. UseWhen ile aynı istek 0,02 saniye sürdü; Redis'te de katalogu gezen ziyaretçiler için kayıt oluşmadı. Form sayfası ise Redis yokken 22 saniye sonra 503 döndü; hata sayfaları yazısındaki StatusCodeSelector'a RedisConnectionException için bir satır ekledim.

Tercihler, Query String ve Çerez Onayı

Katalogdaki filtre ve sıralama query string'de: /books?category=Classics&sort=price linki paylaşılabiliyor, yer imine eklenebiliyor ve arama motoru tarafından taranabiliyor. Bunu session'a koymak, "linki arkadaşıma attım ama başka bir liste gördü" şikâyetinin sebebi olur. Son seçilen sıralamayı bir sonraki ziyarette hatırlamak ise bir tercih ve bir cookie'nin işi:

if (sort is "title" or "price")
{
    // Zorunlu değil: cookie policy ile ancak onaydan sonra yazılıyor
    Response.Cookies.Append(SortCookie, sort, new CookieOptions { MaxAge = TimeSpan.FromDays(180), IsEssential = false });
}
else
{
    sort = Request.Cookies[SortCookie] is "price" ? "price" : "title";
}

Hangi cookie için onay gerektiğine CookiePolicyOptions karar veriyor:

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = _ => true; // zorunlu olmayan cookie'ler onayı bekler
    options.MinimumSameSitePolicy = SameSiteMode.Lax;
});

app.UseCookiePolicy();

Onay yokken /books?sort=price sıralamayı uyguluyor ama bookstore_sort cookie'si yanıtta yok. Onay formu ITrackingConsentFeature.GrantConsent() çağırıyor:

HTTP/1.1 302 Found
Set-Cookie: .AspNet.Consent=yes; expires=Mon, 15 Nov 2027 08:07:26 GMT; path=/; samesite=lax

HTTP/1.1 200 OK
Set-Cookie: bookstore_sort=price; max-age=15552000; path=/; samesite=lax

Burada bir tuzak var. Session ve TempData cookie'lerinin IsEssential değeri varsayılan olarak false. CheckConsentNeeded açıkken bu ikisi onay vermemiş kullanıcıya yazılmıyor. Session ayarındaki IsEssential = true satırını sildiğim denemede 1. adımın yanıtında session cookie'si yoktu ve 2. adım kullanıcıyı başa gönderdi. Configure<CookieTempDataProviderOptions>(o => o.Cookie.IsEssential = true) satırını sildiğimde de sepete ekleme yanıtında TempData cookie'si yoktu, kitap sayfasında "sepete eklendi" mesajı görünmedi. Hata yok, log yok; özellik sessizce çalışmıyor. Sepet, sipariş formu ve flash mesaj kullanıcının istediği işin parçası; sıralama tercihi değil. Hangi cookie'nin zorunlu sayılacağı yine de hukuki bir değerlendirme; KVKK (Kişisel Verilerin Korunması Kanunu) tarafını Veri Gizliliği ve KVKK serisinin aydınlatma, açık rıza ve çerezler yazısında anlattık.

Hangisi Ne Zaman?

İhtiyaçYerNeden
Katalog filtresi, sıralama, sayfaQuery stringPaylaşılabilir ve taranabilir adres
Kullanıcı tercihi (sıralama, dil)Cookie, onaydan sonraKüçük, uzun ömürlü, sunucuda yer tutmaz
Anonim sepetData Protection ile korunan cookieGünlerce yaşar, instance'tan bağımsız
Çok adımlı form taslağıSession + RedisBirkaç yüz byte, form bitince silinir
Flash mesajTempData (cookie provider)Bir sonraki isteğe kadar yaşar
Gönderilmiş sipariş, oturum açmış kullanıcının sepetiVeritabanıKalıcı, cihazlar arası

Hidden field tabloda yok, çünkü Bookstore.Web'de bir yeri yok; ama session'sız bir form için seçenek. Adımlar arası veri formun içinde gidip gelir, sunucu tarafında state tutulmaz. Karşılığı: kullanıcı değerleri değiştirebilir. Her adımda bütün veriyi yeniden doğrulaman ya da veriyi Data Protection ile koruman gerekir.

Üretimde Dikkat

Session verisi bir cache'te, veritabanında değil. Redis yeniden başlarsa ya da IdleTimeout dolarsa taslak gider. Kurumsal sipariş gibi emek isteyen bir formda, kullanıcının "taslağı kaydet" diyebileceği bir veritabanı kaydı daha doğru olabilir. Session'ı vazgeçilebilir veri için kullan.

Session store'a ulaşılamazsa davranış iki türlü. LoadAsync exception fırlatıyor; bu yüzden Bookstore.Web'de istek 503 sayfasına düşüyor. Middleware'in isteğin sonunda yaptığı commit başarısız olursa ise yalnız loglanıyor ve istek başarılı görünüyor. Dokümana göre yazmanın gerçekten kalıcı olduğundan emin olmak istiyorsan await HttpContext.Session.CommitAsync() ile hatayı kendin yakalamalısın. Redis istemcisinin varsayılan zaman aşımları (log'daki 5000 ms) Redis yokken bir isteği 20 saniyeden fazla bekletebiliyor; bağlantı ayarlarını uygulamanın beklentisine göre kısalt.

Data Protection key'lerini paylaşılan, kalıcı ve şifreli bir yere koy. Her container'ın kendi key'ini üretmesi, yeni bir deploy'da bütün sepetlerin, oturumların ve açık formların boşalması demek. Log'daki "No XML encryptor configured" uyarısı key'lerin diske şifresiz yazıldığını söylüyor.

Cookie boyutunu izle. TempData cookie provider'ı veriyi şifreleyip birden fazla cookie'ye bölebiliyor, ama her bölüm her istekte gidiyor. Büyük bir cookie, sayfadaki her CSS ve resim isteğini de büyütüyor. Bu bölümde resmî belgelerden yararlanılmıştır.

Sık Düşülen Tuzaklar

Session'a büyük object koymak. Session'daki her şey her istekte store'dan okunuyor ve değiştiyse geri yazılıyor. Bookstore'un taslağı 208 byte; kitap listesinin tamamını, kullanıcının sipariş geçmişini ya da bir Entity Framework Core entity'sini koymak her adımı yavaşlatır. Entity koymak ayrıca navigation property'ler yüzünden serialization döngüsüne de girebilir.

Bellek içi session ile çok instance. Önce Tahmin Et'teki gibi hata yok, sadece kaybolan veri var. Sticky session sorunu gizler ama çözmez: instance yeniden başladığında ya da deploy sırasında veri yine gider.

TempData'ya karmaşık tip koymak. İkinci sorunun cevabı: kod derleniyor, çalışınca da şu exception geliyor:

InvalidOperationException: The 'Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure.DefaultTempDataSerializer' cannot serialize an object of type 'Bookstore.Web.Institutional.InstitutionalOrderDraft'.

Daha kötüsü zamanlaması: TempData action bittikten sonra, result çalışırken kaydediliyor. Sipariş orders.Submit ile çoktan kaydedilmişti, kullanıcı ise 500 gördü. Varsayılan serializer'ın kaynak koduna göre TempData'ya yalnız string, int, bool, DateTime, Guid, enum, int ve string collection'ları ile IDictionary<string, string> girebiliyor. decimal bile giremiyor: TempData["Total"] = 7272.00m denemesi aynı exception'ı System.Decimal için verdi. Daha fazlası gerekiyorsa id'yi koy, veriyi kaynağından oku.

Session'ı kimlik doğrulama yerine kullanmak. "Session'da UserId var, kullanıcı giriş yapmış" yaklaşımı, cookie authentication'ın verdiği süre, yenileme ve iptal davranışlarını elle yeniden yazmak demek. Oturum için Authentication ve Authorization serisinin Cookie Authentication yazısındaki yolu kullan; session'ı form taslağı gibi geçici veriye bırak.

Küçük Bir Deneme

★☆☆ Bookstore.Web'de CreateProtector("Bookstore.Web.Cart.v1")'i v2 yap, uygulamayı yeniden başlat ve sepeti aç. Ne oldu, log'da hangi satır var? Format değişikliğinde eski cookie'leri okumak için ne yapardın?

★★☆ Kurumsal sipariş formunu session'sız, hidden field'larla yeniden yaz: 1. adımın değerleri 2. adımın formunda gizli alan olarak gitsin. Kullanıcının tarayıcıda kurum adını ya da adetleri değiştirip değiştiremediğini dene ve 3. adımda neyi yeniden doğrulaman gerektiğini listele.

★★★ İki instance'ı Redis ile çalıştır ve taslağın 1. adımını yazdıktan sonra Redis container'ını yeniden başlat. Taslak kayboldu mu? Ardından taslağı veritabanına kaydeden bir "Save draft" butonu ekle ve session'ı yalnız formun o anki adımı için kullan.

Bu yazıyla ASP.NET Core MVC ve Razor Pages serisini kapatıyoruz. MVC pattern'inden başlayıp Razor syntax'ına, partial view ve view component'lere, tag helper'lara, formlara, validation'a, Razor Pages'e, static asset'lere, localization'a, htmx'e, MVC 5'ten geçişe, sürüm farklarına, hata sayfalarına ve request'ler arası state'e kadar server-side render edilen bir uygulamanın production'da ihtiyaç duyduğu parçaları Bookstore üzerinde kurduk. Buradan sonra aynı sunucu tarafı yaklaşımı component modeliyle sürdürmek istersen Frontend kategorisindeki Blazor serisi, oturum ve yetki tarafını derinleştirmek istersen Authentication ve Authorization serisi doğal devam noktaları.