<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hüseyin Bozdemir</title>
    <description>The latest articles on DEV Community by Hüseyin Bozdemir (@huseyinbozdemir).</description>
    <link>https://dev.to/huseyinbozdemir</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203045%2Fb999a946-4993-4f06-b649-22fc41a602ff.jpg</url>
      <title>DEV Community: Hüseyin Bozdemir</title>
      <link>https://dev.to/huseyinbozdemir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/huseyinbozdemir"/>
    <language>en</language>
    <item>
      <title>Ön provizyon nedir, kart neden bloke edilir?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Thu, 12 Nov 2026 08:21:26 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/on-provizyon-nedir-kart-neden-bloke-edilir-1j87</link>
      <guid>https://dev.to/huseyinbozdemir/on-provizyon-nedir-kart-neden-bloke-edilir-1j87</guid>
      <description>&lt;p&gt;Online marketten kiloyla satılan ürünler sipariş etmiştim. Sepet tutarı belliydi ama kartımda ondan fazlası bloke edilmiş görünüyordu. Fazlası ertesi gün geri geldi.&lt;/p&gt;

&lt;p&gt;Bunun adı ön provizyon (pre-authorization). İşyeri son tutarı henüz bilmiyorsa kartından tahmini bir tutarı bloke ettirir, kesin tutarı sonradan ayrı bir işlemle çeker. Aradaki fark kartına geri döner. 500 ₺'lik bir sepette 600 ₺ bloke edilir, ürünler tartılınca 540 ₺ çekilir, kalan 60 ₺ serbest kalır.&lt;/p&gt;

&lt;p&gt;Bu akışı sanal POS entegrasyonlarında ben de yazdım. Kullananlar çoğunlukla akaryakıt işyerleri ve kiloyla satış yapan gıda işyerleriydi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutar neden baştan bilinmiyor?
&lt;/h2&gt;

&lt;p&gt;Çoğu alışverişte tutar ödeme anında bellidir. İşyeri bankaya sorar, onay gelir, para çekilir. Bazı satışlarda ise fiyat ödemeden sonra kesinleşir.&lt;/p&gt;

&lt;p&gt;Tartılı üründe bir kilo domates istersin, poşete 1.080 gram girer. Fiyat tartıdan sonra belli olur, tartı da sen ödemeyi yaptıktan sonra yapılır.&lt;/p&gt;

&lt;p&gt;Akaryakıtta pompa, kaç liralık yakıt alacağını dolum bitene kadar bilmez. Kart çıkaran tarafın gözünden bu akışı &lt;a href="https://docs.stripe.com/issuing/purchases/authorizations" rel="noopener noreferrer"&gt;Stripe'ın dokümanı&lt;/a&gt; anlatıyor: pompa önce küçük bir tutarla kartı sorar, banka bilinmeyen tutarı karşılamak için daha yüksek bir tutarı ayırır, dolum bitince gerçek tutar bildirilir. Oradaki rakamlar ABD'ye göre, akış aynı.&lt;/p&gt;

&lt;p&gt;Otelde ve araç kiralamada da durum benzer. Minibar, ek gece ya da hasar çıkışta belli olur. Otel konaklama sürerken ek provizyon da isteyebilir.&lt;/p&gt;

&lt;p&gt;Üçünde de işyeri parayı garantiye almak ister ama ne kadar olacağını bilmez. O yüzden biraz fazlasını ayırtır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bloke ve tahsilat ayrı işlemler
&lt;/h2&gt;

&lt;p&gt;Ön provizyonda banka parayı işyerine göndermez. Yalnızca kartının kullanılabilir tutarından o kadarını ayırır. Sanal POS dokümanlarında bu işlem tipi &lt;code&gt;PreAuth&lt;/code&gt; diye geçer. Para, işyeri ikinci işlemi gönderince hareket eder. Onun adı provizyon kapama (&lt;code&gt;PostAuth&lt;/code&gt;, İngilizce kaynaklarda capture).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sipariş anında banka 600 ₺'yi ayırır ama işyerine göndermez. Ürünler tartılınca işyeri 540 ₺ için kapama gönderir; kalan 60 ₺ kartın kullanılabilir tutarına geri döner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Bizim yazdığımız akışta kapama, ürün tartıldıktan sonra kilo hesabından çıkan net tutarla gönderiliyordu.&lt;/p&gt;

&lt;p&gt;Blokenin baştan yüksek tutulmasının sebebi de kapama. İşyeri ayırttığından fazlasını çekmeye kalkarsa sorun çıkar: &lt;a href="https://usa.visa.com/content/dam/VCOM/regional/na/us/support-legal/documents/authorization-framework-will-be-updated-to-simplify-authorization-processing-time-frames.pdf" rel="noopener noreferrer"&gt;Visa'nın duyurusunda&lt;/a&gt; provizyondan yüksek tutarla işlenen işlem, kartın bankasının itiraz edebileceği durumlar arasında sayılıyor. Ürün ağır gelirse diye pay baştan bırakılır.&lt;/p&gt;

&lt;p&gt;Kartın türü burada fark ediyor. Kredi kartında bloke limitini düşürür. Banka kartında ise hesaptaki paranın kendisi ayrılır, fazla bloke o yüzden daha çok can yakar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fark ne zaman geri gelir?
&lt;/h2&gt;

&lt;p&gt;En olağan yol kapamanın gelmesi. İşyeri gerçek tutarı gönderir, banka onu çeker ve kalanını bırakır. Benim siparişimde bu ertesi gün oldu.&lt;/p&gt;

&lt;p&gt;İkinci yol kısmi iptal (partial reversal). İşyeri kapamayı beklemeden "şu kadarına ihtiyacım kalmadı" der ve fazla tutar hemen serbest kalır.&lt;/p&gt;

&lt;p&gt;İkisi de gelmezse bloke, süresi dolana kadar durur. Bu süre için elimdeki tek birinci el kaynak Visa'nın ABD'deki üye işyeri bankalarına yazdığı duyuru. Ona göre Nisan 2024'ten beri pompadaki ön provizyon için iki saat içinde kapama ya da iptal gönderilmesi gerekiyor. Tahmini tutarla alınan otel ve araç kiralama provizyonlarında süre 30 gün, kart sahibinin başlattığı internet alışverişinde 10 gün.&lt;/p&gt;

&lt;p&gt;Türkiye'deki bankalar için böyle bir belge bulamadım. Baktığım bir &lt;a href="https://www.akbank.com/blog/provizyon-bekleyen-odeme-ne-demek" rel="noopener noreferrer"&gt;banka rehberi&lt;/a&gt; bekleyen ödemenin karta 1-3 iş günü içinde yansıdığını, sorun çıkarsa bunun bir haftayı bulabildiğini yazıyor. Kapaması hiç gelmeyen bir ön provizyonun ne zaman düşeceğini yazmıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapama hiç gelmezse
&lt;/h2&gt;

&lt;p&gt;Açık kalan ön provizyonların çoğu teknik bir hatadan kalmıyordu. İşyeri kapamayı göndermeyi unutuyordu.&lt;/p&gt;

&lt;p&gt;O durumda blokeyi banka kendisi iptal ediyordu. Ne zaman iptal edeceği bankadan bankaya değişiyordu, kabaca bir ayı buluyordu.&lt;/p&gt;

&lt;p&gt;Kart sahibi için bu, bir ay boyunca kullanamadığı bir para demek. İşyeri de kazançlı çıkmıyor. Kapama gitmediyse para da alınmamıştır ve süre dolduktan sonra o provizyonla tahsilat yapılamaz.&lt;/p&gt;

&lt;p&gt;Biz ödemelerin akıbetini bankadan sürekli sorguluyorduk. Banka bir ön provizyonu iptale aldıysa bunu sorgudan görüyor, kaydı kendi tarafımızda da otomatik olarak iptale çekiyorduk. Böylece bizdeki durum bankadakiyle aynı kalıyordu. Ne zaman iptale düşeceği yine bankaya göre değişiyordu.&lt;/p&gt;

&lt;p&gt;Entegrasyonu sen yazıyorsan açık ön provizyonu "bir gün kapanır" diye bırakma. Durumunu bankaya sor ve kendi kaydını ona göre güncelle. Yoksa banka blokeyi çoktan bırakmışken senin sisteminde o ödeme hâlâ bekliyor görünür.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paran bloke kaldıysa
&lt;/h2&gt;

&lt;p&gt;Önce bankanın uygulamasında bekleyen işlemlere bak. Tutar orada "provizyon" ya da "bekleyen" diye duruyorsa henüz çekilmemiştir.&lt;/p&gt;

&lt;p&gt;Sonra işyerine yaz. Kapamayı da iptali de onlar gönderir, banka kendiliğinden ancak süre dolunca bırakır. İşyeri iptal ettiğini söylüyor ama bloke hâlâ duruyorsa sıra bankada.&lt;/p&gt;

&lt;p&gt;Benzer bir bekleme &lt;a href="https://huseyinbozdemir.com/blog/cevrimdisi-kart-odeme" rel="noopener noreferrer"&gt;çevrimdışı kart ödemesinde&lt;/a&gt; de var. Orada onay sonradan gelir, burada tutar.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak huseyinbozdemir.com'da yayımlandı: &lt;a href="https://huseyinbozdemir.com/blog/on-provizyon-nedir" rel="noopener noreferrer"&gt;https://huseyinbozdemir.com/blog/on-provizyon-nedir&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>payments</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Claude Fable 5.2 nedir? Fiyat, limit, farklar</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Tue, 10 Nov 2026 20:14:26 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/claude-fable-52-nedir-fiyat-limit-farklar-1m0e</link>
      <guid>https://dev.to/huseyinbozdemir/claude-fable-52-nedir-fiyat-limit-farklar-1m0e</guid>
      <description>&lt;p&gt;9 Kasım 2026'da Anthropic iki modeli birlikte yayımladı: Claude Fable 5.2 ve Claude Mythos 5.2. Fable 5.2, Anthropic'in herkese açık en yetenekli modeli; &lt;a href="https://huseyinbozdemir.com/blog/loop-engineering-nedir" rel="noopener noreferrer"&gt;saatlerce süren ajan işleri&lt;/a&gt; ve zor akıl yürütme için konumlanıyor. &lt;a href="https://www.anthropic.com/claude-fable-and-mythos-5-2" rel="noopener noreferrer"&gt;Duyuru&lt;/a&gt; benchmark tablolarıyla dolu. API'yi kendi kodundan çağırıyorsan asıl okuman gereken sayfa ise &lt;a href="https://platform.claude.com/docs/en/models/fable-5-2/whats-new-fable-5-2" rel="noopener noreferrer"&gt;geçiş notları&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Çünkü model adını &lt;code&gt;claude-fable-5-1&lt;/code&gt;'den &lt;code&gt;claude-fable-5-2&lt;/code&gt;'ye çevirmek her zaman yetmiyor. Dört değişiklik çalışan kodu bozabiliyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fable 5.2 ile Mythos 5.2 arasındaki fark ne?
&lt;/h2&gt;

&lt;p&gt;İkisi aynı model. Fark önlerindeki güvenlik katmanında: Fable'da siber güvenlik ve biyoloji için sınıflandırıcılar çalışıyor. Mythos bu korumaların bir kısmı olmadan, yalnızca Anthropic'in doğrulama programlarından geçmiş kuruluşlara açık. Fiyat ve teknik özellikler aynı: 1 milyon token bağlam, 128 bin token çıktı.&lt;/p&gt;

&lt;p&gt;Duyuruya göre Fable'ın sınıflandırıcıları sızma testi, exploit üretimi ve derlenmiş dosya üzerinden zafiyet analizi gibi işleri engelliyor. Engellenen sorguya siber güvenlikte Opus 4.8, biyolojide Opus 5 cevap veriyor. Anthropic, Fable 5.1'e göre engellemelerin yaklaşık %55 azaldığını söylüyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ret, HTTP 200 ve &lt;code&gt;stop_reason: "refusal"&lt;/code&gt; ile gelir. Claude uygulamalarında başka modele düşme kendiliğinden olur; API'de yeniden denemeyi sen açarsın.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Duyuruda alışılmadık bir ayrıntı daha var. Anthropic, eğitim sırasında yapay zekâ geliştirmeyi doğrudan hızlandırabilecek araştırma yeteneklerindeki ilerlemeyi bilerek sınırladığını yazıyor. Verdiği örnekler hızlandırıcı (GPU benzeri donanım) kernel optimizasyonu ve dil modeli mimarisini iyileştirme. Bu alanlarda model Opus 5.5 düzeyinde kalıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fable 5.2 fiyatı ve hızı
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Milyon token fiyatı girdide 8, çıktıda 40 dolar. Fable 5.1'de 10 ve 50 dolardı.&lt;/li&gt;
&lt;li&gt;Çıktı üretimi 5.1'e göre %30'a kadar hızlı.&lt;/li&gt;
&lt;li&gt;Fast mode çıktıyı 2,5 kata kadar hızlandırıyor, fiyatı iki katı: 16 ve 80 dolar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic'in kendi ailesinde Fable en pahalı katman: Opus 5.5 milyon token başına 4 ve 20, Sonnet 5.5 ise 2 ve 10 dolar. &lt;a href="https://platform.claude.com/docs/en/about-claude/models/choosing-a-model" rel="noopener noreferrer"&gt;Model seçimi&lt;/a&gt; önerisi de buna göre: çoğu işte önce Opus'la başla, Opus yüksek effort'ta bile senin ölçümlerinde yetmiyorsa Fable'a geç.&lt;/p&gt;

&lt;h2&gt;
  
  
  GPT-6 Astra ve Gemini 4 Carbon ile karşılaştırma
&lt;/h2&gt;

&lt;p&gt;Duyurudaki tablo Fable 5.2'yi bir önceki sürümle, Opus 5.5'le ve iki rakiple yan yana koyuyor: OpenAI'ın GPT-6 Astra'sı ve Google'ın Gemini 4 Carbon'u. Tablonun bir kısmı:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ölçüm&lt;/th&gt;
&lt;th&gt;Fable 5.2&lt;/th&gt;
&lt;th&gt;Fable 5.1&lt;/th&gt;
&lt;th&gt;Opus 5.5&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;th&gt;Gemini 4 Carbon&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FrontierCode 2.0 (ajanla kodlama)&lt;/td&gt;
&lt;td&gt;%49,1&lt;/td&gt;
&lt;td&gt;%32,4&lt;/td&gt;
&lt;td&gt;%45,5&lt;/td&gt;
&lt;td&gt;%42,8&lt;/td&gt;
&lt;td&gt;%41,1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FrontierSWE v2 (ajanla kodlama)&lt;/td&gt;
&lt;td&gt;%69,9&lt;/td&gt;
&lt;td&gt;%56,3&lt;/td&gt;
&lt;td&gt;%62,3&lt;/td&gt;
&lt;td&gt;%65,5&lt;/td&gt;
&lt;td&gt;%61,2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutomationBench (iş akışları)&lt;/td&gt;
&lt;td&gt;%51,4&lt;/td&gt;
&lt;td&gt;%31,4&lt;/td&gt;
&lt;td&gt;%42,5&lt;/td&gt;
&lt;td&gt;%41,4&lt;/td&gt;
&lt;td&gt;%53,9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSWorld 2.1 (bilgisayar kullanımı)&lt;/td&gt;
&lt;td&gt;%89,3&lt;/td&gt;
&lt;td&gt;%83,5&lt;/td&gt;
&lt;td&gt;%87,2&lt;/td&gt;
&lt;td&gt;%79,5&lt;/td&gt;
&lt;td&gt;%81,5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote Labor Index (bilgi işi)&lt;/td&gt;
&lt;td&gt;%41,3&lt;/td&gt;
&lt;td&gt;%17,9&lt;/td&gt;
&lt;td&gt;%21,3&lt;/td&gt;
&lt;td&gt;%20,8&lt;/td&gt;
&lt;td&gt;yok&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humanity's Last Exam (araçlarla)&lt;/td&gt;
&lt;td&gt;%74,2&lt;/td&gt;
&lt;td&gt;%65,6&lt;/td&gt;
&lt;td&gt;%67,7&lt;/td&gt;
&lt;td&gt;%57,2&lt;/td&gt;
&lt;td&gt;yok&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chartography (grafik okuma)&lt;/td&gt;
&lt;td&gt;%73,4&lt;/td&gt;
&lt;td&gt;%44,8&lt;/td&gt;
&lt;td&gt;%64,4&lt;/td&gt;
&lt;td&gt;%71,0&lt;/td&gt;
&lt;td&gt;%72,5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fable 5.2 her satırda birinci değil. AutomationBench'te Gemini 4 Carbon önde, bilgi işini ölçen AA-Briefcase'in Elo sıralamasında da Opus 5.5 (1800) Fable 5.2'nin (1785) üstünde.&lt;/p&gt;

&lt;p&gt;Maliyet tarafında öne çıkarılan sonuç FrontierCode 2.0'dan: Fable 5.2 &lt;code&gt;high&lt;/code&gt; effort'ta %45,3 alıyor. Bu, Opus 5.5'in en iyi skoruyla (%45,5) neredeyse aynı ve görev başına maliyeti yaklaşık yarısı. Tablodaki %49,1 ise &lt;code&gt;xhigh&lt;/code&gt; effort'un sonucu.&lt;/p&gt;

&lt;p&gt;Bir not: ölçümlerin hepsini Anthropic yaptı ve modeller aynı ayarda koşmadı (FrontierCode'da Gemini &lt;code&gt;medium&lt;/code&gt;, Fable &lt;code&gt;xhigh&lt;/code&gt; effort'ta). Bağımsız ölçümler gelene kadar tabloyu şirketin kendi beyanı olarak oku.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limit sıfırlama (reset) hakkı
&lt;/h2&gt;

&lt;p&gt;Lansmanla birlikte ücretli abonelere "Fable reset" hakkı verildi. &lt;a href="https://support.claude.com/en/articles/17506494" rel="noopener noreferrer"&gt;Kampanya sayfasına&lt;/a&gt; göre kurallar şöyle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pro ve Team standart koltuk:&lt;/strong&gt; 9-16 Kasım arasında haftalık limitin %50'sine kadar Fable kullanımı plana dahil, ayrıca bir reset hakkı var. İkisi de 16 Kasım 23:59'da (Pasifik saati; Türkiye'de 17 Kasım 10:59) bitiyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max, Ultra ve Team premium koltuk:&lt;/strong&gt; abonelik süresine göre bir ile üç arası reset. 6 aydan kısa abonelikte bir, 6-12 ayda iki, 12 aydan uzunda üç. Son kullanma tarihi 8 Mart 2027.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kapsam dışı:&lt;/strong&gt; ücretsiz plan ve kullanım bazlı Enterprise planları.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reset limiti büyütmüyor, harcananı geri veriyor. Haftalık limitte yalnızca Fable modelleriyle harcadığın kısım geri geliyor. Beş saatlik pencere ise diğer modeller dahil tamamen yenileniyor. Limitin dolmasını beklemen gerekmiyor ama kullandıktan sonra geri alamıyorsun.&lt;/p&gt;

&lt;p&gt;Hak kazanmak için 9 Kasım 10:00'dan (Pasifik saati; Türkiye'de 21:00) önce ücretli abone olman gerekiyordu. Reset, Claude'da Ayarlar → Kullanım sayfasındaki "Limit resets" bölümünden "Reset for free" ile kullanılıyor. Sayfanın uyarısı da var: Fable modelleri limiti diğer modellerden daha hızlı tüketiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  5.1'den geçerken ne kırılır?
&lt;/h2&gt;

&lt;p&gt;Geçiş notları dört kırıcı değişiklik sayıyor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Geçmişi düzenleyen kod.&lt;/strong&gt; Model her düşünme bloğunu (thinking block) kendinden önceki içeriğe bağlıyor: &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt; ve önceki mesajlar. Bloğu geri gönderirken bunlardan biri değişmişse istek 400 dönüyor. Fable 5.1'de bu denetim yalnızca 31 Ağustos 2026 ve sonrasında açılan hesaplarda vardı, 5.2'de her hesapta var.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Konuşma ortasında model değiştirmek.&lt;/strong&gt; Fable 5.2'nin düşünme bloklarını yalnızca Fable 5.2 ve Mythos 5.2 okuyabiliyor. Konuşmayı Opus 5.5'e ya da 5.1'e taşırsan istek başarılı olur ama o bloklar düşer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eski computer use aracı.&lt;/strong&gt; &lt;code&gt;computer_20251124&lt;/code&gt; Bedrock dışında 400 dönüyor, yerine &lt;code&gt;computer_toolset_20260801&lt;/code&gt; geliyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisor aracı.&lt;/strong&gt; Yürüten model Fable 5.2 ise danışman da Fable 5.2 ya da Mythos 5.2 olmak zorunda.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En sinsisi ilki, çünkü eski bir hesapta 5.1 ile sorunsuz çalışan kod model adını değiştirdiğin anda kırılabiliyor. "Geçmişi düzenlemek" de kulağa geldiğinden geniş. &lt;a href="https://platform.claude.com/docs/en/build-with-claude/preserved-thinking" rel="noopener noreferrer"&gt;Doküman&lt;/a&gt; şunları sayıyor: system prompt'a her istekte bugünün tarihini yeniden yazmak, eski araç sonuçlarını kısaltmak, eski turları istemci tarafında özetlemek, &lt;code&gt;tools&lt;/code&gt; listesine araç ekleyip çıkarmak. Hata şöyle geliyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid `signature` in `thinking` block. The block is bound to a different conversation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Geçici çıkış beta'daki &lt;code&gt;prefix_mismatch_behavior: "drop_block"&lt;/code&gt; ayarı: bozulan bloklar atılır, istek geçer. Bedeli var; model okuyamadığı akıl yürütmeyi yeniden üretir, bu da daha çok çıktı token'ı ve daha uzun yanıt demek. Kalıcı çözüm geçmişe yalnızca ekleme yapmak.&lt;/p&gt;

&lt;p&gt;Opus 5 ya da daha eski bir Opus'tan geliyorsan iki kural daha var: düşünme kapatılamıyor, &lt;code&gt;tool_choice&lt;/code&gt; ile aracı zorlamak (&lt;code&gt;any&lt;/code&gt; ya da &lt;code&gt;tool&lt;/code&gt;) 400 dönüyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ret hata değil: HTTP 200 döner
&lt;/h2&gt;

&lt;p&gt;Sınıflandırıcı bir isteği reddettiğinde hata kodu gelmiyor. Yanıt başarılı, içerik boş (&lt;a href="https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback" rel="noopener noreferrer"&gt;dokümandaki&lt;/a&gt; örneğin kısaltılmışı):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"stop_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"refusal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"stop_details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"refusal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"cyber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"This request was declined because it could enable cyber harm."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bunun iki sonucu var. İzlemen hata oranına ya da 5xx'e bakıyorsa retleri hiç görmez; &lt;code&gt;stop_reason&lt;/code&gt; değerini ayrı bir metrik olarak sayman gerekir. İkincisi, API'de başka modele düşme kendiliğinden olmuyor. Ya &lt;code&gt;fallbacks&lt;/code&gt; parametresini (beta) açarsın, ya SDK'nın hazır ara katmanını kullanırsın, ya da yeniden denemeyi kendin yazarsın. Fable 5.2 için izin verilen hedefler Opus 4.8 ve Opus 5.&lt;/p&gt;

&lt;p&gt;Zararsız iş de takılabiliyor. Dokümandaki kategori tablosu bunu açıkça yazıyor: iyi niyetli bir güvenlik çalışması &lt;code&gt;cyber&lt;/code&gt;, sıradan bir makine öğrenmesi işi &lt;code&gt;frontier_llm&lt;/code&gt; reddi alabilir. Prompt'unda modelden akıl yürütmesini çıktıya yazmasını istiyorsan (bir &lt;code&gt;&amp;lt;thinking&amp;gt;&lt;/code&gt; bölümü, JSON'da &lt;code&gt;reasoning&lt;/code&gt; alanı) o da &lt;code&gt;reasoning_extraction&lt;/code&gt; reddine yol açıyor. Bu kategorinin fallback'i yok, prompt'u değiştirmen gerekiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geçmeden önce neye bakmalı?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;messages&lt;/code&gt; dizisini kendi kodun mu kuruyor? Öyleyse geçmişe dokunan her yeri bul: tarih, özetleme, araç sonucu kırpma.&lt;/li&gt;
&lt;li&gt;Konuşma ortasında model değiştiren bir router ya da fallback var mı?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stop_reason: "refusal"&lt;/code&gt; log'da ve metriklerde ayrı görünüyor mu?&lt;/li&gt;
&lt;li&gt;Ölçümlerini &lt;code&gt;high&lt;/code&gt; effort'tan başlayarak yeniden koştur. Aynı işi Opus 5.5 daha ucuza yapıyor olabilir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;İstekleri Claude Code, claude.ai ya da Agent SDK kuruyorsa ilk iki maddeyle uğraşmana gerek yok.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/claude-fable-5-2" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>anthropic</category>
      <category>llm</category>
    </item>
    <item>
      <title>Loop engineering nedir, her işe değer mi?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Tue, 10 Nov 2026 20:14:07 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/loop-engineering-nedir-her-ise-deger-mi-4690</link>
      <guid>https://dev.to/huseyinbozdemir/loop-engineering-nedir-her-ise-deger-mi-4690</guid>
      <description>&lt;p&gt;Prod'a doğrudan çıkmayacak hemen her işi yapay zekâya yaptırıyorum. Önce mimari incelemesini yaptırıyorum, sonra çıktıyı kendim inceliyorum, uygunsa prod'a gönderiyorum.&lt;/p&gt;

&lt;p&gt;Bu akışta işi yine ben başlatıyorum ve sonucu yine ben okuyorum. Bu yıl çok konuşulan loop engineering ise aradaki adımların büyük kısmını ajanın çevresine kurulan bir sisteme devretmeyi anlatıyor. Türkçe kaynağı az olduğu için önce kavramı netleştireyim, sonra sınırı nereye koyduğumu anlatayım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loop engineering nedir?
&lt;/h2&gt;

&lt;p&gt;Loop engineering (döngü mühendisliği), yapay zekâ ajanına her adımı tek tek söylemek yerine onu çalıştıran, sonucunu kontrol eden ve gerekirse yeniden çalıştıran sistemi tasarlamaktır. Terim Haziran 2026'da yayıldı. Sık anılan metinlerden biri &lt;a href="https://addyosmani.com/blog/loop-engineering/" rel="noopener noreferrer"&gt;Addy Osmani'nin 7 Haziran tarihli yazısı&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Osmani'nin örneği konuyu iyi anlatıyor. Her sabah bir otomasyon CI hatalarını ve açık issue'ları okuyor. Uygun bulduğu iş için ayrı bir çalışma kopyası (worktree) açılıyor, bir ajan düzeltmeyi hazırlıyor, ikinci bir ajan da onu inceliyor. Geliştirici bu adımları tek tek istemiyor.&lt;/p&gt;

&lt;p&gt;Alışkın olduğumuz akışta prompt yazar, çıktıyı okur ve sıradaki adımı yine sen yazarsın. Döngüde bu turu bir sistem döndürür. Kabaca şöyle görünür:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hedef: bütün testler geçsin
tur sınırı: 5

her turda:
  ajan işi yapar
  testler çalışır
  geçtiyse: dur, insana gönder
  geçmediyse: hatayı ajana ver

sınır dolduysa: dur, haber ver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terim, "agent loop" ile sık karıştırılıyor. &lt;a href="https://developers.google.com/machine-learning/glossary/agent?hl=tr" rel="noopener noreferrer"&gt;Google'ın sözlüğü&lt;/a&gt; agent döngüsünü, ajanın bir sonlandırma koşulu karşılanana kadar tekrar tekrar ilerlediği döngü olarak tanımlıyor. Yani agent loop ajanın kendi çalışma turu. Loop engineering ise bu turların ne zaman başlayacağını, nasıl kontrol edileceğini ve ne zaman duracağını tasarlama işi.&lt;/p&gt;

&lt;p&gt;Kavramın yeni olup olmadığı da tartışılıyor. &lt;a href="https://hackernoon.com/lang/tr/loop-engineerings-dirty-secret" rel="noopener noreferrer"&gt;HackerNoon'daki bir yazı&lt;/a&gt; bunun Kent Beck'in kırmızı, yeşil, refactor döngüsünün ajana devredilmiş hali olduğunu savunuyor. Benzerlik gerçek: ikisinde de işi döndüren şey, geçip geçmediği ölçülebilen bir hedef.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt, context, harness ve loop farkı
&lt;/h2&gt;

&lt;p&gt;Bu dört terim sık sık birbirine karışıyor. &lt;a href="https://machinelearningmastery.com/an-introduction-to-loop-engineering/" rel="noopener noreferrer"&gt;Machine Learning Mastery'nin girişi&lt;/a&gt; bunları iç içe katmanlar olarak sıralıyor. Her katman öncekini içeriyor, yerine geçmiyor.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Katman&lt;/th&gt;
&lt;th&gt;Neyi tasarlarsın?&lt;/th&gt;
&lt;th&gt;Örnek&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt&lt;/td&gt;
&lt;td&gt;Modele verdiğin talimatın kendisini&lt;/td&gt;
&lt;td&gt;"Bu metodu sadeleştir"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;Modelin yanıt verirken gördüğü her şeyi&lt;/td&gt;
&lt;td&gt;Konuşma geçmişi, belgeler, araç çıktıları&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;Ajanın içinde çalıştığı düzeni&lt;/td&gt;
&lt;td&gt;Kural dosyaları, araçlar, kısıtlar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop&lt;/td&gt;
&lt;td&gt;Ajanın ne zaman çalışıp ne zaman duracağını&lt;/td&gt;
&lt;td&gt;Zamanlayıcı, kontrol adımı, durma koşulu&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Context için &lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;Anthropic'in Eylül 2025 yazısı&lt;/a&gt;, harness için &lt;a href="https://mitchellh.com/writing/my-ai-adoption-journey" rel="noopener noreferrer"&gt;Mitchell Hashimoto'nun Şubat 2026 yazısı&lt;/a&gt; iyi başlangıç noktaları. Hashimoto'nun tarifi akılda kalıcı: ajan bir hata yaptığında, o hatayı bir daha yapmayacağı bir düzen kurmak. Ama tanımlar henüz oturmadı ve kaynaktan kaynağa değişiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bir döngünün parçaları
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Tetikleyici döngüyü başlatır. Ajan işi yapar, sonuç kontrol edilir, durum kaydedilir; iş bitmediyse tur başa döner. Döngü bitince sonuç prod'a doğrudan gitmez, önce insan inceler.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Kaynaklar küçük farklarla aynı parçaları sayıyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tetikleyici:&lt;/strong&gt; Döngüyü ne başlatıyor? Bir zamanlayıcı, kırılan bir CI işi ya da yeni açılan bir issue olabilir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test edilebilir hedef:&lt;/strong&gt; "Bitti" neye göre bitti? Hedef kötü yazılırsa döngü yanlış şeyi hızla iyileştirir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kontrol:&lt;/strong&gt; Test ve derleyici gibi aynı girdide aynı sonucu veren araçlar önce gelir. Osmani yazan ile kontrol edeni ayırmayı öneriyor, çünkü kendi ödevini notlandıran model fazla iyimser oluyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durma koşulu:&lt;/strong&gt; Tur sınırı, bütçe ve ilerleme olmadığını fark eden bir kontrol. Bunlar yoksa döngü token yakmaya devam eder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hafıza:&lt;/strong&gt; Model iki çalışma arasında her şeyi unutur. Neyin bittiğini ve sırada ne olduğunu konuşmanın dışında, bir dosyada ya da panoda tutman gerekir.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ne zaman işe yarar, ne zaman yaramaz?
&lt;/h2&gt;

&lt;p&gt;İyi aday, tekrar eden ve sonucu makineyle kontrol edilebilen iştir. Osmani'nin örneğindeki CI hataları buna uyuyor: sorun bellidir, düzeltme testle sınanır.&lt;/p&gt;

&lt;p&gt;Tek seferlik işte durum tersine döner. Machine Learning Mastery, böyle bir işte normal bir ajan oturumunun daha hızlı ve daha güvenli olabileceğini yazıyor. &lt;a href="https://www.ssdnodes.com/learn/lang/tr/what-is-loop-engineering" rel="noopener noreferrer"&gt;SSD Nodes'un rehberi&lt;/a&gt; de ucu açık refactor ve tasarım işlerini henüz uygun görmüyor.&lt;/p&gt;

&lt;p&gt;Kontrol adımı da tek başına güvence değil. Machine Learning Mastery'nin verdiği hata örneği, ajanın kırık testi silerek CI'ı yeşile çevirmesi. Ben de &lt;a href="https://huseyinbozdemir.com/blog/ai-unit-test-mutation-testing" rel="noopener noreferrer"&gt;AI'ın yazdığı testleri ölçerken&lt;/a&gt; kapsam ve mutation skoru %100 çıktığı halde hatanın kaçabildiğini görmüştüm. Yeşil bir sonuç, kontrolün baktığı kadarını söyler.&lt;/p&gt;

&lt;p&gt;Faydası da henüz net ölçülmüş değil. &lt;a href="https://arxiv.org/abs/2608.21884" rel="noopener noreferrer"&gt;Ağustos'ta yayımlanan bir makale&lt;/a&gt; 36.710 açık kaynak depoyu tarayıp 217'sinde otonom ajan döngüsü buluyor. Aynı çalışmaya göre konuyu anlatan blog ve rehberler, iddia ettikleri faydalar için sistematik kanıt sunmuyor. Maliyet tarafında da Osmani, alt ajanların daha çok token harcadığını özellikle hatırlatıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prod kapısı neden insanda kalıyor?
&lt;/h2&gt;

&lt;p&gt;Benim sınırım prod. Hemen her işi yaptırırım ama sonucu incelemeden prod'a göndermem. Neyi incelediğim de projeye göre değişiyor. Backend'de bir iş yaptırıyorsam git diff'ten neler yaptığına bakıyorum ve istediğim mimaride mi diye inceliyorum.&lt;/p&gt;

&lt;p&gt;Testler kodun çalışıp çalışmadığını söyler, istediğin mimaride olup olmadığını söylemez. Bu yüzden diff'i okuyan birine hâlâ ihtiyaç var.&lt;/p&gt;

&lt;p&gt;Kaynaklar da bu noktada aynı yere çıkıyor. Osmani'ye göre döngünün "bitti" demesi kanıt değil, bir iddia. Üretilen kodu okumazsan sahip olduğun anlayış ile var olan kod arasındaki fark büyüyor; buna anlama borcu (comprehension debt) diyor. Machine Learning Mastery de finansal işlem ve veritabanına yazma gibi hassas adımlardan önce açık insan onayı öneriyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  İlk döngünü nasıl kurarsın?
&lt;/h2&gt;

&lt;p&gt;Küçük bir işle başla. "Bitti" tanımını çalıştırılabilir bir komut olarak yaz, örneğin bir test komutu. Tur sınırı koy ve döngünün sonucunu prod'a değil, bir PR olarak açtır. Birkaç PR'ı okuduktan sonra hangi kontrollerin eksik kaldığını görürsün.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://huseyinbozdemir.com/blog/anthropic-scion-git" rel="noopener noreferrer"&gt;Scion yazısında&lt;/a&gt; review yükünün arttığını yazmıştım. Döngü bir sabahta on PR açtığında bunları kimin okuyacağı ise henüz cevabı net olmayan bir soru.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/loop-engineering-nedir" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>.NET 11 çıkıyor, .NET 8 ve 9 bitiyor</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Tue, 10 Nov 2026 20:13:38 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/net-11-cikiyor-net-8-ve-9-bitiyor-1j51</link>
      <guid>https://dev.to/huseyinbozdemir/net-11-cikiyor-net-8-ve-9-bitiyor-1j51</guid>
      <description>&lt;p&gt;10 Kasım 2026 Salı, .NET tarafında üç şey aynı gün oluyor: .NET 8 ve .NET 9'un desteği bitiyor, &lt;a href="https://dotnet.microsoft.com/conf" rel="noopener noreferrer"&gt;.NET Conf 2026&lt;/a&gt; açılıyor ve .NET 11 çıkıyor. O gün ayrıca ayın ikinci salısı, yani Microsoft'un aylık yama günü (Patch Tuesday).&lt;/p&gt;

&lt;p&gt;Desteğin bitmesi uygulamanın durması demek değil. &lt;code&gt;net8.0&lt;/code&gt; hedefleyen bir servis 11 Kasım sabahı da çalışır. Değişen şey, o günden sonra bulunan bir açık için .NET 8 ve 9'a artık yama çıkmayacak olması. Microsoft'un &lt;a href="https://dotnet.microsoft.com/platform/support/policy/dotnet-core" rel="noopener noreferrer"&gt;destek politikasına&lt;/a&gt; göre 10 Kasım bu iki sürümün son yama günü.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neden 8 ve 9 aynı gün bitiyor?
&lt;/h2&gt;

&lt;p&gt;.NET 8 bir LTS (Long Term Support), .NET 9 ise bir STS (Standard Term Support) sürümü. İkisinin farkı yalnızca destek süresinde: &lt;a href="https://github.com/dotnet/core/blob/main/release-policies.md" rel="noopener noreferrer"&gt;sürüm politikası&lt;/a&gt; LTS'ye üç, STS'ye iki yıl veriyor.&lt;/p&gt;

&lt;p&gt;STS eskiden 18 ay desteklenirdi. Microsoft 16 Eylül 2025'te bu süreyi .NET 9'dan başlayarak 24 aya çıkardı. Gerekçe şuydu: .NET 9'un desteği 12 Mayıs 2026'da, kendisinden bir yıl önce çıkan .NET 8'den altı ay önce bitecekti. Yeni kuralla .NET 8 (Kasım 2023 + üç yıl) ve .NET 9 (Kasım 2024 + iki yıl) aynı güne denk geldi.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;LTS üç yıl, STS iki yıl destekleniyor. .NET 8 ve .NET 9 aynı gün, 10 Kasım 2026'da bitiyor. .NET 11 (STS) 9 Kasım 2028'e, .NET 10 (LTS) 14 Kasım 2028'e kadar destekleniyor: aradaki fark beş gün. Kaynak: Microsoft .NET destek politikası.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  .NET 11'de neler var?
&lt;/h2&gt;

&lt;p&gt;Microsoft'un &lt;a href="https://github.com/dotnet/docs/blob/main/docs/core/whats-new/dotnet-11/overview.md" rel="noopener noreferrer"&gt;.NET 11 yenilikleri&lt;/a&gt; listesi uzun. Backend tarafında öne çıkanlar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Async:&lt;/strong&gt; &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; için derleyicinin ürettiği durum makinesi yerine işi runtime'ın kendisi yürütüyor. Sonuç daha temiz stack trace ve daha az yük. &lt;code&gt;net11.0&lt;/code&gt; hedefleyen projelerde artık önizleme bayrağı istemiyor ve .NET'in kendi kütüphaneleri bununla derleniyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C# 15:&lt;/strong&gt; union tipleri ve kapalı sınıf hiyerarşileri (closed hierarchies). Bir metodun "başarılı sonuç ya da şu üç hatadan biri" döndürdüğünü tip sistemiyle anlatmak kolaylaşıyor. Etiketli &lt;code&gt;break&lt;/code&gt;/&lt;code&gt;continue&lt;/code&gt; ve extension indexer da geliyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decimal32, Decimal64, Decimal128:&lt;/strong&gt; IEEE 754 standardındaki ondalık kayan nokta tipleri. Mevcut &lt;code&gt;decimal&lt;/code&gt;'ın yerini almıyor; başka sistemlerle bu standartta veri alışverişi yapanlar için.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LINQ'te &lt;code&gt;FullJoin&lt;/code&gt;:&lt;/strong&gt; İki listeyi eşleştirip iki taraftan da eşi olmayanları getiren join artık hazır. İki kaynaktaki kayıtları karşılaştıran mutabakat kodu bunun tipik kullanım yeri.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asenkron doğrulama:&lt;/strong&gt; DataAnnotations'a &lt;code&gt;AsyncValidationAttribute&lt;/code&gt; geldi. Doğrulama sırasında veritabanına ya da bir servise gitmek gerekiyorsa senkron beklemek zorunda değilsin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP ve sıkıştırma:&lt;/strong&gt; Zstandard desteği, istek gövdesini sıkıştırma ve havuzdaki HTTP bağlantılarını kendi kuralınla kapatabilme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;İzleme:&lt;/strong&gt; &lt;code&gt;Activity&lt;/code&gt; izlemesini &lt;code&gt;ActivityListener&lt;/code&gt; yazmadan, kurallarla açıp kapatabiliyorsun; &lt;code&gt;MemoryCache&lt;/code&gt; için hazır OpenTelemetry metrikleri var.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK:&lt;/strong&gt; &lt;code&gt;dotnet nuget add source&lt;/code&gt; komutuna &lt;code&gt;--min-publish-age-hours&lt;/code&gt; geldi. Yeni yayımlanan bir paketin, belirlediğin süre dolmadan projeye girmesini engelliyor; zehirli sürümlere karşı basit bir tampon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Yükseltmeden önce: işlemci şartı
&lt;/h3&gt;

&lt;p&gt;.NET 11'in &lt;a href="https://github.com/dotnet/docs/blob/main/docs/core/whats-new/dotnet-11/runtime.md" rel="noopener noreferrer"&gt;en düşük donanım gereksinimi&lt;/a&gt; yükseldi. x64'te taban &lt;code&gt;x86-64-v1&lt;/code&gt;'den &lt;code&gt;x86-64-v2&lt;/code&gt;'ye çıktı; işlemcinin SSE4.2 ve POPCNT gibi komut setlerini desteklemesi gerekiyor. Intel ve AMD'nin hâlâ desteklediği bütün işlemciler bu şartı karşılıyor.&lt;/p&gt;

&lt;p&gt;Takılabileceğin yer eski sunucular ya da işlemci özelliklerini misafir makineye kısıtlı gösteren sanallaştırma ortamları. Şart karşılanmazsa uygulama hiç açılmıyor ve "The current CPU is missing one or more of the baseline instruction sets" benzeri bir hata veriyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  .NET 11 için neden RC3 çıktı?
&lt;/h2&gt;

&lt;p&gt;.NET'te alışılmış sıra iki sürüm adayı (RC1, RC2), ardından Kasım'da genel sürümdür. Bu yıl 25 Ekim'de bir de &lt;a href="https://github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc3/11.0.0-rc.3.md" rel="noopener noreferrer"&gt;RC3&lt;/a&gt; çıktı. Sebep yeni bir özellik değil, takvim dışı bir güvenlik yaması: Brotli sıkıştırma kütüphanesindeki CVE-2026-111497. Aynı gün .NET 8, 9 ve 10'a da yama geldi.&lt;/p&gt;

&lt;p&gt;Bu yama, desteği biten sürümde ne kaybettiğini de iyi gösteriyor: aynı açık 11 Kasım'da bulunsaydı .NET 8 ve 9 için düzeltme çıkmayacaktı.&lt;/p&gt;

&lt;h2&gt;
  
  
  LTS mi, STS mi? Hesap değişti
&lt;/h2&gt;

&lt;p&gt;"Canlıda yalnızca LTS kullanılır" kuralı, STS'nin 18 ay desteklendiği dönemden kalma. Bugün .NET 10 (LTS) 14 Kasım 2028'e, &lt;a href="https://github.com/dotnet/core/blob/main/release-notes/11.0/README.md" rel="noopener noreferrer"&gt;.NET 11&lt;/a&gt; (STS) 9 Kasım 2028'e kadar destekleniyor. 10 Kasım'da .NET 11'e geçen bir ekip, .NET 10'da kalan bir ekipten yalnızca beş gün önce yeniden yükseltme yapmak zorunda kalacak.&lt;/p&gt;

&lt;p&gt;Yani seçim artık destek süresine değil, başka sorulara bağlı: .NET 11'in getirdiklerine ihtiyacın var mı, kullandığın kütüphaneler .NET 11'i destekliyor mu, ekibin her yıl sürüm yükseltebiliyor mu?&lt;/p&gt;

&lt;h2&gt;
  
  
  Kendi sisteminde nasıl kontrol edersin?
&lt;/h2&gt;

&lt;p&gt;Dört yere bakman yeterli: projelerin hedef sürümü, sunucudaki runtime'lar, container imajları ve .NET 11'e geçeceksen işlemci.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Projelerin hedeflediği sürüm&lt;/span&gt;
grep -rE &lt;span class="s2"&gt;"&amp;lt;TargetFrameworks?&amp;gt;"&lt;/span&gt; --include=*.csproj .

&lt;span class="c"&gt;# Makinede kurulu runtime'lar&lt;/span&gt;
dotnet --list-runtimes

&lt;span class="c"&gt;# Dockerfile'lardaki taban imajlar&lt;/span&gt;
grep -rn &lt;span class="s2"&gt;"mcr.microsoft.com/dotnet"&lt;/span&gt; --include=&lt;span class="s2"&gt;"Dockerfile*"&lt;/span&gt; .

&lt;span class="c"&gt;# Linux sunucuda x86-64-v2 için gereken komut setleri (ikisi de görünmeli)&lt;/span&gt;
grep -owE &lt;span class="s2"&gt;"sse4_2|popcnt"&lt;/span&gt; /proc/cpuinfo &lt;span class="o"&gt;|&lt;/span&gt; sort -u
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;net8.0&lt;/code&gt;, &lt;code&gt;net9.0&lt;/code&gt; ya da &lt;code&gt;aspnet:8.0&lt;/code&gt; gibi bir sonuç görürsen o servis 10 Kasım'dan sonra yama almayacak. Self-contained yayımlanan uygulamalar runtime'ı kendi içinde taşıdığı için sunucuyu güncellemek onları kurtarmaz; onları yeniden derleyip yayımlaman gerekir.&lt;/p&gt;

&lt;p&gt;Politikaya göre Visual Studio bu sürümleri bir ay sonra "desteği bitmiş" olarak işaretleyecek, altı ay sonra da yeni SDK'lar bu sürümleri hedefleyen projelerde NETSDK1138 uyarısı vermeye başlayacak. Uyarıyı beklemeden listeyi şimdi çıkarmak daha ucuz.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/dotnet-8-9-destek-sonu" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>security</category>
      <category>backend</category>
    </item>
    <item>
      <title>SQL Server'da yavaş sorgu nasıl hızlandırılır?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Mon, 02 Nov 2026 06:53:51 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/sql-serverda-yavas-sorgu-nasil-hizlandirilir-5egh</link>
      <guid>https://dev.to/huseyinbozdemir/sql-serverda-yavas-sorgu-nasil-hizlandirilir-5egh</guid>
      <description>&lt;p&gt;POS hareketlerini listeleyen bir endpoint'imiz vardı. Tabloya her gün milyonlarca satır ekleniyordu ve ekranın en sık kullanılan hâli gün bazlı sorguydu: bir işyerinin belli günlerdeki hareketleri, sayfa sayfa. Bu sorgu dakikalar sürüyordu. &lt;code&gt;CreatedDate&lt;/code&gt; kolonunda indeks vardı, ama sorgu onu kullanmıyor gibiydi.&lt;/p&gt;

&lt;p&gt;Dört değişiklikle sorgu dakikalardan saniyelere indi. Bu yazı için o dört adımı 10 milyon satırlık sentetik bir tabloda yeniden ölçtüm. Ölçümlerden biri, o gün suçladığımız şeyin asıl suçlu olmadığını gösterdi.&lt;/p&gt;

&lt;p&gt;Kısa cevap: SQL Server'da yavaş bir listeleme sorgusunu hızlandırırken önce indeksin gerçekten kullanılıp kullanılmadığına bak. Kolona uygulanan fonksiyonlar (&lt;code&gt;CONVERT&lt;/code&gt;, &lt;code&gt;DATEDIFF&lt;/code&gt;) indeksi devre dışı bırakır; filtrelediğin kolonlar için bileşik indeks gerekir. Derin sayfalarda &lt;code&gt;OFFSET&lt;/code&gt; atlanan her satırı okur; cursor (keyset) sayfalama her sayfada aynı maliyettedir. Geniş listelerde önce yalnızca Id'leri seç, kolonları sonra getir. Join'deki bir CTE her satır için yeniden çalışabilir; ara sonucu temp tabloya almak bunu bir kez çalışır hâle getirir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ölçüm düzeneği
&lt;/h2&gt;

&lt;p&gt;Gerçek tabloyu ve verisini kullanamam, o yüzden benzerini kurdum. &lt;code&gt;PosHareketleri&lt;/code&gt; tablosunda 10 milyon satır var: 30 gün, günde yaklaşık 330 bin hareket. Yanında işyerleri (20 bin), terminaller (60 bin) ve iadeler (200 bin) duruyor. &lt;code&gt;CreatedDate&lt;/code&gt; kolonunda tek kolonlu bir indeks var. Bizim tablomuz 30-40 kolondu; buradaki 10 kolon, yani gerçekte farklar daha da büyür.&lt;/p&gt;

&lt;p&gt;Ortam SQL Server 2022 Express. Sorgular uygulamanın göndereceği gibi parametreli (&lt;code&gt;sp_executesql&lt;/code&gt;), her biri iki kez çalışıyor ve ikincisi sayılıyor; yani önbellek sıcak.&lt;/p&gt;

&lt;p&gt;Süreyi raporluyorum ama asıl baktığım sayı mantıksal okuma (logical reads): sorgunun okuduğu 8 KB'lık sayfa sayısı. Süre makineye, önbelleğe ve o anki yüke göre oynar; okuma sayısı oynamaz. İkisini de şöyle açarsın:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- sorgu&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Messages sekmesinde her tablo için &lt;code&gt;logical reads&lt;/code&gt; ve sorgunun CPU ve geçen süresi görünür. Yanına gerçek çalışma planını (actual execution plan, SSMS'te Ctrl+M) açarsan hangi operatörün bu okumaları yaptığını da görürsün.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tarih filtresi: kolona fonksiyon uygulamak
&lt;/h2&gt;

&lt;p&gt;Gün bazlı sorguda tarih filtresi &lt;code&gt;CreatedDate&lt;/code&gt;'i güne çeviriyordu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;IsyeriId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;isyeri&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bir kolona fonksiyon uyguladığında veritabanı genellikle o kolonun indeksinde arama (seek) yapamaz. İndeks &lt;code&gt;CreatedDate&lt;/code&gt;'in kendisine göre sıralı; &lt;code&gt;CAST(CreatedDate AS date)&lt;/code&gt; ya da &lt;code&gt;CONVERT(char(8), CreatedDate, 112)&lt;/code&gt; sonucuna göre değil. Bu durumda her satır için fonksiyonu hesaplayıp sonucu karşılaştırmak zorunda kalır. Bu tür koşullara sargable olmayan (non-sargable) koşul denir.&lt;/p&gt;

&lt;p&gt;Biz bunu düzelttik: gelen tarihleri bir gün kaydırıp aralık olarak yazdık.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;IsyeriId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;isyeri&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;DATEADD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu yazı için ölçerken şunu gördüm: SQL Server &lt;code&gt;CAST(... AS date)&lt;/code&gt; için bir istisna yapıyor. Kolonu &lt;code&gt;date&lt;/code&gt;'e çevirmeyi tanıyor ve indekste yine aralık araması yapabiliyor. Tek günlük, ilk 50 satırlık sorguda dört farklı yazımın sonucu:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filtre&lt;/th&gt;
&lt;th&gt;Okuma&lt;/th&gt;
&lt;th&gt;Süre&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CAST(CreatedDate AS date) = @gun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;203&lt;/td&gt;
&lt;td&gt;~1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreatedDate &amp;gt;= @gun AND CreatedDate &amp;lt; @gun + 1 gün&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;~0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATEDIFF(day, CreatedDate, @gun) = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14.112&lt;/td&gt;
&lt;td&gt;1.038 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONVERT(char(8), CreatedDate, 112) = ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;169.934&lt;/td&gt;
&lt;td&gt;2.066 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;DATEDIFF&lt;/code&gt; indeksi baştan sona tarıyor, &lt;code&gt;CONVERT&lt;/code&gt; bütün tabloyu okuyor. &lt;code&gt;CAST&lt;/code&gt; ise aralıkla neredeyse aynı. Yani bizim sorgudaki asıl yük CAST değildi.&lt;/p&gt;

&lt;p&gt;Aralıkla yazmak yine de doğru alışkanlık. &lt;code&gt;CAST&lt;/code&gt; istisnası SQL Server'a özgü ve yalnızca &lt;code&gt;date&lt;/code&gt;'e çevirmede geçerli; aynı kod başka bir veritabanında ya da başka bir fonksiyonla aynı şansı bulamaz. Aralık her yerde indeksi kullanır.&lt;/p&gt;

&lt;p&gt;Asıl fark işyeri filtresinde çıktı. Aynı sorguyu bir işyeri ve yedi gün için üç farklı indeks durumunda çalıştırdım:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;İndeks&lt;/th&gt;
&lt;th&gt;Okuma&lt;/th&gt;
&lt;th&gt;Süre&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Yalnızca &lt;code&gt;CreatedDate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;169.934&lt;/td&gt;
&lt;td&gt;454-611 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CreatedDate&lt;/code&gt; + ayrıca &lt;code&gt;IsyeriId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2.044&lt;/td&gt;
&lt;td&gt;2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bileşik &lt;code&gt;(IsyeriId, CreatedDate)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;1-2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yalnızca tarih indeksi varken yedi günlük aralık 2,3 milyon satıra denk geliyor. Optimizer bu kadar satır için indeksten tek tek tabloya gitmektense bütün tabloyu taramayı seçiyor; &lt;code&gt;CAST&lt;/code&gt;'lı ve aralıklı yazımın ikisi de aynı taramaya düşüyor. Bileşik indekste önce işyerine, sonra o işyerinin tarih aralığına iniliyor ve sorgu yalnızca gösterilecek satırları okuyor.&lt;/p&gt;

&lt;p&gt;Bileşik indekste kolon sırası önemli: eşitlikle filtrelenen kolon (&lt;code&gt;IsyeriId&lt;/code&gt;) önce, aralıkla filtrelenen kolon (&lt;code&gt;CreatedDate&lt;/code&gt;) sonra. Ters sırada indeks yine tarih aralığını tarar, işyerini her satırda ayrıca kontrol eder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Derin sayfada OFFSET neden yavaşlar
&lt;/h2&gt;

&lt;p&gt;Listede sayfalama vardı, ama yetersizdi. Klasik &lt;code&gt;OFFSET ... FETCH&lt;/code&gt; sayfalaması şöyle görünür:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tutar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KartMaskeli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeriNo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Isyerleri&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Terminaller&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalId&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;atla&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OFFSET 50000&lt;/code&gt; "50 bininci satırdan başla" demek değil, "50 bin satırı oku, at, sonra 50 satır ver" demek. Veritabanının atlanacak satırların yerini bilmesinin başka yolu yok. Sayfa ilerledikçe okunan ve atılan satır sayısı artar.&lt;/p&gt;

&lt;p&gt;Bir günün hareketlerinde (yaklaşık 330 bin satır) 50'lik sayfalarla ölçünce:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sayfa&lt;/th&gt;
&lt;th&gt;Okuma&lt;/th&gt;
&lt;th&gt;Süre&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;20.339&lt;/td&gt;
&lt;td&gt;21 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;203.269&lt;/td&gt;
&lt;td&gt;176 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.000&lt;/td&gt;
&lt;td&gt;169.934 (tablo taraması)&lt;/td&gt;
&lt;td&gt;918 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;1.000. sayfada işyeri ve terminal tablolarından da 103'er bin okuma var: sorgu atlanacak satırlar için de join yapıyor. 6.000. sayfada optimizer indeksi bırakıp tabloyu taramaya geçiyor. Bizim tablomuz 30-40 kolon olduğu için atılan her satır daha pahalıydı.&lt;/p&gt;

&lt;p&gt;Geçici çözümümüz cursor sayfalama oldu. Fikir, "kaçıncı sayfadayım" yerine "en son hangi satırda kaldım" bilgisini taşımak. İstemci bir sonraki sayfayı isterken önceki sayfanın son satırının &lt;code&gt;CreatedDate&lt;/code&gt;'ini ve &lt;code&gt;Id&lt;/code&gt;'sini gönderir; sorgu oradan devam eder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tutar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KartMaskeli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeriNo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Isyerleri&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Terminaller&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalId&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;sonTarih&lt;/span&gt;
       &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;sonTarih&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;sonId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu sorgu indekste doğrudan kaldığı yere iner ve 50 satır okur. Ölçümde her sayfa 220 civarı okuma ve 2-3 ms; 1. sayfayla 6.000. sayfa arasında fark yok.&lt;/p&gt;

&lt;p&gt;Bedeli de var: kullanıcı "47. sayfaya git" diyemez, yalnızca ileri ve geri gidebilir. Listede toplam sayfa sayısı gösteriliyorsa onu ayrı bir &lt;code&gt;COUNT&lt;/code&gt; ile hesaplamak gerekir. Hareket listesi gibi kullanıcının genellikle baştan aşağı kaydırdığı ekranlarda bu kayıp küçük.&lt;/p&gt;

&lt;h3&gt;
  
  
  GUID'li tenant'larda cursor
&lt;/h3&gt;

&lt;p&gt;Bizde bazı tenant'ların Id'leri &lt;code&gt;long&lt;/code&gt;, bazılarınınki GUID'di. GUID'li tenant'larda Id üzerinden büyüktür/küçüktür yapamıyorduk, o yüzden cursor'dan yalnızca &lt;code&gt;long&lt;/code&gt; Id'li tenant'lar yararlanabildi. Aslında sıralama zaten &lt;code&gt;CreatedDate&lt;/code&gt;'e göre; Id yalnızca aynı tarihli satırları ayırmaya yarıyor. Bu iş için Id'nin sıralı olması değil, tekil olması ve her seferinde aynı sırayı vermesi yeterli. SQL Server'da GUID'ler de kendi (tuhaf ama tutarlı) sıralarıyla karşılaştırılabildiği için yukarıdaki sorgu GUID'le de çalışır. Tek dikkat: son Id'yi uygulamada karşılaştırma, olduğu gibi SQL'e geri gönder; .NET'teki &lt;code&gt;Guid&lt;/code&gt; sıralaması SQL Server'ınkiyle aynı değil.&lt;/p&gt;

&lt;h2&gt;
  
  
  Önce Id'leri seç, kolonları sonra getir
&lt;/h2&gt;

&lt;p&gt;Cursor'ın kullanılamadığı tenant'lar için başka bir yol bulduk: önce olabildiğince filtreleyip yalnızca Id'leri seçmek, join'leri ve kolonları ikinci adımda yapmak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;sayfa&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;atla&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tutar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KartMaskeli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeriNo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sayfa&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Isyerleri&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Terminaller&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalId&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İç sorgu yine &lt;code&gt;OFFSET&lt;/code&gt; kullanıyor ve yine atlanan satırları okuyor, ama bunu yalnızca &lt;code&gt;CreatedDate&lt;/code&gt; indeksinde yapıyor. İndeks dar: içinde tarih ve Id var, 30-40 kolon yok. Join'ler ve geniş satırlar yalnızca seçilen 50 Id için yapılıyor. Bu kalıba deferred join de deniyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Grafik: bir günlük hareketlerde (yaklaşık 330 bin satır, 50 satırlık sayfa) sayfa numarasına göre süre. OFFSET: 1. sayfa 2 ms, 100. sayfa 21 ms, 1.000. sayfa 176 ms, 6.000. sayfa 918 ms. Önce Id, sonra join: 6.000. sayfa 28 ms. Cursor: her sayfa 2-3 ms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;6.000. sayfada &lt;code&gt;OFFSET&lt;/code&gt; 918 ms'ten bu yazımla 28 ms'e iniyor, okuma 170 binden 998'e düşüyor. Cursor kadar düz değil ama sayfa numarasına göre gitme imkânını koruyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join'deki CTE her satırda çalışınca
&lt;/h2&gt;

&lt;p&gt;Filtrelenmiş Id'leri önce bir CTE'de toplayıp ana sorguya join'liyorduk. Planı incelediğimizde CTE'nin her satır için yeniden çalıştığını gördük ve temp tabloya geçmek zorunda kaldık.&lt;/p&gt;

&lt;p&gt;Sebebi, SQL Server'ın CTE'yi bir ara tablo gibi saklamaması. CTE, sorguya gömülen bir alt sorgudur (inline). Optimizer onu nested loops join'in iç tarafına koyarsa, dış taraftan gelen her satır için o alt sorgu yeniden çalışır. Sentetik tabloda aynı şekli kurdum: bir işyeri aralığı ve bir haftalık hareketlerin Id'leri CTE'de, dışarıda join'ler ve iade toplamı.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;secilen&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;isyeriBas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;isyeriBit&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tutar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KartMaskeli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeriNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IadeTutar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Iadeler&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HareketId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;IadeToplam&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Isyerleri&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Terminaller&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalId&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;secilen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gerçek planda optimizer dış tarafta tarih indeksini sondan başa tarıyor ve CTE'nin &lt;code&gt;PosHareketleri&lt;/code&gt; aramasını her satır için bir kez çalıştırıyor. Plandaki "Number of Executions" değeri 5.002.003. Sonuç 40,6 milyon okuma ve 9 saniye.&lt;/p&gt;

&lt;p&gt;Temp tabloyla aynı iş iki adım:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt;
&lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;secilen&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;isyeriBas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;isyeriBit&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bas&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tutar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KartMaskeli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeriNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IadeTutar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Iadeler&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HareketId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;IadeToplam&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;secilen&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;PosHareketleri&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Isyerleri&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsyeriId&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Terminaller&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalId&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filtre bir kez çalışıyor, sonuç &lt;code&gt;#secilen&lt;/code&gt;'e yazılıyor. SQL Server temp tabloya istatistik de oluşturduğu için ikinci sorguda kaç satırla çalışacağını biliyor. İki adım toplam 0,85 saniye; CTE'li hâlin onda biri. Bizde de temp tablo sorguya özeldi; sorgu bitince düşüyordu, ayrıca indeks koymadık.&lt;/p&gt;

&lt;p&gt;Temp tablonun bedeli, sorgunun artık tek bir ifade olmaması. ORM'in ürettiği tek bir &lt;code&gt;SELECT&lt;/code&gt; ile yapılamıyor; ya ham SQL ya da stored procedure gerekiyor. Ara sonuç çok büyükse tempdb'ye yazmanın maliyeti de hesaba girer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kendi sisteminde nasıl fark edersin
&lt;/h2&gt;

&lt;p&gt;Yavaş bir sorguyu &lt;code&gt;SET STATISTICS IO ON&lt;/code&gt; ile çalıştır ve okuma sayısını, sorgunun döndürdüğü satır sayısıyla karşılaştır. 50 satır gösteren bir liste için yüz binlerce okuma görüyorsan sorgu ihtiyacından fazlasını okuyor.&lt;/p&gt;

&lt;p&gt;Sonra gerçek çalışma planında iki şeye bak: indeks üzerinde &lt;code&gt;Seek&lt;/code&gt; yerine &lt;code&gt;Scan&lt;/code&gt; olan operatörler ve "Number of Executions" değeri 1'den çok büyük olan operatörler. İlki indeksin kullanılmadığını, ikincisi bir alt sorgunun ya da CTE'nin her satırda yeniden çalıştığını gösterir. EF Core kullanıyorsan önce üretilen SQL'i log'dan al; &lt;a href="https://huseyinbozdemir.com/blog/ef-core-include-select" rel="noopener noreferrer"&gt;Include ve Select yazısında&lt;/a&gt; ORM tarafındaki karşılığını anlatmıştım.&lt;/p&gt;

&lt;p&gt;Bizim sorguyu dakikalardan saniyelere indiren tek bir değişiklik olmadı. Ölçümde dört adımın ortak noktası aynı çıktı: ekranda gösterilmeyecek satırı okumamak.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/sql-server-yavas-sorgu" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sqlserver</category>
      <category>database</category>
      <category>performance</category>
      <category>sql</category>
    </item>
    <item>
      <title>İnternet yokken kartla ödeme nasıl onaylanır?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:05:09 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/internet-yokken-kartla-odeme-nasil-onaylanir-31i3</link>
      <guid>https://dev.to/huseyinbozdemir/internet-yokken-kartla-odeme-nasil-onaylanir-31i3</guid>
      <description>&lt;p&gt;İnternet ya da elektrik gidince marketteki POS bankaya ulaşamaz. Bugün çoğu yerde kartla ödeme orada biter ve kasiyer nakit olup olmadığını sorar. Mastercard bunu Avrupa'da değiştiriyor: 8 Ekim'de duyurduğu kurala göre Şubat 2027'den sonra basılan her Mastercard kartı, Mayıs 2027'den sonra takılan her yeni POS da bağlantı yokken ödeme alabilecek (&lt;a href="https://thepaypers.com/payments/news/mastercard-expands-offline-payments-across-europe" rel="noopener noreferrer"&gt;The Paypers&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Çevrimdışı ödeme (offline payment), POS'un o an bankaya soramadığı kart ödemesi. Bu adla iki ayrı şey anılıyor: kartın çipinin ödemeyi kendisi onaylaması ve POS'un işlemi saklayıp bağlantı gelince bankaya sorması. Mastercard'ın kuralı birincisi. Aradaki fark, para yetmediğinde açığın kimde kalacağını belirliyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diyagram: Kesintiden kurala zaman çizelgesi. Nisan 2025'te İspanya ve Portekiz'de elektrik kesintisi. Ekim 2026'da Mastercard çevrimdışı ödeme kuralını duyurdu. Şubat 2027'den itibaren yeni basılan kartlar, Mayıs 2027'den itibaren yeni takılan POS'lar çevrimdışı ödemeyi destekleyecek.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;28 Nisan 2025'te İspanya ve Portekiz'de elektrik kesildi. Mastercard kuralı 8 Ekim 2026'da duyurdu. Şubat 2027'den sonra basılan kartlar, Mayıs 2027'den sonra takılan ya da değiştirilen POS'lar bağlantısız ödeme alabilecek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kural nereden çıktı?
&lt;/h2&gt;

&lt;p&gt;28 Nisan 2025'te İspanya ve Portekiz'de elektrik kesildi. Mastercard'a göre o gün yaklaşık 55 milyon kişi dijital ödeme yapamadı. Danimarka, İsveç, Estonya ve Letonya'da çevrimdışı ödeme zaten kullanılıyor; Letonya'daki bir fırtınada jeneratörle çalışan dükkânlarda kartla ödeme sürmüş (&lt;a href="https://www.electronicpaymentsinternational.com/news/mastercard-europe-offline-payment-rules/" rel="noopener noreferrer"&gt;Electronic Payments International&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Yeni kural bunu Avrupa'nın geneline yayıyor. POS tarafı bir gecede değişmeyecek; dükkânlar cihazlarını yeniledikçe yayılacak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kart kendisi onaylayınca
&lt;/h2&gt;

&lt;p&gt;Kartın çipi küçük bir bilgisayar. Ödeme sırasında POS çipe tutarı ve kendi kontrollerinin sonucunu verir, çip de üç cevaptan birini döner: onay (TC), "bankaya sor" (ARQC) ya da ret (AAC). Normal bir günde çip neredeyse her zaman "bankaya sor" der ve kararı banka verir.&lt;/p&gt;

&lt;p&gt;Bağlantı yokken bu yol kapalı. Burada devreye giren, çipli kartların ödeme standardı EMV'nin bağlantısız onayı; sektörde adı offline EMV. Karar, bankanın kartı basarken çipe yazdığı kurallara kalıyor: PIN doğru mu, tutar çevrimdışı limitin içinde mi, kart art arda kaç kez bankaya sorulmadan kullanılmış. Hepsi tamamsa çip işlemi imzalayıp onaylıyor. Bu imza, bankanın işlemi sonradan gördüğünde gerçekten o karttan geldiğini doğrulamasını sağlıyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diyagram: Bağlantı yokken onayı çip verir. POS bankaya ulaşamıyor ve tutarı çipe soruyor. Çip, bankanın önceden yazdığı kurallara bakıyor: PIN doğru mu, tutar çevrimdışı limitin içinde mi, art arda kaçıncı bağlantısız işlem. Üç cevaptan birini veriyor: onay (TC, çip işlemi imzalar), bankaya sor (ARQC, bağlantı olmadığı için yapılamaz) ya da ret (AAC).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Normal günde çip neredeyse her zaman "bankaya sor" der. Bağlantı yokken bu yol kapalı; karar, bankanın kartı basarken çipe yazdığı kurallara kalır.&lt;/p&gt;

&lt;p&gt;Mastercard'ın kuralında harcama limiti çipte duruyor ve onu kartın bankası belirliyor. Tek işlemde üst sınır 200 euro. Limit çipte olduğu için kart hangi POS'a takılırsa takılsın harcama aynı sayaçtan düşüyor; iki ayrı markette art arda alışveriş yapıp limiti ikiye katlamak mümkün değil.&lt;/p&gt;

&lt;p&gt;Çipin bilmediği tek şey hesaptaki para.&lt;/p&gt;

&lt;p&gt;Bakiye yetmezse işlem yine onaylanmış olur ve bağlantı gelince takasa gider. Açık bankada kalır. Limiti bankanın koyması da bu yüzden: ne kadar risk alacağına kendisi karar veriyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  POS saklayıp sonra sorunca
&lt;/h2&gt;

&lt;p&gt;İkinci yolun adı store and forward (sakla ve ilet). Burada çip yine "bankaya sor" der. POS soramayınca işlemi saklar, ekranda kabul gösterir ve bağlantı gelince normal onay isteğini gönderir. Yani o anda kimse onay vermemiştir, karar yalnızca ertelenmiştir (&lt;a href="https://corebaseit-site.pages.dev/posts/offline-emv-vs-store-and-forward" rel="noopener noreferrer"&gt;offline EMV ile farkı&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Banka saatler sonra işlemi yetersiz bakiye ya da kapalı hesap yüzünden reddedebilir. Satıcı ürünü çoktan vermiştir. Adyen'in dokümanı bunu açıkça yazıyor: çevrimdışı alınan ödemelerde başarısız tahsilat ve itiraz riski satıcıda (&lt;a href="https://docs.adyen.com/point-of-sale/offline-payment" rel="noopener noreferrer"&gt;Adyen&lt;/a&gt;). Sağlayıcıların bu yolda tutar, adet ve süre sınırı koyması bundan.&lt;/p&gt;

&lt;p&gt;Backend'den bakınca bu, &lt;a href="https://huseyinbozdemir.com/blog/idempotency-odeme-sistemleri" rel="noopener noreferrer"&gt;idempotency yazısındaki&lt;/a&gt; "bankaya iletildi" durumuna benziyor: ödeme alınmış görünüyor ama sonucunu henüz kimse bilmiyor. Farkı, burada ürünün çoktan kapıdan çıkmış olması.&lt;/p&gt;

&lt;h2&gt;
  
  
  Risk kimde kalıyor?
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Diyagram: Çevrimdışı ödemenin iki yolu. Çip onaylar (offline EMV): kararı çip bankanın kurallarıyla verir, banka sonradan reddedemez, para yetmezse açık bankada ve limitle sınırlı kalır. POS saklar (store and forward): o anda kimse onay vermez, POS kabul gösterir; banka saatler sonra reddedebilir ve açık satıcıda kalır.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;İkisi de kasada "onaylandı" gösterebilir. Farkı, para yetmediğinde ortaya çıkar.&lt;/p&gt;

&lt;p&gt;Çevrimdışı EMV'de risk bankanın çizdiği limitle sınırlı. Çip PIN'i kendisi doğruladığı için PIN'i bilinmeyen çalıntı bir kart da işe yaramıyor. Store and forward'da sınırı ödeme sağlayıcısı koyuyor ama bedeli satıcı ödüyor. Kasadaki ekran iki durumda da "onaylandı" diyebilir.&lt;/p&gt;

&lt;p&gt;Türkiye'nin bu kurala dahil olup olmadığını duyurulardan çıkaramadım. Haberler "Avrupa" diyor ve ülke listesi vermiyor.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/cevrimdisi-kart-odeme" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>payments</category>
      <category>fintech</category>
      <category>emv</category>
      <category>backend</category>
    </item>
    <item>
      <title>Ajanlı ödeme nedir? Yapay zekâ nasıl öder?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:04:46 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/ajanli-odeme-nedir-yapay-zeka-nasil-oder-1o7i</link>
      <guid>https://dev.to/huseyinbozdemir/ajanli-odeme-nedir-yapay-zeka-nasil-oder-1o7i</guid>
      <description>&lt;p&gt;20 Ekim'de iki kart ağı aynı gün, yapay zekâ ajanlarının ödeme yapmasını gerçek işlemlerle denemeye başladı. Visa, fatura ödeme platformu BILL ve tedarikçi portalı Billtrust'la &lt;a href="https://usa.visa.com/about-visa/newsroom/press-releases.releaseId.22856.html" rel="noopener noreferrer"&gt;canlı bir pilot&lt;/a&gt; duyurdu. Mastercard da aynı gün geliştiricilere "Agent Pay for Machines" için erken erişim açtı (&lt;a href="https://www.theblock.co/news/business/2026-10-20-mastercard-opens-early-access-to-ai-agent-payments-following-pilots-with-coinbase-base-and-cloudflare-421406" rel="noopener noreferrer"&gt;The Block&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Ajanlı ödeme (agentic payments), insanın sınırını önceden çizdiği bir yetkiyle bir yazılımın, yani bir yapay zekâ ajanının ödemeyi kendisi yapması. Haberi ilk okuduğumda iki şeyi anlamadım: ödemeyi yapay zekâ mı yapacak, kart numarası olmadan nasıl ödeyecek? İkisinin cevabı da haberin içinde.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ödemeyi yapay zekâ mı yapacak?
&lt;/h2&gt;

&lt;p&gt;Kararı hâlâ insan veriyor. Visa pilotunda BILL'in ödeme ajanı yalnızca alıcının daha önce onayladığı faturaları ödüyor; Visa'ya göre ajan ödeme kararı vermiyor, onaylanmış talimatı yerine getiriyor. Eskiden biri her tedarikçinin portalına tek tek girip öderdi, şimdi bu tekrarı ajan yapıyor. Pilot canlıda ve gerçek işlemler işleniyor.&lt;/p&gt;

&lt;p&gt;Mastercard'ın tarafı makineden makineye ödeme. Bir yazılım iş yaparken kullanım başına ücretli bir servise ihtiyaç duyuyor; insan bir bütçe veriyor, ajan o bütçe içinde kendisi ödüyor. Sistem, ödemeyi HTTP'nin içine gömen açık protokol x402 üzerine kurulu: servis &lt;code&gt;402 Payment Required&lt;/code&gt; dönüyor, ajan imzalı bir ödeme kuponu gönderiyor, satıcı kuponu doğrulayıp hizmeti veriyor ve kuponlar sonra toplu tahsil ediliyor. 402 kodu HTTP standardında hâlâ &lt;a href="https://www.rfc-editor.org/rfc/rfc9110#name-402-payment-required" rel="noopener noreferrer"&gt;"ileride kullanılmak üzere" ayrılmış&lt;/a&gt; duruyor. Demoda bir ajan, onaylı tek bir bütçeden iki sağlayıcıya dokuz ödeme yaptı. Mastercard'ın teknik dokümanı henüz herkese açık değil; ayrıntılar şimdilik basın haberlerinden.&lt;/p&gt;

&lt;p&gt;"Bana 3.000 liranın altında bir uçak bileti bul ve al" diyen bireysel kullanıcı ise bu pilotların kapsamında değil.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kart numarası olmadan nasıl ödüyor?
&lt;/h2&gt;

&lt;p&gt;İki ayrı parça birlikte çalışıyor: parayı token taşıyor, kimliği imza kanıtlıyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token.&lt;/strong&gt; Visa pilotunda ajan, tokenize edilmiş ödeme bilgisiyle ödüyor. Apple Pay ya da Google Pay'de olan da bu: telefon satıcıya karta bağlı ayrı bir numara gönderir, kart ağı onu arkada gerçek kartla eşleştirir ve para yine kart hesabından çıkar. Visa ajan token'ının biçimini ve hangi sınırları taşıyabildiğini açıklamadı.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;İmza.&lt;/strong&gt; Satıcı, karşısındakinin kayıtlı bir ajan mı yoksa sıradan bir bot mu olduğunu Visa'nın &lt;a href="https://developer.visa.com/docs/trusted-agent-protocol/trusted-agent-protocol-specifications" rel="noopener noreferrer"&gt;Trusted Agent Protocol&lt;/a&gt; şartnamesiyle anlıyor. Ajan her HTTP isteğini &lt;a href="https://www.rfc-editor.org/rfc/rfc9421" rel="noopener noreferrer"&gt;RFC 9421 HTTP Message Signatures&lt;/a&gt; ile imzalıyor; satıcı ajanın açık anahtarını Visa'nın &lt;code&gt;/keys&lt;/code&gt; uç noktasından JWK olarak çekip imzayı doğruluyor. Şartnamedeki kurallar kısa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;İmzada en az &lt;code&gt;@authority&lt;/code&gt;, &lt;code&gt;@path&lt;/code&gt;, &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;expires&lt;/code&gt;, &lt;code&gt;keyid&lt;/code&gt;, &lt;code&gt;alg&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt; ve &lt;code&gt;tag&lt;/code&gt; bulunmalı; biri eksikse istek engellenir.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created&lt;/code&gt; ile &lt;code&gt;expires&lt;/code&gt; arası 8 dakikayı geçemez.&lt;/li&gt;
&lt;li&gt;Satıcı son 8 dakikada gördüğü nonce'ları tutar, aynısı tekrar gelirse isteği engeller. Bu, &lt;a href="https://huseyinbozdemir.com/blog/idempotency-odeme-sistemleri" rel="noopener noreferrer"&gt;idempotency&lt;/a&gt; ile aynı aileden bir kontrol: aynı isteğin ikinci kez işlenmemesi.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tag&lt;/code&gt; gezinmede &lt;code&gt;agent-browser-auth&lt;/code&gt;, ödemede &lt;code&gt;agent-payer-auth&lt;/code&gt; değerini taşır.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visa'ya göre bu doğrulama, sitelerin bugüne kadar CAPTCHA ve bot engelleme ile sağladığı korumanın yerini tutabiliyor. Ajan ayrı bir portal girişine de ihtiyaç duymuyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diyagram: Ajanlı ödemenin iki yolu: Visa ve Mastercard. Solda Visa: insan faturayı onaylar, ajan isteği RFC 9421 ile imzalar (8 dakika geçerli, tek kullanımlık nonce), satıcı ajanın açık anahtarını Visa dizininden alıp imzayı doğrular, ödeme gerçek kart numarası yerine token ile yapılır. Sağda Mastercard: insan bütçe verir, ücretli servis HTTP 402 Payment Required döner, ajan bütçe içinde imzalı bir ödeme kuponu gönderir, kuponlar sonra toplu tahsil edilir.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;İki ağ aynı soruya farklı cevap veriyor. Visa kart altyapısının üstüne bir kimlik katmanı ekliyor; Mastercard ödemeyi doğrudan HTTP'nin içine gömüyor. İkisinde de karar insanda, sınır önceden çiziliyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benim yorumum
&lt;/h2&gt;

&lt;p&gt;Fikir bana mantıklı geliyor ama temkinliyim. Bu haberdeki parçaların hiçbiri ödeme dünyası için yeni değil; yeni olan, bankalarla tek tek kurduğumuz şeylerin ilk kez herkes için standart hâle gelmesi.&lt;/p&gt;

&lt;p&gt;Token'ı düşünelim. Daha önce çalıştığım bir kart saklama modülünde kartı şifreleyip saklıyor, işlemi o kartın token'ıyla yapıyorduk. Ama o token yalnızca bizim sistemimizin içinde anlam taşıyordu. Bankaya giderken şifre çözülüyor, kart numarası yine yola çıkıyordu. Ağ düzeyindeki token'da gerçek numara hiç dolaşmıyor. Kart numarasının &lt;a href="https://huseyinbozdemir.com/blog/log-maskeleme-kart-numarasi" rel="noopener noreferrer"&gt;nerelere sızabildiğini&lt;/a&gt; gören biri için bu küçük bir fark değil.&lt;/p&gt;

&lt;p&gt;İmza tarafı bana çok tanıdık geldi. Bir bankayla entegrasyonda body açık gidiyor, header'da body'nin private key ile imzalanmış hâli taşınıyordu. İmzanın içinde zaman damgası ve requestId gibi tek kullanımlık bir değer de vardı. Visa'nın 8 dakika ve nonce kuralı bunun standart hâli. Bugün her banka kendi header adını, kendi kuralını istiyor ve anahtarlar PEM dosyası olarak elle değiş tokuş ediliyor. Burada anahtar bir dizinden geliyor ve kural herkes için aynı.&lt;/p&gt;

&lt;p&gt;Limit kısmında daha az eminim. Günlük tutar limiti gibi kurallar "ne kadar" sorusunu çözüyor, "doğru şeyi mi aldı" sorusunu çözmüyor. Onaylı bir fatura gibi önceden bilinen ödemede bu yeter. Ajanın kendi seçtiği bir alışverişte yetmez.&lt;/p&gt;

&lt;p&gt;Türkiye'de de benzer bir ayrım görüyorum. Benim gördüğüm kadarıyla saklı kartla tekrarlı ödemede ilk işlem 3DS ile onaylanıyor, sonrakiler 3DS sormadan geçebiliyor. Önceden onaylı ajan ödemesi bu kalıba oturur; ajanın kendi seçtiği alışveriş ise yine 3DS ister.&lt;/p&gt;

&lt;p&gt;Asıl açık soru sorumluluk. Ajan limit içinde ama yanlış bir ödeme yaparsa parayı kim geri öder: kart sahibi mi, ajanı yapan şirket mi, satıcı mı? Bunu henüz kimse bilmiyor. Pilotların zaten onaylanmış faturalarla başlamasının sebebi de bence bu.&lt;/p&gt;

&lt;p&gt;Bugün yapabileceğin tek şey varsa, kendi imzalama yapına bakmak: istekler zaman damgası ve tek kullanımlık bir değer taşıyor mu, aynı istek ikinci kez gelince reddediliyor mu? Ajanlar Türkiye'ye geldiğinde ilk sorulacak soru büyük ihtimalle bu olacak.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/ajanli-odeme-nedir" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>payments</category>
      <category>fintech</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>EF Core performansı: Include mı, Select mi?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:19:54 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/ef-core-performansi-include-mi-select-mi-13fc</link>
      <guid>https://dev.to/huseyinbozdemir/ef-core-performansi-include-mi-select-mi-13fc</guid>
      <description>&lt;p&gt;Son işlemler raporu açılmıyordu. Ekran ya timeout'a düşüyor ya da o kadar geç geliyordu ki kullanıcı çoktan sayfayı yenilemiş oluyordu. Tablo çok genişti, rapor da tek bir işlemi göstermek için işyerine, karta, bankaya, iadelere ve durum geçmişine uzanıyordu. Kodda bunların hepsi birer &lt;code&gt;Include&lt;/code&gt; olarak duruyordu.&lt;/p&gt;

&lt;p&gt;EF Core'un log'unu açınca sebebi gördük: gereksiz bir sürü select, gereksiz subquery'ler. Rapor ekranda birkaç kolon gösteriyordu, sorgu ise o işlemle ilişkili her tablonun bütün kolonlarını getiriyordu.&lt;/p&gt;

&lt;p&gt;Kısa cevap: raporda &lt;code&gt;Include&lt;/code&gt; kullanma, &lt;code&gt;Select&lt;/code&gt; ile projeksiyon yap. &lt;code&gt;Include&lt;/code&gt; ilişkili entity'leri bütün kolonlarıyla yükler; koleksiyon &lt;code&gt;Include&lt;/code&gt;'ları ana satırı çoğaltır. &lt;code&gt;Select&lt;/code&gt; ise yalnızca raporun gösterdiği alanları ister, toplamları da SQL'de hesaplatır. Bizde rapor bu değişiklikle baya hızlandı.&lt;/p&gt;

&lt;p&gt;Bu yazıda o raporun nasıl düzeldiğini, projeksiyona geçerken çıkan tuzağı, EF Core'un kendi çözümü olan &lt;code&gt;AsSplitQuery&lt;/code&gt;'yi ve hâlâ çözmediğimiz kısmı anlatacağım. Örnekler sadeleştirilmiş, alan adları jenerik; mantık aynı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Include sana ne getiriyor?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Include&lt;/code&gt;, bir entity'yi yüklerken ilişkili entity'leri de aynı sorguda yüklemeyi söyler (eager loading). Entity'nin tamamını, tablonun bütün kolonlarıyla yükler. Bir ödemeyi işyeriyle birlikte istediğinde işyeri tablosunun adresi, vergi bilgisi, ayarları da gelir.&lt;/p&gt;

&lt;p&gt;Raporu ilk yazdığımızda sorgu kabaca şöyleydi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Merchant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ThenInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Refunds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusHistory&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ThenInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dd.MM.yyyy HH:mm"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;MoneyFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaskedPan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedAt&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okurken masum görünüyor. Sayfalama var, filtre var, 20 satır geliyor. Sorun SQL tarafında.&lt;/p&gt;

&lt;p&gt;Tekil ilişkiler (&lt;code&gt;Merchant&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;Bank&lt;/code&gt;) birer JOIN olur ve her biri kendi tablosunun bütün kolonlarını sonuca ekler. Koleksiyonlar (&lt;code&gt;Refunds&lt;/code&gt;, &lt;code&gt;StatusHistory&lt;/code&gt;) daha pahalıdır: EF Core varsayılan olarak her şeyi tek sorguda getirdiği için iki koleksiyon da ödeme satırına JOIN'lenir ve satırlar çarpılır. İki iadesi ve üç durum kaydı olan bir ödeme veritabanından altı satır olarak döner; ödemenin, işyerinin, kartın ve bankanın bütün kolonları bu altı satırın her birinde tekrar eder. Buna cartesian explosion deniyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Örnek bir ödeme: iki iadesi ve üç durum kaydı var. Include'lu sorgu bu ödeme için altı satır ve on dört kolon döndürür; ödemenin, işyerinin, kartın ve bankanın kolonları her satırda tekrar eder, satırlar iki iade ile üç durum kaydının çarpımından gelir. Select projeksiyonu aynı ödeme için tek satır ve yalnızca raporun gösterdiği kolonları döndürür; iade toplamı ve son durum tarihi SQL'de hesaplanır.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Bir de sayfalama var. Satırlar çarpılınca &lt;code&gt;Skip&lt;/code&gt;/&lt;code&gt;Take&lt;/code&gt;'i doğrudan sonuca uygulamak yanlış olurdu (20 ödeme yerine 20 JOIN satırı alırdın). EF Core bunu doğru yapar: önce 20 ödemeyi bir alt sorguda seçer, sonra ilişkileri o alt sorguya bağlar. Sonuç doğru, ama log'da gördüğümüz subquery'ler buradan geliyordu. Sadeleştirilmiş hali kabaca şöyle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;      &lt;span class="c1"&gt;-- her tablonun bütün kolonları&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Payments&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Merchants&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Cards&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Banks&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Refunds&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;StatusHistory&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ekranın gösterdiği sekiz alan için bu kadar veri geliyordu. Sorguda &lt;code&gt;AsNoTracking()&lt;/code&gt; yoksa C# tarafı bir de her entity'yi change tracker'a kaydeder; raporda hiçbir şeyi değiştirmediğin halde.&lt;/p&gt;

&lt;h3&gt;
  
  
  Üretilen SQL'i görmek
&lt;/h3&gt;

&lt;p&gt;Biz sorunu EF Core'un log'unda gördük. İki yolu var. Geliştirme ortamında context'e log bağlarsın:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ya da tek bir sorgunun SQL'ini çalıştırmadan alırsın: &lt;code&gt;ToListAsync&lt;/code&gt;'ten önceki &lt;code&gt;IQueryable&lt;/code&gt; üzerinde &lt;code&gt;ToQueryString()&lt;/code&gt; çağırırsın. Rapor sorgusunu yazarken bir kez bakmak çoğu zaman yeter.&lt;/p&gt;

&lt;p&gt;Log'da parametre değerlerini de görmek için &lt;code&gt;EnableSensitiveDataLogging()&lt;/code&gt; diye bir seçenek var. Ödeme sisteminde bunu yalnızca yerelde aç; parametrelerin içinde kart ya da müşteri bilgisi olabilir ve log'a düşen veriyi geri toplamak zor. Bunu &lt;a href="https://huseyinbozdemir.com/blog/log-maskeleme-kart-numarasi" rel="noopener noreferrer"&gt;log maskeleme yazısında&lt;/a&gt; ayrıca anlattım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rapor ne istiyorsa onu iste: Select ile projeksiyon
&lt;/h2&gt;

&lt;p&gt;Çözümün ilk ve en büyük adımı &lt;code&gt;Include&lt;/code&gt;'ları atıp sorguyu doğrudan raporun ihtiyacına göre yazmak oldu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentRowData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaskedPan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="m"&gt;0m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedAt&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada birkaç şey birden değişiyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Navigation'lar hâlâ kullanılabiliyor.&lt;/strong&gt; &lt;code&gt;p.Merchant.Name&lt;/code&gt; yazdığında EF Core JOIN'i kendisi kurar, ama yalnızca &lt;code&gt;Name&lt;/code&gt; kolonunu seçer. &lt;code&gt;Include&lt;/code&gt; yazmana gerek yok. Sorgu artık entity değil DTO döndürdüğü için yazsan da EF Core &lt;code&gt;Include&lt;/code&gt;'u yok sayar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Koleksiyonlar satır çoğaltmıyor.&lt;/strong&gt; İade toplamı ve son durum tarihi SQL'de alt sorgu olarak hesaplanıyor; her ödeme tek satır dönüyor. &lt;code&gt;(decimal?)&lt;/code&gt; ve &lt;code&gt;?? 0m&lt;/code&gt; iadesi olmayan ödemede &lt;code&gt;SUM&lt;/code&gt;'ın &lt;code&gt;NULL&lt;/code&gt; dönmesine karşı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change tracking yok.&lt;/strong&gt; Projeksiyon entity döndürmediği için EF Core sonucu takip etmez. Raporda &lt;code&gt;AsNoTracking()&lt;/code&gt; yazmayı unutup unutmadığını düşünmene gerek kalmaz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sıralama tekil.&lt;/strong&gt; &lt;code&gt;ThenByDescending(p =&amp;gt; p.Id)&lt;/code&gt; aynı saniyede düşmüş ödemeler arasında sırayı sabitliyor. Neden önemli olduğu aşağıda, &lt;code&gt;AsSplitQuery&lt;/code&gt; bölümünde.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bu değişiklik raporu baya hızlandırdı; SP'ye ya da ham SQL'e geçmeye gerek kalmadı. Mimaride EF Core kullanıyoruz; projeksiyonla yetinebildiğimiz sürece orada kalıyoruz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Select içindeki metot neden patlıyor?
&lt;/h2&gt;

&lt;p&gt;Projeksiyona geçerken ilk denemede formatlama gibi metotlar da &lt;code&gt;Select&lt;/code&gt;'in içindeydi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dd.MM.yyyy HH:mm"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;MoneyFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// C# metodu&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// InvalidOperationException: ... could not be translated.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hata "could not be translated" oldu. EF Core, SQL'e çeviremediği bir C# metodunu yalnızca en dıştaki &lt;code&gt;Select&lt;/code&gt;'te kabul eder: o durumda gereken kolonları SQL'den alır, metodu bellekte çalıştırır. &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/client-eval" rel="noopener noreferrer"&gt;Client evaluation&lt;/a&gt; dokümanı bunu "top-level projection" diye anlatıyor. Bizim sorguda &lt;code&gt;Select&lt;/code&gt;'ten sonra &lt;code&gt;Skip&lt;/code&gt;/&lt;code&gt;Take&lt;/code&gt; geliyordu; &lt;code&gt;Select&lt;/code&gt; artık en dışta değildi ve EF Core sorgunun tamamını SQL'e çevirmek zorundaydı. &lt;code&gt;MoneyFormat.Format&lt;/code&gt;'ın SQL karşılığı yok.&lt;/p&gt;

&lt;p&gt;Çözüm sırayı ayırmak oldu. SQL'den ham değerleri sayfalanmış olarak al, formatlamayı sonra bellekte yap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// data: yukarıdaki projeksiyon sorgusundan gelen 20 satır&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dd.MM.yyyy HH:mm"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;MoneyFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MerchantName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaskedPan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BankName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MoneyFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RefundTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastStatusAt&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Formatlama artık yalnızca sayfadaki 20 kayıt üzerinde çalışıyor; maliyeti ölçülemeyecek kadar küçük. Bu ayrımın bir faydası daha var: tarih ve para biçimi kültüre bağlı bir sunum işi. SQL'e taşınsaydı bile orada durmaması gerekirdi.&lt;/p&gt;

&lt;h2&gt;
  
  
  AsSplitQuery: EF Core'un kendi çözümü
&lt;/h2&gt;

&lt;p&gt;"Include yavaş" diye aradığında karşına çıkan ilk öneri büyük ihtimalle &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries" rel="noopener noreferrer"&gt;&lt;code&gt;AsSplitQuery()&lt;/code&gt;&lt;/a&gt; olur. EF Core 5'ten beri var ve koleksiyon &lt;code&gt;Include&lt;/code&gt;'larında satır çarpılmasını çözüyor: tek sorgu yerine her koleksiyon için ayrı bir SQL sorgusu çalıştırır. Ödemeler bir sorguda, iadeler ikinci sorguda, durum geçmişi üçüncü sorguda gelir; satırlar çarpılmaz.&lt;/p&gt;

&lt;p&gt;Ama yalnızca satır çarpılmasını çözer. Her entity yine bütün kolonlarıyla gelir, change tracking yine çalışır, veritabanına bir yerine üç kez gidilir. Sorgular arasında veri değişirse (transaction içinde değilsen) ödeme bir anın, iadeleri bir sonraki anın verisi olabilir.&lt;/p&gt;

&lt;p&gt;Raporu düzelttiğimizde &lt;code&gt;AsSplitQuery&lt;/code&gt;'yi bilmiyordum. Bilseydim de projeksiyonu seçerdim; projeksiyon bence daha esnek. Raporun hangi alanı istediğine sen karar veriyorsun, toplamı SQL'de mi bellekte mi yapacağını sen seçiyorsun. &lt;code&gt;AsSplitQuery&lt;/code&gt; ise entity'yi tam yüklemenin gerçekten gerektiği yerde, örneğin bir ödemeyi ilişkileriyle düzenlemek için açtığın ekranda anlamlı.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sayfalamada sıralama tekil olmalı
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AsSplitQuery&lt;/code&gt;'yi &lt;code&gt;Skip&lt;/code&gt;/&lt;code&gt;Take&lt;/code&gt; ile kullanıyorsan bilmen gereken bir tuzak var. Bölünmüş sorguların her biri "hangi 20 ödeme?" sorusunu veritabanına yeniden sorar: iadeleri getiren sorgu, ödemeleri seçen filtreyi, sıralamayı ve &lt;code&gt;OFFSET&lt;/code&gt;'i bir alt sorgu olarak tekrarlar. Cevabın her seferinde aynı çıkması gerekir.&lt;/p&gt;

&lt;p&gt;Raporu yalnızca &lt;code&gt;PaymentDate&lt;/code&gt;'e göre sıralıyorsan ve aynı saniyede birden fazla ödeme düşmüşse (ödeme sisteminde sık olur), veritabanı eşit satırları sorgudan sorguya farklı sırada döndürebilir. Ana sorgu 20. sıraya A ödemesini koyar, iade sorgusu B'yi. Bir ödemenin iadeleri boş gelir ya da sayfada olmayan bir ödemenin iadeleri yüklenir. Hata almazsın, rapor sessizce yanlış olur.&lt;/p&gt;

&lt;p&gt;EF Core 10 bu konuda bir adım attı: sıralama tekil değilse primary key'i &lt;code&gt;ORDER BY&lt;/code&gt;'a kendisi ekliyor (&lt;a href="https://github.com/dotnet/efcore/issues/26808" rel="noopener noreferrer"&gt;dotnet/efcore#26808&lt;/a&gt;). Ama birkaç hafta önce açılan &lt;a href="https://github.com/dotnet/efcore/issues/39206" rel="noopener noreferrer"&gt;#39206&lt;/a&gt; numaralı kayda göre bir durumda düzeltme işlemiyor: split query, projeksiyon, &lt;code&gt;Skip&lt;/code&gt;/&lt;code&gt;Take&lt;/code&gt; ve projeksiyonda bir tekil ilişki (&lt;code&gt;p.Merchant.Name&lt;/code&gt; gibi) bir aradaysa sayfalama alt sorgusunda key yine yok ve bazı satırların koleksiyonları boş geliyor. Bu tarif bizim rapora birebir uyuyor.&lt;/p&gt;

&lt;p&gt;Hangi sürümde olursan ol, sıralamanın sonuna &lt;code&gt;ThenBy(p =&amp;gt; p.Id)&lt;/code&gt;'yi elle yaz. Hiçbir bedeli yok ve sürümün ne yaptığını düşünmek zorunda kalmazsın.&lt;/p&gt;

&lt;h2&gt;
  
  
  .NET 8'den 10'a geçince
&lt;/h2&gt;

&lt;p&gt;Biz bu süreçte .NET 8'den .NET 10'a da geçtik ve EF Core'un bazı ifadeleri SQL'e farklı çevirdiğini gördük.&lt;/p&gt;

&lt;p&gt;Belgelenmiş bir örnek: EF Core 10'da &lt;code&gt;ids.Contains(p.Id)&lt;/code&gt; gibi bir listeyle filtreleme artık varsayılan olarak tek bir JSON parametresi (&lt;code&gt;OPENJSON&lt;/code&gt;) yerine ayrı ayrı parametrelerle (&lt;code&gt;IN (@ids1, @ids2, ...)&lt;/code&gt;) çevriliyor. &lt;a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes" rel="noopener noreferrer"&gt;EF Core 10'un kırıcı değişiklikler listesinde&lt;/a&gt; "düşük etkili" diye geçiyor; ama rapor filtresinde binlerce id gönderiyorsan SQL Server'ın parametre sınırına yaklaşabilirsin.&lt;/p&gt;

&lt;p&gt;Ders basit: sürüm yükseltirken rapor sorgularının SQL'ine bir kez daha bak. LINQ aynı kalır, altındaki SQL değişebilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Excel'e alma hâlâ yavaş
&lt;/h2&gt;

&lt;p&gt;Ekrandaki rapor düzeldi, ama aynı raporun Excel'e alınması hâlâ yavaş. Orada sayfalama yok: 100 bin satırın üstünde veri tek seferde isteniyor ve &lt;code&gt;ToList()&lt;/code&gt; ile önce belleğe alınıyor, sonra dosyaya yazılıyor.&lt;/p&gt;

&lt;p&gt;Burada iki ayrı darboğaz var ve ikisini karıştırmamak gerekiyor. Birincisi veritabanı: 20 satırlık sayfada fark edilmeyen bir tarama, 100 bin satırda kendini gösterir. Bunun için &lt;code&gt;PaymentDate&lt;/code&gt;, &lt;code&gt;Status&lt;/code&gt; ve birkaç kolon daha üzerinde index düşünüyoruz; filtrenin ve sıralamanın kullandığı kolonlar. İkincisi bellek: bütün sonucu &lt;code&gt;ToList()&lt;/code&gt; ile listeye alıp sonra yazmak, satır sayısı kadar nesneyi aynı anda bellekte tutmak demek. EF Core'da &lt;code&gt;AsAsyncEnumerable()&lt;/code&gt; sonuçları satır satır akıtır; ama bunun faydası, Excel'i yazan kütüphanenin de satır satır yazabilmesine bağlı.&lt;/p&gt;

&lt;p&gt;Bunu henüz çözmedik.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kendi raporunda nasıl fark edersin?
&lt;/h2&gt;

&lt;p&gt;Rapor sorgunda &lt;code&gt;Include&lt;/code&gt; görüyorsan dur ve sor: bu entity'nin tamamına gerçekten ihtiyacım var mı? Sorgunun &lt;code&gt;ToQueryString()&lt;/code&gt; çıktısında JOIN sayısına ve &lt;code&gt;SELECT&lt;/code&gt;'teki kolon sayısına bak. Veritabanından dönen satır sayısı ekranda gösterdiğin satır sayısından fazlaysa koleksiyonlar satırları çarpıyordur. &lt;code&gt;Select&lt;/code&gt; içinde bir C# metodu çağırıyorsan da onu sayfalamadan sonraya al.&lt;/p&gt;

&lt;p&gt;Raporun sorusu birkaç kolondur; sorguyu o soruya göre yaz, entity modeline göre değil.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/ef-core-include-select" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>performance</category>
    </item>
    <item>
      <title>IDOR: ASP.NET Core'da [Authorize] neden yetmez?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:29:41 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/idor-aspnet-coreda-authorize-neden-yetmez-55f9</link>
      <guid>https://dev.to/huseyinbozdemir/idor-aspnet-coreda-authorize-neden-yetmez-55f9</guid>
      <description>&lt;p&gt;IDOR (Insecure Direct Object Reference), giriş yapmış bir kullanıcının URL'deki ya da istekteki id'yi değiştirerek kendisine ait olmayan bir kayda erişebilmesidir. Kendi yaptığım bir güvenlik testinde tam bunu gördüm: aitlik kontrolü her controller'da elle yazılmıştı ve bir endpoint'te unutulmuştu. Kimlik doğrulama kusursuz çalışıyordu. Kullanıcı giriş yapmıştı, token geçerliydi, &lt;code&gt;[Authorize]&lt;/code&gt; yerindeydi. Ama o endpoint, istenen kaydın o kullanıcıya ait olup olmadığına hiç bakmıyordu.&lt;/p&gt;

&lt;p&gt;IDOR, &lt;a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/" rel="noopener noreferrer"&gt;OWASP API güvenliği listesinde&lt;/a&gt; BOLA (Broken Object Level Authorization) adıyla birinci sırada duruyor. &lt;code&gt;GET /cards/{id}&lt;/code&gt; isteğinde kendi kartının id'sini başka bir kartınkiyle değiştirmek yetiyor. Yani eksik olan kimlik doğrulama değil, kaynak sahipliği kontrolü; bu yazıda ona kısaca aitlik diyeceğim.&lt;/p&gt;

&lt;p&gt;Bu yazı "id'yi kontrol et" demek için değil. Herkes id'yi kontrol etmesi gerektiğini biliyor. Asıl soru şu: bu kontrolü, yarın yeni bir endpoint yazan birinin unutamayacağı bir yere nasıl koyarsın?&lt;/p&gt;

&lt;h2&gt;
  
  
  Kimlik doğrulama "kimsin" der, "bu senin mi" demez
&lt;/h2&gt;

&lt;p&gt;Kurumsal kart yöneten bir API'den tipik bir endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cards/{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CardDto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[Authorize]&lt;/code&gt; burada tek bir soruya cevap veriyor: bu isteği atan, sistemin tanıdığı biri mi? Cevap evetse kapı açılıyor. Ama "bu kart bu kişiye mi ait?" sorusunu kimse sormuyor. Token'ı geçerli olan herkes, id'sini bildiği her kartı okuyabiliyor.&lt;/p&gt;

&lt;p&gt;Rol ve yetki kontrolü de bu boşluğu doldurmuyor. Bizde her endpoint bir izne (permission) bağlı ve kullanıcı tipi ne olursa olsun, admin bile olsa, o izne sahip değilse endpoint'e erişemiyor. Bu iyi bir kural, ama başka bir soruya cevap veriyor: "Bu işlemi yapabilir misin?" Kart görüntüleme iznine sahip bir kullanıcı, bu izin sayesinde başkasının kartını da görebiliyorsa sorun yine orada duruyor.&lt;/p&gt;

&lt;p&gt;Yani her istekte iki ayrı kontrol var ve ikisinin de geçmesi gerekiyor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;İşlem yetkisi:&lt;/strong&gt; Bu kullanıcı bu işlemi yapabilir mi? (rol, permission, policy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kaynak aitliği:&lt;/strong&gt; Bu kullanıcı &lt;em&gt;bu kayıt&lt;/em&gt; üzerinde bu işlemi yapabilir mi?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Her istekte iki ayrı kontrol: Token'lı istek önce işlem yetkisi kontrolünden geçer (kullanıcı bu işlemi yapabilir mi, endpoint'in bağlı olduğu izne bakılır; geçemezse 403 döner). Sonra kaynak aitliği kontrolünden geçer (kayıt kullanıcıya ait mi, kaydın departman id'si token'daki listede mi; geçemezse 403 ya da 404 döner). İkisi de geçerse kayıt döner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Framework'ler birincisi için hazır araç verir. İkincisi senin domain'ine ait, onu kimse senin yerine bilemez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aitlik tek seviyeli değildir
&lt;/h2&gt;

&lt;p&gt;"Kayıt kullanıcıya ait mi" diye düşününce iş kolay görünüyor: &lt;code&gt;card.UserId == currentUserId&lt;/code&gt;. Gerçek sistemlerde aitlik nadiren bu kadar düz.&lt;/p&gt;

&lt;p&gt;Şöyle bir örnek düşün: şirketlere kurumsal kart veren bir sistem. Bir şirketin üç departmanı var ve her departmanın çalışanlarına verilmiş kartlar var. Her kart ayrı bir kaynak. Kurallar şöyle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Şirket yöneticisi tüm departmanların kartlarını görebilir.&lt;/li&gt;
&lt;li&gt;Departman yöneticisi yalnızca kendi departmanının kartlarını görebilir; aynı şirketteki başka bir departmanın kartını göremez.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kart doğrudan kullanıcıya bağlı değil. Kart departmana ait, departman şirkete. Kullanıcı ise ya şirket adına ya da belirli bir departman adına işlem yapıyor. "Bu kart senin mi" sorusunun cevabı, kullanıcının hiyerarşide nerede durduğuna bağlı.&lt;/p&gt;

&lt;p&gt;Bunu çözmek için token'da kullanıcının ana kaynaklarını taşımak işe yarıyor; bizde de yaklaşım bu. Örnekte bu, şirket id'si ve erişilebilen departman id'leri demek. Şirket yöneticisinin token'ında üç departmanın id'si birden bulunuyor, departman yöneticisinin token'ında yalnızca kendi departmanı. Böylece aitlik sorusu tek bir kurala iniyor: kaydın &lt;code&gt;DepartmentId&lt;/code&gt;'si, token'daki departman listesinde var mı?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Şirket, departman ve kart aitlik hiyerarşisi: bir şirkete bağlı Departman A, B ve C var; her departmanın kendi kartları bulunuyor. Şirket yöneticisinin token'ında üç departmanın id'si de var ve tüm kartları görüyor. Departman B yöneticisinin token'ında yalnızca Departman B var ve yalnızca onun kartlarını görüyor.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICurrentUser&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CompanyId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DepartmentIds&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bunun bir bedeli var ve bilerek ödemen gerekiyor: token'daki liste, token'ın ömrü boyunca sabit kalıyor. Bir kullanıcının bir departmana erişimini kaldırdığında bu değişiklik, kullanıcının elindeki token süresi dolana kadar yansımıyor. Token ömrünü buna göre kısa tutmak ya da kritik işlemlerde listeyi kaynaktan tekrar doğrulamak gerekiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Her controller'a elle yazılan kontrol
&lt;/h2&gt;

&lt;p&gt;Başlangıçta aitlik kontrolü her action'ın içinde elle yazılıydı:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Forbid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tek başına bakınca bu kodda yanlış bir şey yok. Sorun, doğruluğunun tamamen hafızaya bağlı olması. Yüz endpoint varsa yüz kez hatırlaman gerekiyor. Yeni gelen biri, acil bir düzeltme, kopyalanıp kısaltılmış bir action: biri bir gün mutlaka atlıyor. Benim testte bulduğum da tam olarak buydu. Ortada karmaşık bir açık yoktu, sadece yazılmamış dört satır vardı.&lt;/p&gt;

&lt;p&gt;İlk adım olarak bu kontrolü merkezi bir metoda topladık:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;accessValidator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDepartmentAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// erişim yoksa exception&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tekrar azaldı, kural tek bir yerde toplandı. Ama asıl sorun yerinde duruyor: &lt;code&gt;ValidateDepartmentAccess&lt;/code&gt;'i çağırmak yine geliştiricinin aklına kalmış. Kaydı getirmek ve kontrol etmek hâlâ iki ayrı adım ve ikincisini atlasan da kod derleniyor, test geçiyor, endpoint çalışıyor.&lt;/p&gt;

&lt;p&gt;Kontrolü daha da merkeze, örneğin bir filter'a ya da middleware'e taşımak istediğinde ise başka bir duvara çarpıyorsun: erişilmeye çalışılan kaynağın tipini nasıl bilecek? Middleware URL'de bir &lt;code&gt;Guid&lt;/code&gt; görüyor. O id'nin bir karta mı, bir işleme mi, bir departmana mı ait olduğunu, hangi tabloya bakacağını ve hangi alanın sahibi gösterdiğini bilmiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Önerdiğim yapı: kaynak kendi sahibini söylesin
&lt;/h2&gt;

&lt;p&gt;Bu sorunun cevabı, bilgiyi kontrol eden tarafa değil, kaynağın kendisine koymak. Hangi alanın sahibi gösterdiğini entity'nin kendisi söylerse, generic bir kontrol her kaynak için çalışabilir.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marker interface
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IEntity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDepartmentScoped&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEntity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;DepartmentId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDepartmentScoped&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;DepartmentId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;HolderName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsFrozen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Artık "bu kaynağın sahibi kim" sorusunun cevabı tipte yazıyor. Şirket seviyesinde sahiplenilen kaynaklar için aynı şekilde bir &lt;code&gt;ICompanyScoped&lt;/code&gt; tanımlayabilirsin. Önemli olan, bir kaynağın hangi seviyede sahiplenildiğini derleyicinin de görebilmesi.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tek sorguda "var mı ve senin mi"
&lt;/h3&gt;

&lt;p&gt;Bu interface'in üzerine generic bir koruma yazınca, kaydı getirmek ile aitliği kontrol etmek tek bir çağrıya dönüşüyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OwnershipGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetOwnedAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IDepartmentScoped&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;departmentIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentIds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;departmentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResourceNotAccessibleException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal akışta kullanıcı kendi kaydını istediği için bu exception seyrek fırlar; &lt;a href="https://huseyinbozdemir.com/blog/csharp-exception-performansi" rel="noopener noreferrer"&gt;exception'ın maliyeti&lt;/a&gt; seyrek durumda hissedilmez.&lt;/p&gt;

&lt;p&gt;Controller da şu hale geliyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cards/{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetOwnedAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CardDto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fark küçük görünüyor ama önemli. Eskiden kontrolü unutmak için bir satırı yazmamak yeterliydi. Şimdi kontrolü unutmak için kaydı da getirmemen gerekiyor. Kaydı getirmenin yolu guard'dan geçiyorsa, aitlik kontrolü o yolun bir parçası oluyor, ayrı bir adım olmaktan çıkıyor.&lt;/p&gt;

&lt;p&gt;Bunu tam anlamıyla işe yaratmak için bir kural daha gerekiyor: controller'lar &lt;code&gt;DbContext&lt;/code&gt;'e doğrudan dokunmamalı. Yoksa biri yine &lt;code&gt;db.Cards.FindAsync&lt;/code&gt; yazar ve guard'ı hiç çağırmaz. Bunu code review'a bırakmak yerine bir mimari testle de zorlayabilirsin: "API katmanındaki hiçbir tip &lt;code&gt;AppDbContext&lt;/code&gt;'e bağımlı olamaz" gibi bir kural.&lt;/p&gt;

&lt;p&gt;Sorgunun yapısına da dikkat et. "Var mı" ve "senin mi" tek bir &lt;code&gt;WHERE&lt;/code&gt; ile soruluyor. Önce kaydı getirip sonra sahibine bakmıyorsun, başkasının kaydı belleğe hiç gelmiyor. Bunun 403/404 tartışmasında da işine yarayacak bir yan etkisi var, aşağıda değineceğim.&lt;/p&gt;

&lt;h3&gt;
  
  
  ASP.NET Core'un resource-based authorization'ı neden yetmez
&lt;/h3&gt;

&lt;p&gt;ASP.NET Core'un bu iş için kendi aracı da var: &lt;a href="https://learn.microsoft.com/aspnet/core/security/authorization/resourcebased" rel="noopener noreferrer"&gt;resource-based authorization&lt;/a&gt;. Kuralı bir &lt;code&gt;AuthorizationHandler&lt;/code&gt; içine yazıyorsun, controller'da kaydı getirdikten sonra &lt;code&gt;IAuthorizationService&lt;/code&gt; ile kontrol ediyorsun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authorizationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AuthorizeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SameDepartment"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Forbid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kuralı tek bir yerde topladığı için iyi bir araç. Ama yapısı yukarıdaki &lt;code&gt;ValidateDepartmentAccess&lt;/code&gt; ile aynı: önce kaydı getiriyorsun, sonra kontrolü çağırıyorsun. Çağırmayı unutursan kod yine derleniyor, endpoint yine çalışıyor. Üstelik kayıt kontrolden önce belleğe geliyor; yok ile senin değil arasındaki fark, 404 ve 403 olarak istemciye de yansıyor. Handler'ı kural sınıfı olarak kullanmaya devam edebilirsin, ama kontrolün çağrılmasını geliştiricinin hafızasına bırakmamak için kaydı getiren yolun içine koyman gerekiyor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global query filter neden tek başına yetmez
&lt;/h3&gt;

&lt;p&gt;EF Core'da aynı işi &lt;a href="https://learn.microsoft.com/ef/core/querying/filters" rel="noopener noreferrer"&gt;global query filter&lt;/a&gt; ile de yapabilirsin. &lt;code&gt;IDepartmentScoped&lt;/code&gt; uygulayan her entity'ye &lt;code&gt;DepartmentId&lt;/code&gt;'yi token'daki listeyle süzen bir filtre eklersin, her sorgu otomatik olarak filtrelenir. Kâğıt üzerinde daha da güvenli görünüyor çünkü çağırmayı unutabileceğin bir şey bile kalmıyor.&lt;/p&gt;

&lt;p&gt;Pratikte bu filtreyi son savunma hattı olarak görmek daha doğru:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bir yerde &lt;code&gt;IgnoreQueryFilters()&lt;/code&gt; çağrıldığında, örneğin bir admin ekranı için, filtre o sorguda tamamen devre dışı kalıyor. Bunu kodda aramak zorundasın.&lt;/li&gt;
&lt;li&gt;Dapper ile ya da stored procedure ile yazılmış raporlama sorguları filtreyi hiç görmüyor.&lt;/li&gt;
&lt;li&gt;Filtre görünmez çalışıyor. Kaydın neden gelmediğini anlamaya çalışan bir geliştirici önce veriyi, sonra sorguyu kontrol ediyor, filtre en son aklına geliyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Filtreyi açarsan zarar etmez, bir katman daha eklemiş olursun. Ama "aitlik nerede kontrol ediliyor?" sorusunun cevabı açık ve okunabilir bir yerde olmalı. O yer de kaydı getiren çağrının kendisi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Body'deki sahip id'sine güvenme
&lt;/h2&gt;

&lt;p&gt;Aitlik sadece URL'deki id ile ilgili değil. İstek gövdesi de aynı riski taşıyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;IssueCardRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CompanyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;HolderName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İstemci &lt;code&gt;CompanyId&lt;/code&gt; olarak başka bir şirketin id'sini gönderirse ne olacak? Bizde bu soru tek bir kuralla kapanıyor: body'de sahip id'si gelse bile token'daki değer esas alınır. İstemcinin gönderdiği değer ezilir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IssueCardCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CompanyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompanyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// body'deki değer değil&lt;/span&gt;
    &lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HolderName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HolderName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResourceNotAccessibleException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Department&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada iki farklı durum var. &lt;code&gt;CompanyId&lt;/code&gt; gibi kullanıcının tek bir değeri olan alanlar doğrudan token'dan alınır, istemcinin ne gönderdiğinin önemi yoktur. &lt;code&gt;DepartmentId&lt;/code&gt; gibi birden fazla geçerli değeri olabilen alanlar ise ezilemez, token'daki listeye karşı doğrulanır. En temizi, ezeceğin alanı request DTO'suna hiç koymamak. Ama eski bir sözleşme yüzünden alan oradaysa, ona güvenme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mikroservis ve kuyruk tarafı
&lt;/h2&gt;

&lt;p&gt;Mikroservislerde aitlik bilgisinin yolda kaybolması kolay. Gateway token'ı doğrular ve isteği arkaya iletir. Arkadaki servis "gateway zaten kontrol etti" diye düşünebilir. Ama gateway kimlik doğrulamasını yapabilir, aitliği yapamaz: kart kaydını görmüyor, hangi departmana ait olduğunu bilmiyor. Aitlik sorusunu ancak verinin sahibi olan servis cevaplayabilir.&lt;/p&gt;

&lt;p&gt;Bu yüzden ana kaynakları token'da taşımak burada da işe yarıyor. Kullanıcının id'si, bağlı olduğu şirket ve departman id'leri token'da olduğu için her servis, aitlik sorusunu kendi verisine karşı, başka bir servise sormadan cevaplayabiliyor.&lt;/p&gt;

&lt;p&gt;Asenkron akışta durum biraz daha ince. Bizde aitlik kontrolü genellikle mesaj publish edilmeden önce yapılmış oluyor. Buna rağmen consumer mesajdaki id'ye körü körüne güvenmiyor. Kullanıcı bağlamı mesajla birlikte taşınıyor ve consumer, mesaj gövdesindeki sahip id'sini bu bağlamdaki değerle ezip kaynağın gerçekten var olup olmadığına bakıyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CompanyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DepartmentIds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CardFreezeRequested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CardId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserContext&lt;/span&gt; &lt;span class="n"&gt;RequestedBy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardFreezeRequestedHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IMessageHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardFreezeRequested&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CardFreezeRequested&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;departmentIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestedBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentIds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cards&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CardId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;departmentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepartmentId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// logla; böyle bir kaynak yok ya da bu kullanıcıya ait değil&lt;/span&gt;

        &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsFrozen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publisher'da kontrol edilmişken bunu consumer'da tekrar yapmak gereksiz görünebilir. Ama mesaj, publish ile consume arasında bir sınırı geçiyor. Yarın aynı kuyruğa başka bir servis de mesaj atmaya başlayabilir, ya da biri bir mesajı elle yeniden kuyruğa koyabilir. Consumer'ın "bu mesaj güvenilir bir yerden geldi" varsayımı, kontrolü atlamak için yeterli bir sebep değil. Kontrol ucuz, unutulduğunda ortaya çıkan sonuç pahalı.&lt;/p&gt;

&lt;h2&gt;
  
  
  GUID seni korumaz
&lt;/h2&gt;

&lt;p&gt;IDOR konuşulurken mutlaka biri şunu söyler: "Bizde id'ler GUID, kimse tahmin edemez." Bizde de id'ler GUID'di. Unutulmuş kontrol yine de oradaydı.&lt;/p&gt;

&lt;p&gt;GUID'in tahmin edilmesi zor, bu doğru. Ama bir saldırganın id'yi tahmin etmesine gerek yok, bulması yeterli. GUID'ler sanıldığından çok daha fazla yere sızıyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL'lerde taşındıkları için tarayıcı geçmişine, proxy ve &lt;a href="https://huseyinbozdemir.com/blog/log-maskeleme-kart-numarasi" rel="noopener noreferrer"&gt;uygulama loglarına&lt;/a&gt; düşüyorlar.&lt;/li&gt;
&lt;li&gt;E-postayla ya da mesajla paylaşılan linklerin içinde dolaşıyorlar.&lt;/li&gt;
&lt;li&gt;Destek talebine eklenen ekran görüntülerinde görünüyorlar.&lt;/li&gt;
&lt;li&gt;Başka bir endpoint'in yanıtında ilişkili kaydın id'si olarak dönüyorlar. Bir departmanın görebildiği bir harcama kaydı, başka bir kaynağın id'sini taşıyabiliyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Daha önemlisi, id'nin tahmin edilemez olması tasarımın bilinçli bir güvenlik özelliği değil, bir yan etkisi. Yarın biri performans için sıralı bir id şemasına geçerse, aitlik kontrolünü GUID'e emanet ettiğin her yerde güvenliğin de onunla birlikte gider. GUID saldırıyı zorlaştırır, yetkilendirmenin yerini tutmaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  403 mü, 404 mü?
&lt;/h2&gt;

&lt;p&gt;Başkasına ait bir kayda erişilmeye çalışıldığında ne dönmeli? Bizde mimari tercih 403 Forbidden'dı. Ben 404 Not Found'un daha sağlıklı olduğunu düşünüyorum.&lt;/p&gt;

&lt;p&gt;403, "bu kayıt var ama senin değil" demek. Yani kaydın varlığını doğruluyor. Elinde bir id listesi olan biri, hangilerinin gerçek olduğunu öğrenebiliyor. 404 ise iki durumu ayırt etmiyor: kayıt ya yok ya da senin değil. &lt;a href="https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api" rel="noopener noreferrer"&gt;GitHub'ın, erişimin olmayan private repository'ler için 404 dönmesinin&lt;/a&gt; sebebi de bu.&lt;/p&gt;

&lt;p&gt;Aslında asıl sızıntı 403 ya da 404 seçiminde değil, bu ikisinin farklı durumlarda dönmesinde. Var olmayan kayda 404, başkasının kaydına 403 dönen bir API, ikisinin farkını açıkça söylüyor. Yukarıdaki &lt;code&gt;GetOwnedAsync&lt;/code&gt; tek sorguyla çalıştığı için iki durumu zaten ayırt edemiyor ve ikisine de aynı yanıtı dönüyor. Hangi kodu seçtiğin, tutarlı olmaktan daha az önemli.&lt;/p&gt;

&lt;p&gt;404'ün de bir bedeli var. İstemci ya da destek ekibi "kayıt yok mu, yoksa yetkim mi yok?" ayrımını yanıttan yapamıyor. Bu ayrımı API yanıtında değil, sunucu tarafındaki logda tutman gerekiyor. Logda gerçek sebebi açıkça yaz, istemciye ise genel yanıtı dön.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kendi API'ni nasıl test edersin
&lt;/h2&gt;

&lt;p&gt;İki kullanıcıyla giriş yap. A'nın kaydının id'sini B'nin token'ıyla iste ve 404 bekle. Bunu elle bir kez yapmak açığı bulur; integration testlerine her endpoint için ekleyince açığın geri gelmesini de engeller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okura kalan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[Authorize]&lt;/code&gt; ve rol kontrolü "bu işlemi yapabilir misin" sorusunu cevaplar. "Bu kayıt senin mi" ayrı bir soru ve onu ayrıca sorman gerekir.&lt;/li&gt;
&lt;li&gt;Aitlik kontrolünü her endpoint'e elle yazarsan bir gün biri unutur. Kontrolü, kaydı getiren çağrının parçası yap ki unutmak için kaydı da getirmemek gereksin.&lt;/li&gt;
&lt;li&gt;Kaynağın sahibini entity'nin kendisine söylet (&lt;code&gt;IDepartmentScoped&lt;/code&gt;). Generic bir kontrol ancak tipi tanıyabilirse çalışır.&lt;/li&gt;
&lt;li&gt;Body'deki ve kuyruk mesajındaki sahip id'lerine güvenme. Tek değerli olanları token'dan al, çok değerli olanları token'a karşı doğrula.&lt;/li&gt;
&lt;li&gt;GUID id'yi bulmayı zorlaştırır, yetkilendirmenin yerini tutmaz. 403 ya da 404, hangisini seçersen seç, iki durumu birbirinden ayırt ettirme.&lt;/li&gt;
&lt;li&gt;Daha fazlası için: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP IDOR Prevention Cheat Sheet&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/api-kaynak-aitligi-idor" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>security</category>
      <category>csharp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Idempotency nedir? Ödemede para iki kez çekilmesin</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:29:36 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/idempotency-nedir-odemede-para-iki-kez-cekilmesin-6k4</link>
      <guid>https://dev.to/huseyinbozdemir/idempotency-nedir-odemede-para-iki-kez-cekilmesin-6k4</guid>
      <description>&lt;p&gt;Bir kullanıcı fatura ödemek için butona iki kez bastı. İki istek de bankaya gitti. Banka ilkini işledi, ikincisini aynı fatura zaten ödendiği için reddetti. Para bir kez çekildi. Teknik olarak her şey yolundaydı. Kullanıcının ekranında kalan son mesaj ise ikinci isteğin cevabıydı: işlem başarısız. Parası çekilmişti ama ödemesinin geçmediğini düşündü.&lt;/p&gt;

&lt;p&gt;Kısa cevap: idempotency, aynı istek bir kez de gelse on kez de gelse sistemde bıraktığı etkinin aynı kalmasıdır. Ödeme API'lerinde bundan bir adım fazlası beklenir: ikinci isteğin, ilk isteğin cevabını da alması. Bunu istemcinin her ödeme niyeti için ürettiği bir idempotency key sağlar.&lt;/p&gt;

&lt;p&gt;Bu olay bana idempotency'nin yarısını bildiğimizi gösterdi. İlk yarısı, yani paranın bir kez çekilmesi, bankanın sayesinde tutmuştu. İkinci yarısı hiç yoktu. Paranın bir kez çekilmesini sağlamak işin kolay tarafı. Zor olan, ikinci isteğe doğru cevabı verebilmek ve bankadan hiç cevap gelmediğinde ne yapacağını bilmek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency nedir, tekrarı reddetmekten farkı ne
&lt;/h2&gt;

&lt;p&gt;Idempotency, bir işlemin bir kez de yapılsa birden fazla kez de yapılsa sistemde aynı etkiyi bırakmasıdır. HTTP tarafında bu kavram zaten tanımlı: &lt;a href="https://www.rfc-editor.org/rfc/rfc9110#name-idempotent-methods" rel="noopener noreferrer"&gt;RFC 9110&lt;/a&gt;'a göre &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt; ve &lt;code&gt;DELETE&lt;/code&gt; idempotent, &lt;code&gt;POST&lt;/code&gt; değil. Ödeme başlatmak neredeyse her zaman bir &lt;code&gt;POST&lt;/code&gt;. Yani protokol sana bir garanti vermiyor; idempotency'yi senin kurman gerekiyor.&lt;/p&gt;

&lt;p&gt;Aynı ödeme isteği ikinci kez geldiğinde bir sistem üç şekilde davranabilir:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;İkinci istek geldiğinde&lt;/th&gt;
&lt;th&gt;Para&lt;/th&gt;
&lt;th&gt;Kullanıcının gördüğü&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hiç koruma yok&lt;/td&gt;
&lt;td&gt;İki kez çekilir&lt;/td&gt;
&lt;td&gt;İki başarılı ödeme&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tekrar reddedilir&lt;/td&gt;
&lt;td&gt;Bir kez çekilir&lt;/td&gt;
&lt;td&gt;Hata mesajı&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency key ile&lt;/td&gt;
&lt;td&gt;Bir kez çekilir&lt;/td&gt;
&lt;td&gt;İlk ödemenin sonucu&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Benim hikâyemdeki banka ikinci satırdaydı. HTTP'nin tanımıyla bu davranış idempotent sayılır: RFC 9110 yalnızca isteğin sunucudaki etkisine bakar, cevabın aynı olmasını şart koşmaz. &lt;code&gt;DELETE&lt;/code&gt; de idempotenttir, ama ikinci çağrı 404 dönebilir.&lt;/p&gt;

&lt;p&gt;Ödeme API'lerinde bu yetmiyor. Stripe ve IETF taslağı, tamamlanmış bir isteğin tekrarına ilk işlemin sonucunun dönülmesini bekliyor; başarılı da olsa hata da olsa. Tekrarı reddetmek etkiyi korur, kullanıcıyı korumaz. Kullanıcı açısından "ödemen alınamadı" ile "ödemen zaten alındı" arasında büyük fark var ve ikinci isteğe ilkinin sonucunu dönmeyen bir sistem bu farkı kullanıcıya bırakıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tekrar nereden gelir
&lt;/h2&gt;

&lt;p&gt;Çift tıklama en görünür kaynak, ama tek kaynak değil. Kullanıcı bir kez bassa bile aynı ödeme sistemin içinde birkaç yerde çoğalabiliyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kullanıcı:&lt;/strong&gt; Butona iki kez basıyor ya da sayfa yanıt vermiyor diye yeniliyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;İstemci:&lt;/strong&gt; Mobil uygulama ya da frontend, cevap gelmeyince isteği kendiliğinden tekrar deniyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ara katman:&lt;/strong&gt; Gateway ya da load balancer, arkadaki servisten cevap alamayınca isteği başka bir instance'a yeniden gönderiyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mesaj kuyruğu:&lt;/strong&gt; Consumer mesajı işledi ama ack göndermeden çöktü. Broker mesajı tekrar teslim ediyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://huseyinbozdemir.com/blog/outbox-pattern#her-i%C5%9F-tekrar-denenmez" rel="noopener noreferrer"&gt;Arka plan işleri&lt;/a&gt;:&lt;/strong&gt; Hangfire'da hata alan job otomatik olarak yeniden çalışıyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Her katmanın tekrar denemesi kendi açısından doğru bir karar. Sorun, bu kararların hepsinin aynı para hareketine dokunabilmesi. Bu yüzden korumayı tek bir katmana koyup rahatlamak işe yaramıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kilit tekrarı durdurur, sonucu vermez
&lt;/h2&gt;

&lt;p&gt;Olaydan sonra ilk yaptığımız şey, aynı faturaya aynı anda iki ödemenin girmesini engellemekti. Fatura üzerinden bir Redis lock koyduk. İkinci istek kilidi bekliyor, kilit açılınca veritabanında bu fatura için ödeme olup olmadığına bakıyor ve varsa isteği reddediyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invoices/{invoiceId:guid}/payments"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;locks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AcquireAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"invoice-payment:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;alreadyPaid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvoiceId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;invoiceId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alreadyPaid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bu fatura için ödeme zaten var."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentDto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kilit iki isteği sıraya koyuyor, veritabanı kontrolü ikincisini durduruyor. Bankaya artık tek istek gidiyor ve kullanıcının "başarısız" sanma sorunu çözüldü.&lt;/p&gt;

&lt;p&gt;Ama sorun başka bir kılığa girdi. İkinci istek kilidi beklediği için kullanıcının ekranında yükleniyor işareti uzun süre döndü ve kullanıcı bu sefer işlemin uzun sürdüğünü sandı. Kilit tekrarı durduruyor ama ikinci isteğe söyleyecek bir şeyi yok. Bildiği tek şey "bu fatura için zaten bir ödeme var".&lt;/p&gt;

&lt;p&gt;Kilidin bir de kendi sınırları var. Süresi işlemden kısa kalırsa kilit sen işlemi bitirmeden açılıyor. Redis'e ulaşılamazsa koruma tamamen ortadan kalkıyor. Bu yüzden kilidi bir hızlandırıcı, asıl garantiyi ise veritabanı kısıtı olarak görmek gerekiyor. Ona aşağıda geleceğim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency key: ikinci istek ilkinin sonucunu alsın
&lt;/h2&gt;

&lt;p&gt;Bu yüzden kilidin üstüne bir katman daha koyduk: idempotency key. Kilit aynı faturaya iki ödemenin girmesini engellemeye devam ediyor; anahtar ise ikinci isteğe doğru cevabı veriyor.&lt;/p&gt;

&lt;p&gt;Fikir basit. İstemci her ödeme niyeti için benzersiz bir anahtar üretiyor ve isteği &lt;code&gt;Idempotency-Key&lt;/code&gt; başlığıyla gönderiyor. Kullanıcı butona iki kez basarsa ya da istemci isteği tekrar denerse anahtar aynı kalıyor. Sunucu bu anahtarı, isteğin özetini ve sonunda verdiği cevabı saklıyor. Aynı anahtar tekrar geldiğinde işlemi yeniden yapmıyor, sakladığı cevabı dönüyor.&lt;/p&gt;

&lt;p&gt;Bunun için IETF'te bir &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/" rel="noopener noreferrer"&gt;standart taslağı&lt;/a&gt; var ve &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe gibi ödeme sağlayıcıları&lt;/a&gt; yıllardır bu şekilde çalışıyor. Anahtarın ömrü sınırsız olmak zorunda değil; Stripe anahtarları en az 24 saat sonra silinebilir sayıyor.&lt;/p&gt;

&lt;p&gt;Bir istek geldiğinde sunucunun dört durumu ayırt etmesi gerekiyor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anahtar ilk kez geliyor:&lt;/strong&gt; Kaydı "işleniyor" olarak ekle, ödemeyi yap, cevabı kaydet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anahtar tamamlanmış:&lt;/strong&gt; İşlemi yapma, kaydedilmiş cevabı dön. Çift tıklayan kullanıcı ilk ödemenin sonucunu görüyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anahtar hâlâ işleniyor:&lt;/strong&gt; İlk istek bitmedi. &lt;code&gt;409 Conflict&lt;/code&gt; dön; istemci biraz sonra aynı anahtarla tekrar sorar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anahtar aynı ama istek farklı:&lt;/strong&gt; Aynı anahtar başka bir tutarla gelmiş. &lt;code&gt;422&lt;/code&gt; dön; bu bir tekrar değil, istemci hatası.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ASP.NET Core'da bunu bir &lt;a href="https://learn.microsoft.com/aspnet/core/mvc/controllers/filters" rel="noopener noreferrer"&gt;action filter&lt;/a&gt; olarak yazabilirsin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncActionFilter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnActionExecutionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ActionExecutingContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ActionExecutionDelegate&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key başlığı zorunlu."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IdempotencyRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActionArguments&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// (UserId, Key) unique: aynı anda gelen ikinci istek burada düşer&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbUpdateException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InnerException&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PostgresException&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;SqlState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PostgresErrorCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UniqueViolation&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Detached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fingerprint&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Fingerprint&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UnprocessableEntityObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bu anahtar farklı bir istekle kullanılmış."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;CompletedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConflictObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"İlk istek hâlâ işleniyor."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ContentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;executed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ObjectResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// kendi hatamız: istemci aynı anahtarla tekrar deneyebilsin&lt;/span&gt;

        &lt;span class="nc"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHexString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HashData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToDictionary&lt;/span&gt;&lt;span class="p"&gt;())));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kodda birkaç karar var ve her biri bilerek verilmeli:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anahtar kullanıcıyla birlikte saklanıyor.&lt;/strong&gt; Unique kısıt &lt;code&gt;(UserId, Key)&lt;/code&gt; üzerinde. Yalnızca anahtara bakarsan, başka bir kullanıcının anahtarını bilen biri onun ödeme cevabını alabilir. Bu da başka bir kılıkta &lt;a href="https://huseyinbozdemir.com/blog/api-kaynak-aitligi-idor" rel="noopener noreferrer"&gt;IDOR&lt;/a&gt; olur.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eşzamanlılığı kısıt çözüyor, kontrol değil.&lt;/strong&gt; "Önce bak, yoksa ekle" yazarsan aynı anda gelen iki istek ikisi de "yok" görür. Ekleyip kısıtın patlamasını beklemek, iki isteği veritabanının sıraya koymasını sağlıyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hangi cevabın saklanacağı domain kararı.&lt;/strong&gt; Banka ödemeyi reddettiyse bu ödemenin sonucu; saklanır ve aynı anahtarla gelen herkes aynı reddi görür. Bizim servisimiz çöktüyse bu bir sonuç değil; kayıt silinir ve istemci tekrar deneyebilir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anahtarın &lt;a href="https://huseyinbozdemir.com/blog/idempotency-key-istemcide-uretme" rel="noopener noreferrer"&gt;istemcide nerede üretildiği&lt;/a&gt; de önemli. Butona her basışta yeni anahtar üretirsen hiçbir şey kazanmazsın. Anahtar, ödeme ekranı açılırken bir kez üretilmeli ve o ödeme niyetinin bütün denemelerinde aynı kalmalı.&lt;/p&gt;

&lt;p&gt;NuGet'te bunu hazır yapan paketler de var. Kullanabilirsin, ama yukarıdaki üç kararı paket senin yerine veremez. Hangisini nasıl verdiğine bakmadan eklersen, hata cevaplarını da saklayan ve kullanıcıyı sonsuza dek aynı hataya kilitleyen bir sistem kurabilirsin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumer tarafı: aynı mesaj iki kez gelirse
&lt;/h2&gt;

&lt;p&gt;API katmanı istemciden gelen tekrarı çözüyor. Ödeme bir kuyruğa düştükten sonra ise tekrar başka yerden geliyor: RabbitMQ gibi broker'lar mesajı "en az bir kez" teslim eder. Consumer bankayı çağırıp ack göndermeden çökerse aynı mesaj tekrar gelir.&lt;/p&gt;

&lt;p&gt;Literatürde buna &lt;a href="https://microservices.io/patterns/communication-style/idempotent-consumer.html" rel="noopener noreferrer"&gt;idempotent consumer&lt;/a&gt; deniyor ve genelde işlenen mesajları tutan ayrı bir inbox tablosuyla anlatılıyor. Bizde ayrı bir tablo yok; işin kendi durumu ve bir unique constraint aynı işi görüyor.&lt;/p&gt;

&lt;p&gt;Durum kontrolünün tek bir tuzağı var: önce okuyup sonra yazarsan iki consumer aynı anda "Oluşturuldu" görür ve ikisi de bankayı çağırır. Kontrolü okuma ve yazma olarak değil, &lt;a href="https://learn.microsoft.com/ef/core/saving/execute-insert-update-delete" rel="noopener noreferrer"&gt;tek bir koşullu update&lt;/a&gt; olarak yapmak gerekiyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentRequested&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteUpdateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SentToBank&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// başka bir consumer almış ya da zaten işlenmiş&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PayAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExecuteUpdateAsync&lt;/code&gt; etkilenen satır sayısını döndürüyor. Satırı yalnızca bir consumer "Oluşturuldu"dan "Bankaya iletildi"ye çekebiliyor; diğeri sıfır alıp çıkıyor.&lt;/p&gt;

&lt;p&gt;Unique constraint ise son savunma hattı. Aynı fatura için başarısız olmamış tek bir ödeme olabilir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsUnique&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"Status\" &amp;lt;&amp;gt; 'Failed'"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kilit atlansa, filter yanlış yazılsa, consumer'da bir hata olsa bile veritabanı aynı faturaya ikinci bir canlı ödeme yazılmasına izin vermiyor. Filtredeki &lt;code&gt;Failed&lt;/code&gt; istisnası da önemli: başarısız bir ödemeden sonra kullanıcı aynı faturayı tekrar ödeyebilmeli.&lt;/p&gt;

&lt;h2&gt;
  
  
  Banka timeout'u: sonucu bilinmeyen çağrı
&lt;/h2&gt;

&lt;p&gt;Buraya kadarki her şey, bizim kendi veritabanımızın sınırları içinde çalışıyor. Banka çağrısı bu sınırın dışında. İstek gitti, cevap gelmedi. Para çekildi mi? Bilmiyorsun.&lt;/p&gt;

&lt;p&gt;İlk refleks bu ödemeyi başarısız kaydetmek oluyor. Ama yukarıdaki unique constraint'e bak: başarısız ödeme kısıtın dışında. Ödemeyi başarısız kaydedersen kullanıcı aynı faturayı tekrar ödeyebilir. Banka ilk isteği aslında işlediyse para iki kez çekilir. Timeout'u başarısızlık saymak, bütün koruma katmanlarını kendi elinle açmak demek.&lt;/p&gt;

&lt;p&gt;Bu yüzden bizde ödeme bankaya gönderildiği anda "Bankaya iletildi" durumuna geçiyor ve sonuç gelene kadar orada kalıyor. Banka cevap verirse ödeme Başarılı ya da Başarısız oluyor. Cevap gelmezse arka planda 15 dakikada bir çalışan bir Hangfire job'ı bekleyen ödemeleri bankaya soruyor:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bir ödemenin durumları: Ödeme Oluşturuldu durumunda başlar. Bankaya gönderilince Bankaya iletildi durumuna geçer; sonuç henüz bilinmiyor. Banka cevap verirse ya da periyodik durum sorgusu sonuç getirirse Başarılı ya da Başarısız olur; banka işlemi hiç bulamazsa da Başarısız olur. Cevap gelmezse ödeme Bankaya iletildi durumunda kalır ve bir sonraki sorguda tekrar sorulur.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;RecurringJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOrUpdate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PendingPaymentsJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"pending-payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"*/15 * * * *"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PendingPaymentsJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IBankGateway&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SentToBank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;BankQueryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Approved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkSucceeded&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;BankQueryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rejected&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;BankQueryResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// bulunamadı: istek bankaya hiç ulaşmamış&lt;/span&gt;
            &lt;span class="c1"&gt;// sonuç hâlâ belirsizse ödeme olduğu yerde kalır, bir sonraki turda tekrar sorulur&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sorgunun üçüncü bir cevabı daha var: banka işlemi hiç bulamayabilir. Bu, isteğin bankaya ulaşmadığı anlamına geliyor ve ödeme "alınamadı" olarak kapanıyor. Kullanıcı faturayı tekrar ödeyebiliyor, çünkü çekilmiş bir para yok. Burada güvendiğin tek şey, bankanın "bulunamadı" cevabının gerçekten "istek bana hiç gelmedi" demek olması. Bu yüzden bu cevabı her bankanın dokümanında ve testte ayrıca doğrulamak gerekiyor.&lt;/p&gt;

&lt;p&gt;Bu sorgunun çalışabilmesi için bir şart var: bankaya giden her isteğin bizim ürettiğimiz ve tekrar denemelerde değişmeyen bir referansı olmalı. Yazının başındaki bankanın ikinci isteği reddedebilmesi de aynı fikre dayanıyordu: iki istek aynı faturayı taşıyordu. Referans, banka tarafındaki idempotency key'in ta kendisi.&lt;/p&gt;

&lt;p&gt;Bu durumu kullanıcıya da dürüstçe göstermek gerekiyor. "Bankaya iletildi" durumundaki bir ödeme için kullanıcıya "başarısız" değil, "sonuç bekleniyor" demek gerekiyor. Aksi hâlde yazının başındaki kullanıcıyı bu kez kendi elinle yaratırsın.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kendi sisteminde nasıl test edersin
&lt;/h2&gt;

&lt;p&gt;Idempotency'yi elle test etmek zor, çünkü tekrarlar zamanlamaya bağlı. Birkaç integration testi bu açıkları önceden yakalar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aynı ödeme isteğini &lt;code&gt;Task.WhenAll&lt;/code&gt; ile aynı anda iki kez gönder. Veritabanında tek ödeme olmalı, banka sahtesi bir kez çağrılmalı ve iki cevap aynı olmalı.&lt;/li&gt;
&lt;li&gt;Aynı &lt;code&gt;Idempotency-Key&lt;/code&gt; ile farklı tutar gönder. &lt;code&gt;422&lt;/code&gt; bekle.&lt;/li&gt;
&lt;li&gt;Consumer'a aynı mesajı iki kez ver. Banka sahtesi bir kez çağrılmalı.&lt;/li&gt;
&lt;li&gt;Banka sahtesini timeout verecek şekilde ayarla. Ödeme "Bankaya iletildi"de kalmalı, başarısıza düşmemeli ve aynı fatura için yeni bir ödeme açılamamalı.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Okura kalan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tekrarı reddetmek etkiyi korur, kullanıcıyı korumaz. İkinci istek, ilk isteğin sonucunu dönmeli; yoksa kullanıcı parası çekilmiş bir ödemeyi başarısız sanır.&lt;/li&gt;
&lt;li&gt;Tekrar tek bir yerden gelmiyor. Kullanıcı, istemci, gateway, broker ve arka plan işleri ayrı ayrı tekrar dener; her katmanın kendi koruması olmalı.&lt;/li&gt;
&lt;li&gt;Kilit tekrarı sıraya koyar, cevap vermez. Asıl garanti veritabanındaki unique constraint'te; idempotency key ise ikinci isteğe doğru cevabı verir.&lt;/li&gt;
&lt;li&gt;Durum kontrolünü okuma ve yazma olarak değil, tek bir koşullu update olarak yap.&lt;/li&gt;
&lt;li&gt;Timeout bir sonuç değil. Cevabı gelmeyen ödemeyi başarısız kaydetme; sonuç bilinene kadar ara bir durumda tut ve sor.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/idempotency-odeme-sistemleri" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>fintech</category>
      <category>payments</category>
      <category>csharp</category>
    </item>
    <item>
      <title>C# exception performansı: .NET 10 ölçümü</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:29:27 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/cta-exception-pahali-mi-net-10-olcumleri-fgk</link>
      <guid>https://dev.to/huseyinbozdemir/cta-exception-pahali-mi-net-10-olcumleri-fgk</guid>
      <description>&lt;p&gt;"Exception pahalıdır, akış kontrolünde kullanma." Bu cümleyi ilk kez bir söyleşide duymuştum; arkasından gelen öneri de belliydi: hata durumunda exception fırlatma, sonuç döndür, yani Result pattern. Ama "ne kadar pahalı" sorusuna kimse sayı vermiyordu, ben de ölçtüm.&lt;/p&gt;

&lt;p&gt;Kısa cevap: &lt;code&gt;try/catch&lt;/code&gt; bloğunun kendisi neredeyse bedava. Pahalı olan exception fırlatmak: .NET 10'da tek bir &lt;code&gt;throw&lt;/code&gt; ve &lt;code&gt;catch&lt;/code&gt;, aynı durumu dönüş değeriyle bildirmekten yaklaşık 1.300 kat yavaş. Yine de mutlak değer mikrosaniye düzeyinde; hata seyrekse bunu hissetmezsin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ölçüm düzeneği
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://benchmarkdotnet.org/" rel="noopener noreferrer"&gt;BenchmarkDotNet&lt;/a&gt; 0.15.8, .NET 10.0.1, Windows 11, AMD Ryzen 7 5700X. Ölçümün özü şu iki metot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Baseline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;SonucDon&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SonucIle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Derinlik&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Kod&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;FirlatYakala&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Firlat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Derinlik&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Derinlik&lt;/code&gt;, hatanın kaç metot aşağıda oluştuğunu belirliyor (1 ve 10). JIT ara metotları birleştirip sonucu çarpıtmasın diye hepsi &lt;code&gt;[MethodImpl(MethodImplOptions.NoInlining)]&lt;/code&gt; ile işaretli. Ölçüm kodunun tamamı ve ham sonuçlar &lt;a href="https://github.com/huseyinbozdemir/blog-olcumleri/tree/main/exception-maliyeti" rel="noopener noreferrer"&gt;GitHub'da&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  try/catch performansı düşürür mü?
&lt;/h2&gt;

&lt;p&gt;Hata hiç oluşmadığında:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Süre&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;try/catch yok&lt;/td&gt;
&lt;td&gt;0,24 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;try/catch var&lt;/td&gt;
&lt;td&gt;0,91 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fark bir nanosaniyenin altında. .NET'te &lt;code&gt;try&lt;/code&gt; bloğu her çalıştığında ek bir iş yapılmaz; hangi kod aralığının hangi &lt;code&gt;catch&lt;/code&gt;'e ait olduğu bir tabloda durur ve bu tabloya ancak exception fırlatıldığında bakılır. Kalan 0,7 ns, bir veritabanı sorgusunun ya da HTTP isteğinin yanında ölçülemeyecek kadar küçük.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exception fırlatmanın maliyeti
&lt;/h2&gt;

&lt;p&gt;Hata oluştuğunda tablo değişiyor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hatanın derinliği&lt;/th&gt;
&lt;th&gt;Sonuç döndürmek&lt;/th&gt;
&lt;th&gt;throw + catch&lt;/th&gt;
&lt;th&gt;Ayrılan bellek&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 metot&lt;/td&gt;
&lt;td&gt;1,1 ns&lt;/td&gt;
&lt;td&gt;1.491 ns&lt;/td&gt;
&lt;td&gt;320 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 metot&lt;/td&gt;
&lt;td&gt;11,2 ns&lt;/td&gt;
&lt;td&gt;3.820 ns&lt;/td&gt;
&lt;td&gt;1.336 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Exception fırlatıldığında runtime bir nesne oluşturur, uygun &lt;code&gt;catch&lt;/code&gt;'i bulmak için yığını yürür, stack trace bilgisini toplar ve aradaki her çerçeveyi geri sarar. Derinlik arttıkça bu iş büyüyor: 10 seviyede süre 2,5 katına, ayrılan bellek 4 katına çıkıyor.&lt;/p&gt;

&lt;p&gt;Bu sayılar eskisinden iyi. &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime" rel="noopener noreferrer"&gt;.NET 9'da&lt;/a&gt; CoreCLR'ın exception işleme altyapısı yeniden yazıldı ve Microsoft'un mikro ölçümlerinde 2-4 kat hızlandı. Eski bir yazıda gördüğün rakamlar bugün için fazla karamsar olabilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parse mı, TryParse mı?
&lt;/h2&gt;

&lt;p&gt;Gündelik hayattaki karşılığı geçersiz girdi. &lt;code&gt;"12a4"&lt;/code&gt; gibi bir metni sayıya çevirmeye çalışınca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1.708 ns, 480 B&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;girdi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 8 ns, bellek ayırmıyor&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;girdi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sayi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;sayi&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yaklaşık 200 kat fark. Kullanıcıdan, dosyadan ya da bir HTTP isteğinden gelen girdinin hatalı olması beklenen bir durum; orada &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.int32.tryparse" rel="noopener noreferrer"&gt;&lt;code&gt;TryParse&lt;/code&gt;&lt;/a&gt; hem hızlı hem de niyeti açık.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exception yerine Result döndürmek
&lt;/h2&gt;

&lt;p&gt;Söyleşide önerilen yol buydu. En sade hali:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Sonuc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Deger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Hata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Basarili&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Hata&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Sonuc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;deger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Sonuc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;hata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Sonuc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Siparis&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Getir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;_depo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;siparis&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Sonuc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Siparis&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siparis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sonuc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Siparis&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sipariş bulunamadı"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bedeli de var: çağıran taraf sonucu kontrol etmek zorunda. Kontrolü unutursa hata sessizce kaybolur; exception'ın iyi yanı tam olarak bunu unutmana izin vermemesi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ne zaman önemli?
&lt;/h2&gt;

&lt;p&gt;Mutlak sayıya bak: 1,5 µs. Saniyede 1.000 exception fırlatan bir servis bunun için saniyede yaklaşık 1,5 ms CPU harcar. Veritabanı bağlantısının kopması gibi seyrek ve beklenmeyen durumlarda exception doğru araç, maliyeti önemsiz.&lt;/p&gt;

&lt;p&gt;Hesap, hata normal akışın parçası olduğunda değişir. Kayıt bulunamadığında exception fırlatıp middleware'de 404'e çeviren yapılar gördüm. Okunaklı görünüyor; ama "bulunamadı" sık yaşanıyorsa her istek bu bedeli öder, ayrılan bellek de GC'ye iş çıkarır. Microsoft'un &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions" rel="noopener noreferrer"&gt;exception önerileri&lt;/a&gt; de aynı yönde: sık yaşanan durumları exception'a bırakmadan ele al.&lt;/p&gt;

&lt;p&gt;Kendi servisinde bakmak için &lt;code&gt;dotnet-counters monitor -n &amp;lt;süreç-adı&amp;gt; System.Runtime&lt;/code&gt; çalıştır ve exception sayacını izle. Saniyede yüzlerce exception görüyorsan hangilerinin aslında beklenen bir sonuç olduğuna bak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kısa kural
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;try/catch&lt;/code&gt; yazmanın bir bedeli yok; seyrek hatada exception fırlatmak da hissedilmez.&lt;/li&gt;
&lt;li&gt;Exception fırlatmak, sonuç döndürmekten yüzlerce kat yavaş ve bellek ayırıyor; sık yaşanan durumlarda bu fark birikir.&lt;/li&gt;
&lt;li&gt;Beklenen sonuçlar (geçersiz girdi, bulunamadı, doğrulama hatası) dönüş değeriyle, beklenmeyenler exception'la.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/csharp-exception-performansi" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>IMemoryCache mi, HybridCache mi?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:29:19 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/imemorycache-mi-hybridcache-mi-3n7g</link>
      <guid>https://dev.to/huseyinbozdemir/imemorycache-mi-hybridcache-mi-3n7g</guid>
      <description>&lt;p&gt;Yalnızca bellek içi cache kullanan bir sistemde banka listesi gibi nadiren değişen bir verinin sunucudan sunucuya farklı olduğunu gördüm: bir ekranda eksik, diğerinde fazla banka. Normalde veri değiştiğinde ilgili key'in cache'ten silinmesi lazım. Ama silme yalnızca değişikliği yapan sunucuda oluyor; TTL de yoksa diğerleri eski listeyi süresiz tutuyor. Bu durum pub/sub'a geçmeden çözülemedi.&lt;/p&gt;

&lt;p&gt;Bellek içi cache'in (&lt;code&gt;IMemoryCache&lt;/code&gt;) birden fazla sunucuda iki ayrı derdi var. Biri bu: her sunucu kendi kopyasını tutuyor ve kopyalar birbirinden habersiz. Diğeri daha sessiz: kaydın süresi dolduğu anda herkesin aynı anda veritabanına koşması (cache stampede). &lt;code&gt;HybridCache&lt;/code&gt;, .NET 9 ile gelen, bellek içi ve dağıtık cache'i tek API'de birleştirip stampede'e karşı koruma sunan yeni soyutlama; ikinci derdi çözmek için geldi. Hangisini ne kadar çözdüğünü ölçtüm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache boşaldığında herkes veritabanına koşar
&lt;/h2&gt;

&lt;p&gt;Popüler bir kaydın süresi dolduğu anda o kaydı isteyen herkes cache'i boş bulur ve aynı sorgu için hep birlikte veritabanına gider. Buna &lt;em&gt;cache stampede&lt;/em&gt; (izdiham) deniyor; İngilizce kaynaklarda "thundering herd" diye de geçer. &lt;code&gt;IMemoryCache.GetOrCreateAsync&lt;/code&gt; buna karşı koruma sunmuyor: aynı anahtar için gelen her istek kendi sorgusunu çalıştırıyor.&lt;/p&gt;

&lt;p&gt;Ölçüm düzeneği: her sorgusu 50 ms süren, aynı anda en fazla 100 bağlantıya izin veren sahte bir veritabanı (SqlClient ve Npgsql'de varsayılan havuz boyutu 100). Aynı anahtara, cache boşken eşzamanlı istek gönderdim. .NET 10, Microsoft.Extensions.Caching.Hybrid 10.10.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;İstek&lt;/th&gt;
&lt;th&gt;IMemoryCache&lt;/th&gt;
&lt;th&gt;HybridCache&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100 sorgu, 63 ms&lt;/td&gt;
&lt;td&gt;1 sorgu, 61 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;1.000 sorgu, p99 612 ms&lt;/td&gt;
&lt;td&gt;1 sorgu, 53 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.000&lt;/td&gt;
&lt;td&gt;5.000 sorgu, p99 2,9 sn&lt;/td&gt;
&lt;td&gt;1 sorgu, 64 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;100 isteğe kadar süre farkı yok; zarar, bağlantı havuzu dolunca başlıyor. Ondan sonra gecikme istek sayısıyla birlikte doğrusal büyüyor.&lt;/p&gt;

&lt;p&gt;Daha gerçekçi senaryo sürekli trafik: 200 kullanıcı 10 saniye boyunca istek atıyor, kayıt 1 saniyede bir doluyor. &lt;code&gt;IMemoryCache&lt;/code&gt; her süre dolmasında veritabanına ortalama &lt;strong&gt;180&lt;/strong&gt; kez gitti, &lt;code&gt;HybridCache&lt;/code&gt; &lt;strong&gt;1&lt;/strong&gt; kez.&lt;/p&gt;

&lt;h2&gt;
  
  
  HybridCache neyi çözüyor, neyi çözmüyor?
&lt;/h2&gt;

&lt;p&gt;Kurulumu iki satır; &lt;code&gt;GetOrCreateAsync&lt;/code&gt; aynı anahtar için gelen eşzamanlı istekleri tek sorguda birleştiriyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHybridCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HybridCache&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrCreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"product:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bir &lt;code&gt;IDistributedCache&lt;/code&gt; (ör. Redis) kayıtlıysa onu ikinci katman olarak kendisi kullanıyor. Ama baştaki banka listesi problemini çözmüyor. &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid" rel="noopener noreferrer"&gt;Microsoft'un dokümanı&lt;/a&gt; bunu açıkça söylüyor: bir kaydı anahtarla ya da tag ile sildiğinde silme o sunucuda ve Redis'te gerçekleşiyor, diğer sunucuların belleğindeki kopya etkilenmiyor.&lt;/p&gt;

&lt;p&gt;Bunu da ölçtüm. İki HybridCache örneği (A ve B sunucusu) ortak bir ikinci katmanı paylaşıyor. İkisi de banka listesini önbelleğe alıyor, veritabanında liste değişiyor, A kaydı siliyor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bellek ömrü&lt;/th&gt;
&lt;th&gt;Silme&lt;/th&gt;
&lt;th&gt;B güncel listeyi görüyor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 sn&lt;/td&gt;
&lt;td&gt;anahtarla&lt;/td&gt;
&lt;td&gt;5 sn sonra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 sn&lt;/td&gt;
&lt;td&gt;anahtarla&lt;/td&gt;
&lt;td&gt;30 sn sonra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 sn&lt;/td&gt;
&lt;td&gt;tag ile&lt;/td&gt;
&lt;td&gt;30 sn sonra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL yok (&lt;code&gt;IMemoryCache&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;anahtarla&lt;/td&gt;
&lt;td&gt;hiç&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A silmeden hemen sonra yeni listeyi görüyor; B ise bellek katmanının ömrü dolana kadar eski listeyi göstermeye devam ediyor. HybridCache bizim yaşadığımız "süresiz eski" durumunu "en fazla N saniye eski"ye çeviriyor, ama sıfırlamıyor. İzdiham koruması da aynı şekilde sunucu başına; 5 sunucun varsa veritabanına 1 değil, en fazla 5 sorgu gider.&lt;/p&gt;

&lt;p&gt;Yani bizim pub/sub ihtiyacımız HybridCache'le ortadan kalkmazdı. Bunu hazır çözen bir seçenek olarak &lt;a href="https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md" rel="noopener noreferrer"&gt;FusionCache'in backplane'i&lt;/a&gt; silme mesajını Redis pub/sub üzerinden bütün sunuculara dağıtıyor. HybridCache'te ise bellek katmanının süresini (&lt;code&gt;LocalCacheExpiration&lt;/code&gt;) kısa tutarak eski veriyle yaşanacak süreyi sınırlıyorsun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gizli maliyet: her okumada deserialize
&lt;/h2&gt;

&lt;p&gt;Ölçerken beklemediğim bir şey çıktı; açıkçası ben de bilmiyordum. Kayıt zaten bellekteyken okumanın bedeli:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Okuma&lt;/th&gt;
&lt;th&gt;Süre&lt;/th&gt;
&lt;th&gt;Bellek ayırma&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IMemoryCache.TryGetValue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28 ns&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HybridCache, &lt;code&gt;{ get; set; }&lt;/code&gt; özellikli sınıf&lt;/td&gt;
&lt;td&gt;520 ns&lt;/td&gt;
&lt;td&gt;120 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HybridCache, &lt;code&gt;sealed&lt;/code&gt; + &lt;code&gt;[ImmutableObject(true)]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;43 ns&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HybridCache değiştirilebilir (mutable) bir tipi bellekte tutsa bile her okumada yeniden deserialize ediyor. Sebebi güvenlik: iki istek aynı nesneyi alıp biri değiştirirse cache bozulur. Microsoft varsayılanı bu yüzden temkinli seçmiş. Tip &lt;code&gt;sealed&lt;/code&gt; ve &lt;code&gt;[ImmutableObject(true)]&lt;/code&gt; ile işaretliyse aynı nesneyi döndürüyor ve fark 18 kattan 1,5 kata iniyor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ImmutableObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hangisini seçmeli?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tek sunucu, seyrek değişen veri: &lt;code&gt;IMemoryCache&lt;/code&gt; yeter. Popüler anahtarların süresi aynı anda doluyorsa izdihama dikkat et.&lt;/li&gt;
&lt;li&gt;Birden fazla sunucu: tutarlılık için ya ortak bir katman (Redis) ya da silme mesajını herkese ulaştıran bir mekanizma (pub/sub, backplane) gerekiyor. HybridCache tek başına bunu vermiyor.&lt;/li&gt;
&lt;li&gt;HybridCache'e geçiyorsan cache'lediğin tipleri &lt;code&gt;sealed&lt;/code&gt; + &lt;code&gt;[ImmutableObject(true)]&lt;/code&gt; yap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HybridCache kullanamadığın eski bir projede (ör. .NET Framework'te paket eklemek istemiyorsan) izdiham korumasını &lt;code&gt;IMemoryCache&lt;/code&gt; üzerine kendin de kurabilirsin. Ölçümdeki elle yazılmış sürüm, HybridCache'le aynı sonucu verdi (1.000 istekte 1 sorgu):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TekSorguCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMemoryCache&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_bekleyenler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetOrCreateAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

        &lt;span class="c1"&gt;// Aynı anahtar için yalnızca ilk istek factory'yi çalıştırır, diğerleri aynı Task'ı bekler.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_bekleyenler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;finally&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_bekleyenler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryRemove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bütün senaryoların kodu ve ham sonuçlar &lt;a href="https://github.com/huseyinbozdemir/blog-olcumleri/tree/main/hybridcache" rel="noopener noreferrer"&gt;GitHub'da&lt;/a&gt;; kendi makinende &lt;code&gt;dotnet run -c Release -- soguk 1000&lt;/code&gt; ile tekrarlayabilirsin.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/imemorycache-hybridcache" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>caching</category>
      <category>performance</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
