<?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>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;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;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ı. 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.&lt;/p&gt;

&lt;p&gt;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="k"&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="k"&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#'ta exception pahalı mı? .NET 10 ölçümleri</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>
    <item>
      <title>Outbox pattern nedir? Kuyruk yokken outbox</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:29:09 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/outbox-pattern-nedir-kuyruk-yokken-outbox-21ip</link>
      <guid>https://dev.to/huseyinbozdemir/outbox-pattern-nedir-kuyruk-yokken-outbox-21ip</guid>
      <description>&lt;p&gt;Kullanıcılara mail ve bildirim göndermek gibi işleri isteğin içinde, senkron yapıyorduk. Kullanıcı, işlemi bitse bile mail sunucusunun cevabını da bekliyordu. Bu işleri arka plana almanın klasik cevabı bir mesaj kuyruğudur. Bizim kuyruk altyapımız yoktu ve ilk aşamada RabbitMQ istenmedi. Elimizde bir veritabanı ve birkaç sunucu vardı.&lt;/p&gt;

&lt;p&gt;Çözüm, kuyruğu veritabanının içine koymak oldu. Yapılacak işi, işlemin kendisiyle aynı transaction'da bir tabloya satır olarak yazdık, isteği bitirdik. Arkada çalışan &lt;code&gt;BackgroundService&lt;/code&gt;'ler o tabloyu okuyup işi yaptı. Literatürde bunun adı outbox pattern: gönderilecek mesajı ya da yapılacak işi, iş verisiyle aynı transaction'da bir outbox tablosuna yazmak ve ayrı bir süreçle oradan işlemek.&lt;/p&gt;

&lt;p&gt;Bu yazıda outbox'ı genelde anlatıldığı gibi broker'ın önünde bir ara durak olarak değil, &lt;strong&gt;broker yokken kuyruğun kendisi&lt;/strong&gt; olarak kullanmayı anlatacağım. Tablo kuyruk gibi davranmaya başlayınca kuyruğun dertleri de geliyor: aynı satırı iki işçinin alması, başarısız işler ve hiç tekrar denenmemesi gereken işler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outbox pattern nedir, hangi problemi çözer?
&lt;/h2&gt;

&lt;p&gt;Outbox pattern (transactional outbox), bir işlemin yan etkisini (mesaj yayınlamak, mail göndermek, başka bir servisi çağırmak) hemen yapmak yerine, işlemle aynı veritabanı transaction'ında bir outbox tablosuna satır olarak yazmaktır. Ayrı bir işçi bu tabloyu sonradan okuyup işi yapar. İş kaydı ve outbox satırı ya birlikte kaydedilir ya hiç kaydedilmez; yan etki kaybolmaz.&lt;/p&gt;

&lt;p&gt;Outbox'ın çözdüğü problemin adı dual write: bir işlemde iki ayrı sisteme yazman gerektiğinde, ikisinin birlikte başarılı olacağının garantisi yok. Klasik örnek veritabanı ve RabbitMQ:&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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="n"&gt;order&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&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;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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="c1"&gt;// burada çökersen event kaybolur&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SaveChangesAsync&lt;/code&gt; başarılı oldu, publish'ten hemen önce süreç çöktü. Sipariş veritabanında var, ama onu bekleyen hiçbir servis bundan haberdar değil. Sırayı tersine çevirirsen bu sefer veritabanında olmayan bir siparişin event'i yayılır.&lt;/p&gt;

&lt;p&gt;Broker olmadan da aynı problem var. Mail göndermek de veritabanının dışında bir yan etki:&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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="n"&gt;order&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendOrderConfirmationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;// mail sunucusu yanıt vermezse?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mail sunucusu yavaşsa kullanıcı onu bekler. Hata verirse ya isteği başarısız sayarsın (oysa sipariş kaydedildi) ya da hatayı yutarsın (mail hiç gitmez ve kimse bilmez).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;Klasik transactional outbox&lt;/a&gt; bunu şöyle çözer: event'i doğrudan broker'a değil, işlemle aynı transaction'da bir outbox tablosuna yazarsın. Ayrı bir süreç (relay) tabloyu okur ve mesajı broker'a iletir. Tablo yalnızca broker'a giden yolda bir ara duraktır.&lt;/p&gt;

&lt;p&gt;Bizde broker yoktu. O yüzden relay'in işi mesajı bir yere iletmek değil, işin kendisini yapmaktı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broker yokken tablo kuyruk olur: EF Core ve BackgroundService
&lt;/h2&gt;

&lt;p&gt;Outbox tablosundaki her satır yapılacak bir iş. Bizim kurduğumuz yapıyı jenerik alan adlarıyla sadeleştirirsem şöyle bir şeye benziyor:&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;OutboxJob&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;long&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;set&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;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Type&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;// "notification.email", "payout" ...&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;WorkerKey&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;// bu satırı hangi işçi alacak&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Payload&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;// işin JSON gövdesi&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;Retryable&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;// başarısız olursa tekrar denensin mi&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OutboxStatus&lt;/span&gt; &lt;span class="n"&gt;Status&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Attempts&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="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&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="k"&gt;public&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;NextAttemptAt&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;OutboxStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Done&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İşi yazan taraf, iş kaydıyla outbox satırını aynı &lt;code&gt;SaveChangesAsync&lt;/code&gt; içinde kaydediyor. EF Core ilişkisel veritabanlarında tek bir &lt;code&gt;SaveChanges&lt;/code&gt; çağrısını &lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/transactions" rel="noopener noreferrer"&gt;zaten bir transaction içinde&lt;/a&gt; çalıştırır. Yani sipariş ve "bu siparişin mailini gönder" satırı ya birlikte yazılıyor ya hiç yazılmı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="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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="n"&gt;order&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;OutboxJobs&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="n"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"notification.email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&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;OrderConfirmationEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerEmail&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;retryable&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="c1"&gt;// ikisi tek transaction&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&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="n"&gt;order&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İstek burada bitiyor. Kullanıcı mail sunucusunu beklemiyor.&lt;/p&gt;

&lt;p&gt;Tabloyu okuyan taraf bir &lt;code&gt;BackgroundService&lt;/code&gt;. Belirli aralıklarla bekleyen satırları çekiyor, her birini türüne göre ilgili handler'a veriyor ve durumunu güncelliyor:&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;OutboxWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IServiceScopeFactory&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OutboxOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&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;ExecuteAsync&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;opt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&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;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&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;PeriodicTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PollInterval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;while&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;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForNextTickAsync&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="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;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&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;db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OutboxProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;now&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;UtcNow&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;jobs&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;OutboxJobs&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;j&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkerKey&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkerKey&lt;/span&gt;
                         &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;j&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;OutboxStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;
                         &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextAttemptAt&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="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextAttemptAt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;j&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;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchSize&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;job&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&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;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessAsync&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;,&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;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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/workers" rel="noopener noreferrer"&gt;&lt;code&gt;BackgroundService&lt;/code&gt;&lt;/a&gt; ASP.NET Core uygulamasının içinde, uygulamayla birlikte başlayıp duran bir işçi. Ayrı bir servis kurmana, ayrı bir altyapı işletmene gerek yok. Kuyruk altyapısı olmayan bir ekip için outbox'ı cazip yapan şey de bu: bizim için kurulumu RabbitMQ'dan daha kolaydı. Yeni bir tablo ve uygulamanın içinde birkaç sınıf.&lt;/p&gt;

&lt;p&gt;Sorgudaki &lt;code&gt;WorkerKey&lt;/code&gt; filtresi ise bir sonraki bölümün konusu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aynı satırı iki sunucu almasın
&lt;/h2&gt;

&lt;p&gt;Uygulama tek sunucuda çalışıyorsa yukarıdaki döngü yeter. Birden fazla sunucuda çalışıyorsa her sunucunun &lt;code&gt;BackgroundService&lt;/code&gt;'i aynı tabloyu okur. İki sunucu aynı anda aynı &lt;code&gt;Pending&lt;/code&gt; satırı çekerse kullanıcıya iki mail gider.&lt;/p&gt;

&lt;p&gt;Bu problemin yaygın çözümü satır kilidi. SQL Server'da &lt;code&gt;UPDLOCK, READPAST&lt;/code&gt; &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table" rel="noopener noreferrer"&gt;ipuçlarıyla&lt;/a&gt; her işçi bekleyen satırları kilitleyerek alır, başka bir işçinin kilitlediği satırları da beklemeden atlar. PostgreSQL'de karşılığı &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;. İngilizce kaynakların çoğu bu yolu anlatıyor ve doğru bir yol.&lt;/p&gt;

&lt;p&gt;Biz başka bir yol seçtik: satırları sunuculara paylaştırdık. Her outbox satırının bir &lt;code&gt;WorkerKey&lt;/code&gt;'i var ve bu key genelde işin türüne göre belirleniyor. Her sunucunun &lt;code&gt;appsettings&lt;/code&gt;'inde de hangi key'i okuyup işleyeceği yazıyor:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Outbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"WorkerKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PollInterval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00:00:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BatchSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bir sunucu yalnızca kendi key'ine sahip satırları görüyor. İki sunucu aynı satıra hiç bakmadığı için kilide de ihtiyaç kalmıyor. Bir sunucuya bir şey olduğunda yedek sunucuya aynı key'i veriyorduk ve onun satırlarını o devralıyordu.&lt;/p&gt;

&lt;p&gt;Bu yapının tek bir kuralı var: &lt;strong&gt;aynı key aynı anda iki sunucuya asla verilmez.&lt;/strong&gt; Biz hiç vermedik, o yüzden çift işleme yaşamadık. Ama bu garantiyi veritabanı değil, bir operasyon kuralı veriyor. Yanlış kopyalanmış bir &lt;code&gt;appsettings&lt;/code&gt; dosyası, kilitli yaklaşımın hiç izin vermeyeceği bir çift işlemeye yol açar.&lt;/p&gt;

&lt;p&gt;İki yaklaşımı yan yana koyarsak:&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;Satır kilidi (&lt;code&gt;UPDLOCK, READPAST&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Key ile paylaştırma&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Çift işlemeyi engelleyen&lt;/td&gt;
&lt;td&gt;Veritabanı&lt;/td&gt;
&lt;td&gt;Yapılandırma kuralı&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bir sunucu düşerse&lt;/td&gt;
&lt;td&gt;Diğerleri satırları alır&lt;/td&gt;
&lt;td&gt;Yedeğe key elle verilir&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bir iş türü yoğunlaşırsa&lt;/td&gt;
&lt;td&gt;Tüm sunucular paylaşır&lt;/td&gt;
&lt;td&gt;O key'in sunucusu tek başına taşır&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karmaşıklık&lt;/td&gt;
&lt;td&gt;Kilit, timeout, takılan satır&lt;/td&gt;
&lt;td&gt;Bir &lt;code&gt;Where&lt;/code&gt; koşulu&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Key yaklaşımının asıl bedeli üçüncü satırda. Bir iş türü tek bir sunucuya bağlı olduğu için o türün işleri yatayda ölçeklenmiyor. Bildirim gibi hacmi öngörülebilir işlerde bu sorun olmayabilir; ani yük gelen bir iş türünde o sunucunun kuyruğu uzar ve diğer sunucular boşta bekler.&lt;/p&gt;

&lt;p&gt;Karşılığında aldığımız şey sadelik. Kilitli yaklaşımda işçi satırı aldıktan sonra çökerse o satırın kilidinin ya da "işleniyor" durumunun nasıl çözüleceğini de tasarlaman gerekir. Key yaklaşımında böyle bir ara durum yok.&lt;/p&gt;

&lt;h2&gt;
  
  
  Her iş tekrar denenmez
&lt;/h2&gt;

&lt;p&gt;Outbox satırının işi başarısız olabilir: mail sunucusu yanıt vermez, karşı servis hata döner. Burada en kolay karar "başarısız olanı birkaç kez daha dene" olur. Biz bunu her iş için yapmadık.&lt;/p&gt;

&lt;p&gt;Her satırın bir &lt;code&gt;Retryable&lt;/code&gt; bayrağı var. Tekrar denenebilecek işler bu bayrakla işaretleniyor ve hata aldıklarında bir süre sonra yeniden deneniyor. Riskli gördüğümüz para çıkışı işlemleri ise hiç tekrar denenmiyor: başarısız olunca satır &lt;code&gt;Failed&lt;/code&gt; durumuna geçiyor ve incelenmesi için ilgili ekiplere mail gidiyor.&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;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutboxJob&lt;/span&gt; &lt;span class="n"&gt;job&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;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;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&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;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;).&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;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&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="n"&gt;job&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;OutboxStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&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;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&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;.&lt;/span&gt;&lt;span class="n"&gt;Attempts&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;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Retryable&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attempts&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&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;.&lt;/span&gt;&lt;span class="n"&gt;NextAttemptAt&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;UtcNow&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="nf"&gt;Backoff&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;.&lt;/span&gt;&lt;span class="n"&gt;Attempts&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;// Pending kalır, sonra tekrar alınır&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;.&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;OutboxStatus&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotifyTeamAsync&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;,&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;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// bir insan baksın&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;Bu ayrım bence yazının en önemli kararı. Bir bildirimi iki kez göndermenin bedeli küçük: kullanıcı aynı maili iki kez görür. Para çıkışında "başarısız" çoğu zaman "olmadı" değil, "ne olduğunu bilmiyorum" demektir. Karşı taraf işlemi yapmış ama cevabı sana ulaşmamış olabilir. Böyle bir işi otomatik olarak tekrar denemek, paranın iki kez çıkması riskini sisteme kendi elinle yazmak olur. &lt;a href="https://huseyinbozdemir.com/blog/idempotency-odeme-sistemleri#banka-timeoutu-sonucu-bilinmeyen-%C3%A7a%C4%9Fr%C4%B1" rel="noopener noreferrer"&gt;Idempotency yazısında&lt;/a&gt; banka timeout'u için anlattığım durum bunun aynısı.&lt;/p&gt;

&lt;p&gt;Bir ayrıntı daha: outbox işi "en az bir kez" yapar, "tam bir kez" değil. İşçi maili gönderip satırı &lt;code&gt;Done&lt;/code&gt; olarak kaydetmeden önce çökerse, sunucu ayağa kalktığında satır hâlâ &lt;code&gt;Pending&lt;/code&gt; görünür ve mail tekrar gider. Tekrar denenecek her iş, iki kez çalışmaya dayanıklı olmalı. Dayanıklı değilse ya &lt;a href="https://huseyinbozdemir.com/blog/idempotency-odeme-sistemleri" rel="noopener noreferrer"&gt;tüketici tarafında idempotency&lt;/a&gt; kurarsın ya da bizim para çıkışında yaptığımız gibi hiç otomatik denemezsin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kendi sisteminde nasıl izlersin
&lt;/h2&gt;

&lt;p&gt;Tablo kuyruğa dönüşünce kuyruğun metrikleri de sana kalıyor. RabbitMQ'nun yönetim panelinde hazır gördüğün şeyleri burada kendin sorgulaman gerekiyor. Üç sayı çoğu sorunu erkenden gösterir:&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;WorkerKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;Bekleyen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&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;EnEskiBekleyen&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;OutboxJobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;-- Pending&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;WorkerKey&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;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;Basarisiz&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;OutboxJobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Failed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bekleyen satır sayısı&lt;/strong&gt; sürekli artıyorsa işçi yetişemiyor ya da hiç çalışmıyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;En eski bekleyen satırın yaşı&lt;/strong&gt; tek başına en iyi alarm. Polling aralığın 5 saniyeyken 10 dakikalık bir satır varsa o key'in sunucusunda bir sorun var. Key ile paylaştırmada bu özellikle önemli, çünkü bir sunucu durduğunda onun satırlarını başka kimse almıyor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Başarısız satır sayısı&lt;/strong&gt; artıyorsa giden mailler de artıyor demektir; o mailler gözden kaçmaya başlamadan önce sayıya bakmak gerekiyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;İki bakım işi de unutulmamalı. Birincisi index: işçinin sorgusu her birkaç saniyede bir çalışıyor, &lt;code&gt;(WorkerKey, Status, Id)&lt;/code&gt; gibi bir index olmazsa tablo büyüdükçe bu sorgu yavaşlar. İkincisi temizlik: &lt;code&gt;Done&lt;/code&gt; satırlar sonsuza kadar tabloda kalırsa tablo şişer. Belirli bir süreden eski tamamlanmış satırları silen ya da arşive taşıyan bir iş gerekir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ne zaman RabbitMQ'ya geçmeli?
&lt;/h2&gt;

&lt;p&gt;Biz sonradan hibrit bir yapıya geçtik: bazı yerlerde RabbitMQ, bazı yerlerde outbox kaldı. Outbox tablosu bir kuyruğun yerini tamamen tutmuyor, ama her iş için de kuyruk gerekmiyor.&lt;/p&gt;

&lt;p&gt;Genel bir çerçeve çizmem gerekirse outbox tablosu şu durumlarda yetiyor: iş aynı uygulamanın içinde yapılıyor, hacim öngörülebilir ve birkaç saniyelik gecikme sorun değil. Bir event'i birden fazla servisin dinlemesi gerekiyorsa, aynı iş türünü birçok işçiye dağıtman gerekiyorsa ya da tüketicileri bağımsız ölçeklemek istiyorsan broker'ın sağladığı şeyleri tabloyla yeniden yazmaya başlarsın. Orada RabbitMQ daha ucuz.&lt;/p&gt;

&lt;p&gt;Broker'a geçince outbox'ı atman da gerekmiyor. Klasik kullanımı tam olarak bu: veritabanı ile broker arasındaki dual write'ı kapatmak. &lt;a href="https://masstransit.io/documentation/patterns/transactional-outbox" rel="noopener noreferrer"&gt;MassTransit&lt;/a&gt; ve &lt;a href="https://cap.dotnetcore.xyz/" rel="noopener noreferrer"&gt;CAP&lt;/a&gt; gibi kütüphaneler EF Core ile çalışan hazır bir transactional outbox sunuyor. Broker'ı kurduğunda elle yazılmış relay'i bırakıp bunlardan birine geçmek çoğu zaman daha az kod demek.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Arka plan işi için mesaj kuyruğu şart değil. İşi iş kaydıyla aynı transaction'da bir tabloya yazan outbox ve bir &lt;code&gt;BackgroundService&lt;/code&gt; birçok durumda yeter.&lt;/li&gt;
&lt;li&gt;Tablo kuyruğa dönüşünce çift işleme senin sorunun olur. Ya satır kilidiyle (&lt;code&gt;UPDLOCK, READPAST&lt;/code&gt; / &lt;code&gt;SKIP LOCKED&lt;/code&gt;) ya da satırları key ile sunuculara paylaştırarak çözersin. İkincisi basit ama garantisi bir yapılandırma kuralına dayanıyor.&lt;/li&gt;
&lt;li&gt;Her işi tekrar deneme. İki kez yapılması zararsız olan işi dene. Sonucu belirsiz kalabilen para hareketini başarısız say ve bir insana göster.&lt;/li&gt;
&lt;li&gt;Kuyruğun metriklerini artık sen tutuyorsun: bekleyen satır sayısı, en eski satırın yaşı ve başarısız satırlar.&lt;/li&gt;
&lt;/ul&gt;




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

</description>
      <category>dotnet</category>
      <category>architecture</category>
      <category>csharp</category>
      <category>backend</category>
    </item>
    <item>
      <title>Idempotency key nedir, istemcide nerede üretilir?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:59 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/idempotency-key-nedir-istemcide-nerede-uretilir-496j</link>
      <guid>https://dev.to/huseyinbozdemir/idempotency-key-nedir-istemcide-nerede-uretilir-496j</guid>
      <description>&lt;p&gt;Sunucuda idempotency'yi kurdun: anahtar saklanıyor, aynı anahtar gelince cevap tekrar dönüyor, kuyrukta consumer tekrarı tanıyor. Sonra istemciye baktın; ödeme butonuna her basışta yeni bir &lt;code&gt;Idempotency-Key&lt;/code&gt; üretiliyor. Kullanıcı iki kez bastı, iki farklı anahtar gitti, sunucu ikisini de ayrı ödeme sandı. Bütün o altyapı hiçbir şeyi korumadı.&lt;/p&gt;

&lt;p&gt;Idempotency key, istemcinin ürettiği ve sunucunun aynı işlemin tekrarını tanımasını sağlayan benzersiz anahtardır. Kısa cevap: butona basışta değil, ödeme niyeti oluştuğu anda, yani ödeme ekranı açılırken üretilir.&lt;/p&gt;

&lt;p&gt;Bunu &lt;a href="https://huseyinbozdemir.com/blog/idempotency-odeme-sistemleri" rel="noopener noreferrer"&gt;Idempotency yazısının&lt;/a&gt; sonunda tek cümleyle geçmiştim. Bu kısa not o cümlenin devamı; çünkü "nerede üretilir" sorusunun cevabı bizde de ilk başta göründüğü kadar basit çıkmadı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key bir isteğin değil, bir niyetin kimliği
&lt;/h2&gt;

&lt;p&gt;Idempotency key, istemcinin "ben bu ödemeyi yapmak istiyorum" dediği anı temsil eden benzersiz bir değerdir. Tanımdaki önemli kelime istek değil, niyet. Bir niyet için kaç istek giderse gitsin (çift tıklama, timeout sonrası retry, ağ kopması) hepsi aynı anahtarı taşır. Sunucunun tekrarı tanıması ancak böyle mümkün olur; &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe'ın dokümanı&lt;/a&gt; da anahtarı aynı mantıkla tanımlar.&lt;/p&gt;

&lt;p&gt;Bu yüzden anahtar butonun tıklama olayında değil, niyetin doğduğu yerde üretilir. Bizde bu yer ödeme ekranının açılışı. Kullanıcı sepetini onaylayıp ödeme ekranına geldiğinde sipariş artık değişmiyor; ekran açılırken üretilen tek anahtar o siparişin bütün denemelerinde aynı kalıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ekran değişebiliyorsa
&lt;/h2&gt;

&lt;p&gt;Her ödeme ekranı donmuş değil. Tutarın, taksit seçeneğinin ya da ödeme yönteminin ekranda değişebildiği akışlar var. Orada "ekran açılışı" yanlış an olur: kullanıcı taksidi değiştirir, aynı anahtarla farklı bir istek gider.&lt;/p&gt;

&lt;p&gt;Bu durumda anahtar, ödemenin snapshot'ı alındığı anda üretilir. Snapshot, kullanıcının gördüğü ve onayladığı son halin kaydıdır; tutar ve seçenekler o anda sabitlenir, anahtar o kayda bağlanır. Sonrasında bir şey değişirse yeni bir snapshot ve yeni bir anahtar doğar.&lt;/p&gt;

&lt;p&gt;Sunucu tarafı bu kuralın bekçisi. Aynı anahtar farklı bir istek gövdesiyle gelirse bu bir tekrar değil, istemci hatasıdır ve &lt;code&gt;422&lt;/code&gt; döner. İlk yazıdaki dört durumdan biri buydu; istemci anahtarı doğru yerde üretmezse en çok bu kapıya çarpar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sayfa yenilenince ne olur
&lt;/h2&gt;

&lt;p&gt;Anahtar istemcide bellekte duruyor. Kullanıcı sayfayı yenilerse ya da geri dönüp tekrar gelirse anahtar gider; ekran yeniden açılır ve yeni bir anahtar üretilir. İlk bakışta bu, yenilemeyle çift ödemeye kapı açıyor gibi görünür.&lt;/p&gt;

&lt;p&gt;Bizde bunu anahtar değil, başka bir soru engelliyor. Ödeme ekranı açılırken istemci sunucuya "bu sipariş ödendi mi?" diye soruyor. İlk deneme sonuçlandıysa ekran ödeme formunu değil sonucu gösteriyor; sonuçlanmadıysa kullanıcı yeni anahtarla devam ediyor. Yani anahtar aynı niyetin tekrarlarını birbirine bağlıyor, sipariş durumu sorgusu ise niyetin ikinci kez doğmasını engelliyor. İki ayrı koruma, iki ayrı iş.&lt;/p&gt;

&lt;p&gt;Anahtarı &lt;code&gt;sessionStorage&lt;/code&gt;'da saklayıp yenilemeden sonra da aynı anahtarla devam etmek mümkün. Bunun bedeli, saklanan anahtarın ait olduğu siparişle hâlâ eşleşip eşleşmediğini ayrıca takip etmek. Sipariş durumunu zaten soruyorsan bu ek karmaşıklık pek bir şey kazandırmıyor; biz o yüzden saklamadık.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kodda nasıl görünüyor
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentScreen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Niyet başına bir anahtar: ekran açılırken (ya da snapshot anında) üretilir.&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&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="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentApi&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="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPaid&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showForm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Çift tıklama, timeout, retry: hepsi aynı anahtarla gider.&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Money&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&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;Anahtarın üretildiği satır &lt;code&gt;pay&lt;/code&gt; metodunun içinde olsaydı her tıklamada yenilenirdi; sınıfın alanı olunca ekranın ömrü boyunca sabit kalıyor. Başlık adı için &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/" rel="noopener noreferrer"&gt;IETF'nin Idempotency-Key taslağı&lt;/a&gt; var; biz de aynı adı kullanıyoruz.&lt;/p&gt;

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

&lt;p&gt;Ödeme ekranında butona hızlıca iki kez bas ve ağ sekmesinde iki isteğin &lt;code&gt;Idempotency-Key&lt;/code&gt; başlığına bak. Değerler farklıysa anahtar yanlış yerde üretiliyor; sunucudaki idempotency katmanı o iki isteği birbirine bağlayamaz.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Anahtar niyet başına bir tane; isteğe değil, ödeme niyetine bağla.&lt;/li&gt;
&lt;li&gt;Ekran değişebiliyorsa anahtar snapshot anında doğar, aynı anahtarla farklı istek &lt;code&gt;422&lt;/code&gt; ile reddedilir.&lt;/li&gt;
&lt;li&gt;Sayfa yenilemesine karşı korumayı anahtara yükleme; ekran açılırken sipariş durumunu 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-key-istemcide-uretme" rel="noopener noreferrer"&gt;huseyinbozdemir.com&lt;/a&gt;'da yayımlandı.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>typescript</category>
      <category>api</category>
      <category>payments</category>
    </item>
    <item>
      <title>Log maskeleme: .NET'te kart numarası sızmasın</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:54 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/log-maskeleme-nette-kart-numarasi-sizmasin-2g8p</link>
      <guid>https://dev.to/huseyinbozdemir/log-maskeleme-nette-kart-numarasi-sizmasin-2g8p</guid>
      <description>&lt;p&gt;Kart numarasını log'a yazmadan önce regex'le maskelemek yaygın bir yöntem: her endpoint kendi log satırında maskeleme fonksiyonunu çağırır. Çağırmayı unutan tek bir endpoint olduğunda kart numarası log'a açık düşer.&lt;/p&gt;

&lt;p&gt;Log maskeleme, hassas bir değeri log'a yazılmadan önce okunamaz hale getirmektir. Kart numarasında genelde ilk 6 ve son 4 hane kalır: &lt;code&gt;411111******1111&lt;/code&gt;. PCI DSS de gösterimde en fazla BIN ve son dört haneye izin veriyor (&lt;a href="https://www.pcisecuritystandards.org/faqs/1492/" rel="noopener noreferrer"&gt;PCI SSC SSS 1492&lt;/a&gt;). Bu bir üst sınır; BIN 8 haneyse "ilk 6" kuralı da değişiyor, log'da daha azını göstermek daha güvenli.&lt;/p&gt;

&lt;p&gt;Sorun maskelemenin kendisinde değil, nerede yapıldığında. Endpoint başına yapılan maskeleme, birinin hatırlamasına bağlıdır.&lt;/p&gt;

&lt;p&gt;Bu notta maskelemeyi hatırlamaya bırakmayan bir yapı öneriyorum: kart numarasını kendi tipine sarmak, .NET'in yerleşik &lt;code&gt;Microsoft.Extensions.Compliance.Redaction&lt;/code&gt; aracı ve log'u tarayan bir test. Örneklerin hepsini .NET 10'da küçük bir projede çalıştırıp çıktısına baktım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kart numarası log'a hangi yollardan düşer
&lt;/h2&gt;

&lt;p&gt;Tek yol unutulan bir &lt;code&gt;LogInformation&lt;/code&gt; satırı değil. Örnek projede şunların hepsi numarayı açık yazdı:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nesneyi olduğu gibi loglamak.&lt;/strong&gt; &lt;code&gt;record&lt;/code&gt; tipinin &lt;code&gt;ToString&lt;/code&gt;'i bütün alanları yazar; &lt;code&gt;log.LogInformation("Ödeme {Istek}", istek)&lt;/code&gt; çıktısında &lt;code&gt;KartNo = 4111111111111111&lt;/code&gt; görünür, CVV de. Aynı DTO &lt;code&gt;class&lt;/code&gt; iken yalnızca tip adı yazılıyordu; yani DTO'yu &lt;code&gt;record&lt;/code&gt;'a çevirmek, log koduna dokunmadan sızıntı başlatıyor. Serilog'da &lt;code&gt;{@Istek}&lt;/code&gt; aynısını JSON olarak yapar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exception mesajı.&lt;/strong&gt; &lt;code&gt;long.Parse("4111 1111 1111 1111")&lt;/code&gt; fırlattığı &lt;code&gt;FormatException&lt;/code&gt;'ın mesajına girdinin kendisini koyar; &lt;code&gt;KeyNotFoundException&lt;/code&gt; da aranan anahtarı yazar. Kendi kodun numarayı hiç loglamasa da &lt;code&gt;LogError(ex, ...)&lt;/code&gt; onu log'a taşır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query string.&lt;/strong&gt; &lt;code&gt;GET /kart?pan=...&lt;/code&gt; gibi bir istek, ASP.NET Core'un kendi "Request starting" log'unda tam URL'iyle görünür. Şablonda &lt;code&gt;Microsoft.AspNetCore&lt;/code&gt; seviyesi &lt;code&gt;Warning&lt;/code&gt; olduğu için çoğu zaman görünmez; hata ayıklarken &lt;code&gt;Information&lt;/code&gt;'a çektiğin gün görünür.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HttpLogging.&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-logging/" rel="noopener noreferrer"&gt;HttpLogging&lt;/a&gt; middleware'inde &lt;code&gt;RequestBody&lt;/code&gt; açıksa gövde olduğu gibi yazılır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EF Core.&lt;/strong&gt; &lt;code&gt;EnableSensitiveDataLogging&lt;/code&gt; açıksa sorgu parametreleri düz yazılır: &lt;code&gt;@p0='4111111111111111'&lt;/code&gt;. Kapalıyken &lt;code&gt;'?'&lt;/code&gt; görünür.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regex bunların bir kısmını yakalar, bir kısmını yakalamaz. Hangisini yakaladığını da çoğu zaman kimse bilmez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Varsayılanı tersine çevir: kart numarası kendi tipini taşısın
&lt;/h2&gt;

&lt;p&gt;Önerim kart numarasını &lt;code&gt;string&lt;/code&gt; olarak dolaştırmamak. İstek gövdesi sınırda bir &lt;code&gt;KartNo&lt;/code&gt; tipine bağlanır ve bu tipin &lt;code&gt;ToString&lt;/code&gt;'i her zaman maskeli döner. Açık değer yalnızca bankaya giden istekte, adıyla çağrılan bir metotla alınır.&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;JsonConverter&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;KartNoJsonConverter&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;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;KartNo&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="kt"&gt;string&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;private&lt;/span&gt; &lt;span class="nf"&gt;KartNo&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;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="n"&gt;_deger&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;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;KartNo&lt;/span&gt; &lt;span class="nf"&gt;Parse&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;girdi&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;rakamlar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&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;girdi&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="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDigit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;rakamlar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;13&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;19&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Luhn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Gecerli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rakamlar&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;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Kart numarası geçersiz."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// değeri mesaja koyma&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KartNo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rakamlar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Açık değer yalnızca bankaya giden istekte.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Acik&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;_deger&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;override&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ToString&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;_deger&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="s"&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="n"&gt;_deger&lt;/span&gt;&lt;span class="p"&gt;[..&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]}{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'*'&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="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;10&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="m"&gt;4&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;KartNoJsonConverter&lt;/code&gt; okurken &lt;code&gt;KartNo.Parse&lt;/code&gt;'ı çağırıyor, yazarken &lt;code&gt;ToString&lt;/code&gt;'i. Yani bu tipi taşıyan bir nesne yanlışlıkla JSON olarak loglansa da maskeli çıkar; bankaya giden istek ayrı bir modelde &lt;code&gt;Acik()&lt;/code&gt; ile kurulur.&lt;/p&gt;

&lt;p&gt;Aynı örnek projede bu tiple &lt;code&gt;{KartNo}&lt;/code&gt;, &lt;code&gt;{Istek}&lt;/code&gt;, string interpolasyonu ve kendi yazdığımız exception mesajı hep &lt;code&gt;411111******1111&lt;/code&gt; yazdı. Endpoint'i yazan kişinin maskelemeyi hatırlaması gerekmiyor: unutursa log'a maskeli bir satır düşüyor. Açık değeri istemek ise &lt;code&gt;Acik()&lt;/code&gt; diye ayrıca yazılması gereken, code review'da göze batan bir çağrı. &lt;a href="https://huseyinbozdemir.com/blog/api-kaynak-aitligi-idor" rel="noopener noreferrer"&gt;IDOR yazısında&lt;/a&gt; aitlik kontrolü için de aynı şeyi savunmuştum: unutulabilen kontrolü unutulamaz hale getirmek.&lt;/p&gt;

&lt;p&gt;Serilog kullanıyorsan &lt;code&gt;{@Istek}&lt;/code&gt; içinde de &lt;code&gt;ToString&lt;/code&gt;'in kullanılması için yapılandırmaya bir satır ekledim: &lt;code&gt;.Destructure.AsScalar&amp;lt;KartNo&amp;gt;()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Hazır bir paket istersen &lt;a href="https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive" rel="noopener noreferrer"&gt;Serilog.Enrichers.Sensitive&lt;/a&gt; kart numarasını regex'le maskeliyor. Ama denediğim sürüm (2.1.0) yalnızca 16 haneyi tanıdı ve Luhn kontrolü yapmadı: 15 haneli bir Amex test kartını açık bıraktı, 16 haneli bir sipariş numarasını maskeledi.&lt;/p&gt;

&lt;h3&gt;
  
  
  .NET'in yerleşik çözümü: Redaction
&lt;/h3&gt;

&lt;p&gt;.NET'in kendi aracı &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/data-redaction" rel="noopener noreferrer"&gt;Microsoft.Extensions.Compliance.Redaction&lt;/a&gt;. Veriyi bir attribute ile sınıflandırıyorsun, bir redactor kaydediyorsun, log pipeline'ında redaction'ı açıyorsun. &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator" rel="noopener noreferrer"&gt;Kaynak üretimli&lt;/a&gt; &lt;code&gt;[LoggerMessage]&lt;/code&gt; metotlarında ve &lt;code&gt;[LogProperties]&lt;/code&gt; ile loglanan nesnelerde iyi çalıştı.&lt;/p&gt;

&lt;p&gt;Ama aynı logger'la yazılan klasik &lt;code&gt;LogInformation("Kart {KartNo}", ...)&lt;/code&gt; satırı, &lt;code&gt;record&lt;/code&gt;'un &lt;code&gt;ToString&lt;/code&gt;'i ve exception mesajı açık kaldı. Redaction, doğru log metodunu kullanmayı hatırlamaya bağlı; yani endpoint başına regex'le aynı soruyu taşıyor. Tip yaklaşımıyla birlikte iyi bir katman, tek başına değil.&lt;/p&gt;

&lt;p&gt;Kendi 6+4 redactor'ını yazacaksan bir ayrıntı: &lt;code&gt;ApplyDiscriminator&lt;/code&gt; varsayılan olarak açık ve redactor'a değer yerine &lt;code&gt;değer:alanAdı&lt;/code&gt; geliyor. Maskelenmiş çıktının sonunda kart hanesi yerine alan adının son harflerini görürsen sebep bu; kapatman gerekiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regex'in doğru yeri: son kontrol
&lt;/h2&gt;

&lt;p&gt;Regex atılmaz, yeri değişir. Entegrasyon testlerinde bütün log çıktısını toplayıp kart numarasına benzeyen ve Luhn kontrolünden geçen bir dizi arayan bir kontrol, unutulan yeri prod'a çıkmadan yakalar.&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;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KartTarayici&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;GeneratedRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"(?&amp;lt;!\d)(?:\d[ -]?){12,18}\d(?!\d)"&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="k"&gt;partial&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt; &lt;span class="nf"&gt;Aday&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="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;KartVar&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;satir&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;Aday&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;satir&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Any&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;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;Luhn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Gecerli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&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;m&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="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDigit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;Luhn kontrolü sipariş numarası gibi 16 haneli ama kart olmayan değerlerin çoğunu eler. Örnek projede test kartı yakalandı, Luhn'dan geçmeyen 16 haneli bir sipariş numarası yakalanmadı.&lt;/p&gt;

&lt;p&gt;Tipin göremediği iki yer için iki kural daha gerekiyor: ödeme endpoint'lerinde HttpLogging'in gövde loglaması kapalı kalır, kart numarası query string'e hiç girmez. İkisi de istek tipe bağlanmadan önce log'a düşüyor.&lt;/p&gt;

&lt;p&gt;Regex'in ne kadar iyi yazıldığı ikinci planda kalıyor; asıl risk, birinin onu çağırmayı hatırlamasına dayanması.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bu yazı ilk olarak &lt;a href="https://huseyinbozdemir.com/blog/log-maskeleme-kart-numarasi" 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>fintech</category>
      <category>csharp</category>
    </item>
    <item>
      <title>AI'ın yazdığı testler hatayı yakalar mı? Mutation testing ile ölçüm</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:46 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/aiin-yazdigi-testler-hatayi-yakalar-mi-mutation-testing-ile-olcum-o1f</link>
      <guid>https://dev.to/huseyinbozdemir/aiin-yazdigi-testler-hatayi-yakalar-mi-mutation-testing-ile-olcum-o1f</guid>
      <description>&lt;p&gt;Bir faturanın durumunu bankadan gelen koda göre eşleyen eski bir kod vardı. Kural kısaydı: bankadan "A" gelirse faturayı ödendi olarak işaretle. Yapay zekâya bu koda test yazdırdım; testler kurala uygun, düzgün ve yeşildi. Sonra bankada "A"nın bambaşka bir durum olduğu ortaya çıktı. Testler hatayı yakalamamıştı, üstüne bir de "doğru" diye imzalamıştı.&lt;/p&gt;

&lt;p&gt;Kısa cevap: test kalitesini ölçen iki sayıda da, kapsamda (coverage) ve mutation skorunda (koda bilerek konan küçük hataların testler tarafından yakalanma oranı), AI'ın testleri doğru kodda tam not aldı. Koda bilerek bir hata koyduğumda da tam not aldı. Testler yeşildi, kapsam %100'dü, mutation skoru %100'dü ve hata oradaydı.&lt;/p&gt;

&lt;p&gt;Yeni bir kodu iş kuralıyla birlikte verdiğimde AI temiz test yazıyor. Eski kodda ise kural hatalıysa test de hatalı geliyor. Bu farkın ne kadar büyük olduğunu C# ve Stryker.NET ile dört varyantta ölçtüm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapsam neyi ölçer, mutation testing neyi?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Kapsam&lt;/strong&gt;, testler çalışırken kodun hangi satırlarından ve dallarından geçildiğini sayar. Bir &lt;code&gt;if&lt;/code&gt;'in iki kolu da çalıştıysa dal kapsamı tamdır. Ama kapsam, testin sonucu doğru kontrol edip etmediğini bilmez; hiçbir &lt;code&gt;Assert&lt;/code&gt; içermeyen bir test de satırları "kapsar".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutation testing&lt;/strong&gt; bu boşluğu kapatmak için var. Araç, kodda küçük bir değişiklik yapar (buna mutant denir), sonra testleri çalıştırır. Örneğin:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;araToplam&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;SabitKuponEsigi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// asıl kod&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;araToplam&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SabitKuponEsigi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// mutant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testlerden en az biri kırılırsa mutant "öldürülmüş" sayılır: testler bu değişikliği fark etmiştir. Hepsi yeşil kalırsa mutant "hayatta kalır" ve testlerinde bir kör nokta olduğunu gösterir. Mutation skoru, öldürülen mutantların oranıdır. .NET'te bunun aracı &lt;a href="https://stryker-mutator.io/docs/stryker-net/introduction/" rel="noopener noreferrer"&gt;Stryker.NET&lt;/a&gt;; Microsoft'un dokümantasyonunda da &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/testing/mutation-testing" rel="noopener noreferrer"&gt;kısa bir rehberi&lt;/a&gt; var.&lt;/p&gt;

&lt;p&gt;Yaygın kanı şu: AI'ın yazdığı testler kapsamı kolayca şişirir, mutation skoru ise bunun içinin boş olduğunu ortaya çıkarır. Ben de bunu bekliyordum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deney
&lt;/h2&gt;

&lt;p&gt;Sıfırdan küçük bir sepet hesaplayıcı yazdım. Kuralı az ama sınır değeri çok; mutation testing'in en iyi yakaladığı türden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ara toplam 1.000 TL ve üstündeyse %5 kademe indirimi.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YUZDE10&lt;/code&gt; kuponu kalan tutarın %10'u, en fazla 200 TL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SABIT50&lt;/code&gt; kuponu 50 TL; ara toplam &lt;strong&gt;250 TL ve üstündeyse&lt;/strong&gt; geçerli.&lt;/li&gt;
&lt;li&gt;İndirimlerden sonra 500 TL ve üstü ücretsiz kargo, altı 49,90 TL. Ağırlık 20 kg'ı aşarsa ücretsiz kargoda bile 30 TL ağır yük ücreti.&lt;/li&gt;
&lt;li&gt;Her adım kuruşa yuvarlanır (&lt;code&gt;MidpointRounding.AwayFromZero&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testleri Claude Opus 5.5 yazdı. Her seferinde bağlamı boş yeni bir oturum açtım, yalnızca sınıfı verdim ve aynı cümleyi kullandım: "Bu C# sınıfı için xUnit testleri yaz." Testlere elle dokunmadım. Ölçüm: coverlet ile kapsam, Stryker.NET 5.0 ile mutation skoru, .NET 10 üzerinde.&lt;/p&gt;

&lt;p&gt;Aynı sınıfı dört şekilde verdim:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Varyant&lt;/th&gt;
&lt;th&gt;Kurallar sınıfın başındaki yorumda&lt;/th&gt;
&lt;th&gt;Kod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;var&lt;/td&gt;
&lt;td&gt;doğru&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;yok&lt;/td&gt;
&lt;td&gt;doğru&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;yok&lt;/td&gt;
&lt;td&gt;hatalı&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;var&lt;/td&gt;
&lt;td&gt;hatalı&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hata tek karakterlik: &lt;code&gt;SABIT50&lt;/code&gt; kontrolünde &lt;code&gt;&amp;gt;=&lt;/code&gt; yerine &lt;code&gt;&amp;gt;&lt;/code&gt;. Yani tam 250 TL'lik sepette kupon sessizce çalışmıyor. Sınırda bir birimlik kayma (off-by-one); 250,00 TL'lik sepet nadir olduğu için uzun süre fark edilmeyebilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beklenti tutmadı: %100
&lt;/h2&gt;

&lt;p&gt;Doğru kodda iki varyantın sonucu:&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;Test sayısı&lt;/th&gt;
&lt;th&gt;Dal kapsamı&lt;/th&gt;
&lt;th&gt;Mutation skoru&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A (kurallar yorumda)&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;%100&lt;/td&gt;
&lt;td&gt;%100 (41/41)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B (yorum yok)&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;%100&lt;/td&gt;
&lt;td&gt;%100 (41/41)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hayatta kalan tek mutant yok. Testler eşiklerin hem hemen altını hem tam kendisini deniyordu: 999,99 ve 1.000; 249,99 ve 250; 499,99 ve 500; 20 kg ve 20,001 kg. Yarım kuruşu yukarı yuvarlamayı, kupondan sonra kargo kararının değiştiği durumu, ücretsiz kargoda ağır yük ücretini ayrı ayrı yakalıyordu. Çoğu test, sonucun tamamını tek seferde karşılaştırıyordu:&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;Fact&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;void&lt;/span&gt; &lt;span class="nf"&gt;Sabit50_esikte_50_TL_duser&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;sonuc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Hesapla&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SABIT50"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&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;SepetSonucu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250m&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="m"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;49.90m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;249.90m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu, mutation testing açısından güçlü bir alışkanlık. Tek bir alanı kontrol eden test, diğer alanlardaki değişikliği kaçırır; record'un tamamını karşılaştıran test kaçırmaz.&lt;/p&gt;

&lt;p&gt;B'nin sonucu beni A'dan daha çok şaşırttı. Kuralları anlatan yorumu kaldırınca da AI sınırları kodu okuyarak buldu. Yani bu modülde "AI'ın testleri yüzeysel kalır" iddiası tutmadı.&lt;/p&gt;

&lt;p&gt;Bir yan not: coverlet'in raporunda satır kapsamı A için %79, C için %69 göründü. İkisinde de çalışan kodun tamamı kapsanıyordu. Açıkta kalan satırlar &lt;code&gt;const&lt;/code&gt; bildirimleri ve record'un property'leriydi; testler &lt;code&gt;Assert.Equal&lt;/code&gt; ile record'u bütün olarak karşılaştırınca getter'lar hiç çağrılmıyor. Aynı kapsamda iki test dosyası, yazım tarzı yüzünden on puan farklı sayı alabiliyor. Satır yüzdesini tek başına ölçü yapmamak için bir neden daha.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hatayı koyunca: yeşil testler, %100 skor
&lt;/h2&gt;

&lt;p&gt;C varyantında kod hatalı ve kurallar yorumda yok. AI 42 test yazdı, hepsi geçti. Dal kapsamı %100, mutation skoru %100.&lt;/p&gt;

&lt;p&gt;AI 250 TL sınırını gördü ve test etti. Ama gördüğü davranışı doğru kabul etti:&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;Fact&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;void&lt;/span&gt; &lt;span class="nf"&gt;Sabit50_esikte_uygulanmaz_ama_gecersiz_sayilmaz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Beklenen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250m&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="m"&gt;0m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;49.90m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;299.90m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;H&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SABIT50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;K&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250m&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;Testin adı durumu özetliyor: "Eşikte uygulanmaz." Kuralı bilmeyen biri için bu, kodun söylediğinin dürüst bir tarifi. Kuralı bilen biri için bir hata raporu.&lt;/p&gt;

&lt;p&gt;Asıl ilginç olan, mutation testing'in burada ne yaptığı. Stryker'ın denediği mutantlardan biri &lt;code&gt;&amp;gt;&lt;/code&gt;'yi &lt;code&gt;&amp;gt;=&lt;/code&gt;'ye çevirmek, yani hatayı düzeltmek. Bu test o mutantı öldürüyor: biri hatayı düzeltirse test kırılıyor. Mutation testing, hatalı davranışı korumaya yardım etti. Çünkü ölçtüğü şey testlerin değişikliği fark edip etmediği; değişikliğin iyi mi kötü mü olduğunu bilemez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kuralı verince test kırıldı
&lt;/h2&gt;

&lt;p&gt;D varyantında kod aynı şekilde hatalı ama sınıfın başında kurallar yazıyor: "ara toplam 250 TL ve üstündeyse geçerli". Bu sefer AI kuralın tarafını tuttu:&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;Fact&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;void&lt;/span&gt; &lt;span class="nf"&gt;Sabit50_tam_esikte_250_TL_gecerlidir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Kural: "ara toplam 250 TL ve üstündeyse geçerli".&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sonuc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Hesapla&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SABIT50"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&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;SepetSonucu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250m&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="m"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;49.90m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;249.90m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected: SepetSonucu { AraToplam = 250, KuponIndirimi = 50, Odenecek = 249,90, ... }
Actual:   SepetSonucu { AraToplam = 250, KuponIndirimi = 0,  Odenecek = 299,90, ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;44 testten biri kırıldı ve kırılan test tam olarak hatayı gösteriyordu. Aynı model, aynı istem, aynı kod; tek fark, kuralın koddan ayrı bir yerde yazılı olması.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neden böyle oluyor?
&lt;/h2&gt;

&lt;p&gt;Test literatüründe bunun bir adı var: oracle problemi. Bir testin iki parçası vardır: girdiyi hazırlayan kısım ve "doğru sonuç şu olmalı" diyen kısım. İkinci kısım, yani oracle, beklenen davranışı bilmeyi gerektirir. Kodu okuyarak bulabileceğin şey ise kodun bugünkü davranışıdır.&lt;/p&gt;

&lt;p&gt;AI'a yalnızca kodu verdiğinde, beklenen davranış için elindeki tek kaynak o kod. 24 Java projesi üzerinde yapılan &lt;a href="https://arxiv.org/abs/2410.21136" rel="noopener noreferrer"&gt;bir çalışma&lt;/a&gt; da aynı sonuca varıyor: LLM ile üretilen testler çoğunlukla programın beklenen değil gerçek davranışını yakalıyor, bu da hata bulmayı zorlaştırıyor. Benim küçük deneyim bu bulgunun tek bir örneği.&lt;/p&gt;

&lt;p&gt;Fatura hikâyesine dönünce bir ayrıntı daha var. Orada hata koddan değil kuralın kendisinden geliyordu: "A gelirse ödendi" kuralı da yanlıştı. Testleri o kuralla yazdırsaydım yine yeşil kalırlardı. Yani "kuralı ver" derken kastettiğim, kuralın asıl kaynağı: bankanın durum kodu tablosu, sözleşme, analiz dokümanı. Kodun başındaki yorum bile çoğu zaman kodu yazanın anladığıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eski kodu bilerek mühürlemek
&lt;/h2&gt;

&lt;p&gt;Burada bir ayrım yapmak gerekiyor, çünkü "bugünkü davranışı kilitleyen test" her zaman kötü değil.&lt;/p&gt;

&lt;p&gt;Eski bir koda dokunmadan önce, onun şu an ne yaptığını testlerle kilitlemek bilinen bir yöntem: &lt;a href="https://michaelfeathers.silvrback.com/characterization-testing" rel="noopener noreferrer"&gt;characterization test&lt;/a&gt;. Amacı, refactor sırasında davranışın değişmediğinden emin olmak. Bu iş için AI gerçekten iyi; C varyantındaki testler refactor güvencesi olarak çok işe yarar.&lt;/p&gt;

&lt;p&gt;Sorun, bu testlerin sonradan "kodun doğru olduğunun kanıtı" diye okunması. Altı ay sonra biri 250 TL'lik sepette kuponun çalışmadığını bildirdiğinde, ilk tepki kolayca "testi var, öyle olmalı" olabilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI'a test yazdırırken
&lt;/h2&gt;

&lt;p&gt;Bu ölçümden ve kendi yaşadığımdan çıkardıklarım:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kuralı koddan ayrı ver.&lt;/strong&gt; Yeni kodda zaten iş kuralıyla birlikte veriyorsun, sorun çıkmıyor. Eski kodda da kuralı ayrıca yaz ya da kaynağını ekle; yalnızca sınıfı verirsen test, kodun fotokopisi olur.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kırılan testi hemen "düzelt" deme.&lt;/strong&gt; D'de kırılan test, hatayı bulan testti. AI'a "testler kırıldı, düzelt" dersen büyük ihtimalle testi koda uydurur ve C'ye dönersin. Önce kodun mu testin mi yanlış olduğuna karar ver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eski koda yazdırılan testleri ne olduklarıyla adlandır.&lt;/strong&gt; Bunlar characterization test. İsimde ya da açıklamada "mevcut davranış" dediğinde, okuyan kişi onları şartname sanmaz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation skorunu doğru oku.&lt;/strong&gt; Yüksek skor, testlerin değişikliği fark ettiğini söyler. Testlerin doğru şeyi koruduğunu söylemez. Hatalı davranış da %100 skorla korunabiliyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ölçmediğim şeyler
&lt;/h2&gt;

&lt;p&gt;Bu deney küçük: 80 satırlık tek bir modül, tek bir model, her varyant için tek bir üretim. Başka araçları (Copilot, Cursor vb.) denemedim. Yan etkisi olan, veritabanına ya da dış servise bağlı büyük bir sınıfta AI'ın sınırları bu kadar iyi bulup bulmadığını da bilmiyorum; benim modülümdeki sınırlar kodda açık açık sabit olarak duruyordu. Gerçek kodda kural çoğu zaman üç sınıfa dağılmış durumda ve oraya baktığımda sonucun bu kadar temiz çıkacağını sanmıyorum.&lt;/p&gt;

&lt;p&gt;Kesin söyleyebileceğim tek şey şu: kodu verdiğinde AI kodun ne yaptığını test eder; ne yapması gerektiğini test etmesini istiyorsan, bunu ona sen söylemelisin.&lt;/p&gt;




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

</description>
      <category>dotnet</category>
      <category>testing</category>
      <category>ai</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Feature flag teknik borca nasıl dönüşür?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:35 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/feature-flag-teknik-borca-nasil-donusur-3572</link>
      <guid>https://dev.to/huseyinbozdemir/feature-flag-teknik-borca-nasil-donusur-3572</guid>
      <description>&lt;p&gt;Bir tenant'ı mutlu etmek için açılmış bir flag'imiz vardı. Ürün o tenant'ta biraz farklı çalışsın diye veritabanındaki ayar tablosuna bir satır eklenmiş, kodda da o satıra bakan bir &lt;code&gt;if&lt;/code&gt; yazılmıştı. Sonraki her geliştirmede o &lt;code&gt;if&lt;/code&gt;'i bozmamak için kulağı tersten tuttuk. Yeni özellik o tenant'ta nasıl davranacak, eski yol hâlâ çalışıyor mu, testte o tenant'ın ayarını da açtık mı? Sonra bir gün öğrendik ki o tenant ürünü kullanmayı çoktan bırakmış.&lt;/p&gt;

&lt;p&gt;Feature flag, kodu yeniden yayınlamadan bir davranışı açıp kapatan anahtardır. Bu yazının iddiası şu: tek bir müşteri için açılan flag'in bir sahibi ve bir bitiş tarihi olmaz; bu yüzden kapanmaz ve zamanla teknik borca dönüşür.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature flag nedir, neden kapanmaz?
&lt;/h2&gt;

&lt;p&gt;En basit hali bir config değeri; bizde olduğu gibi veritabanında tenant bazlı bir ayar da olabilir: "bu tenant'ta yeni ekran açık, diğerlerinde kapalı."&lt;/p&gt;

&lt;p&gt;Martin Fowler'ın &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggle yazısı&lt;/a&gt; flag'leri ömürlerine göre ayırır. Bir özelliği yarım haliyle canlıya almak için açılan release toggle birkaç hafta yaşar ve özellik herkese açılınca silinir. Bir müşteriye ya da pakete özel davranışı açan permissioning toggle ise uzun ömürlüdür; aslında bir ürün kararıdır.&lt;/p&gt;

&lt;p&gt;Sorun, tenant'a özel flag'in ikisinin arasında kalması. Geçici bir ihtiyaç için açılır ("şimdilik bu müşteride böyle olsun"), ama kalıcı bir ayar gibi saklanır. Kapatılacağı bir tarih yoktur, kapatma kararını verecek bir sahibi de yoktur. Ayar tablosunda bir satır olarak durur ve kimse onu silmeye cesaret edemez, çünkü kimin kullandığını bilen yoktur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bedeli kodda ödenir
&lt;/h2&gt;

&lt;p&gt;Her flag kodda iki yol demek. İki flag dört kombinasyon, üç flag sekiz. Bunların hepsi test edilmez; genelde varsayılan yol test edilir, flag'li yol "eskiden çalışıyordu" diye kalır.&lt;/p&gt;

&lt;p&gt;Asıl maliyet yeni geliştirmede ortaya çıkıyor. Bizdeki flag tek bir tenant içindi ama o tenant'ın yolu her yeni işin içine girdi. Tasarımı o yolu bozmayacak şekilde eğip büktük. Sonunda kodun bir kısmı, artık kimsenin kullanmadığı bir davranışı korumak için vardı.&lt;/p&gt;

&lt;p&gt;Tenant'ın ürünü bıraktığını da sonradan öğrendik. O dalın en son ne zaman çalıştığını gösteren bir şey yoktu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temizlik
&lt;/h2&gt;

&lt;p&gt;Bu olaydan sonra feature flag temizliği yaptık ve tenant'a özel davranışların hepsini ortak davranışa çektik: her tenant aynı yoldan geçiyor. Flag'li dallar koddan silinince, her yeni işte sorulan "o tenant'ta ne olur?" sorusu da ortadan kalktı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yeni bir flag açarken
&lt;/h2&gt;

&lt;p&gt;Temizlik bir kez yapılınca bitmiyor; yeni flag'ler aynı yoldan gelir. Bir flag eklerken şu üç soruya cevap verilmiş olmalı:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sahibi kim?&lt;/strong&gt; Kapatma kararını verecek kişi ya da ekip. Sahibi olmayan flag sonsuza kadar yaşar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ne zaman silinecek?&lt;/strong&gt; Geçici bir flag'in son kullanma tarihi olmalı. Fowler'ın yazısında anlatılan bir yöntem, tarihi geçen flag'de testi kırmak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kim kullanıyor?&lt;/strong&gt; Flag'li yol çalıştığında bunu tenant bilgisiyle loglamak ya da saymak. Son 90 günde hiç çalışmamış bir dal, silinmek için en güçlü argümandır.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;İkinci maddenin .NET'teki basit bir 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;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;FeatureFlag&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;Ad&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;Sahip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;SonKullanma&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;FlagTarihleriTesti&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&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;void&lt;/span&gt; &lt;span class="nf"&gt;Suresi_gecmis_flag_kalmasin&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;bugun&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&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;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// FlagKatalogu: flag'lerin tutulduğu yer (ayar tablosu, config)&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gecmis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FlagKatalogu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tumu&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;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SonKullanma&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bugun&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;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;f&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="s"&gt; (&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sahip&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;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gecmis&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;Test kırıldığında iki seçenek var: flag'i silmek ya da tarihi bilerek uzatmak. İkisi de bir karar; önemli olan, flag'in kendiliğinden yaşamaya devam etmemesi.&lt;/p&gt;

&lt;p&gt;Bizim hikâyemizde bu üç sorudan yalnızca üçüncüsü sorulmuş olsaydı, o tenant'ın gittiğini çok daha önce öğrenirdik.&lt;/p&gt;




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

</description>
      <category>dotnet</category>
      <category>architecture</category>
      <category>featureflags</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Anthropic Scion nedir, git neden yetmedi?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:30 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/anthropic-scion-nedir-git-neden-yetmedi-a5o</link>
      <guid>https://dev.to/huseyinbozdemir/anthropic-scion-nedir-git-neden-yetmedi-a5o</guid>
      <description>&lt;p&gt;Anthropic Mayıs 2026'da günde ortalama 2.500 kod değişikliği birleştiriyordu. Eylül'de bu sayı 30.000'i geçti. Kodun çoğunu artık ajanlar yazıyor ve 13 Ekim'de yayımlanan &lt;a href="https://anthropic.com/engineering/redesigning-source-control" rel="noopener noreferrer"&gt;mühendislik yazısına&lt;/a&gt; göre bu yükü git taşıyamadı. Scion, Anthropic'in kendi monorepo'su (şirketin bütün kodunun durduğu tek depo) için yazdığı ve git'in yerine geçirdiği şirket içi sürüm kontrol sistemi.&lt;/p&gt;

&lt;p&gt;Scion'u yedi kişilik bir ekip yazdı, göç beş ayda bitti. Aynı dönemde kendi code review aracını (Antique), kod aramayı ve dizin bazında yetkilendirmeyi de baştan yazdılar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git'in hangi varsayımı bozuldu?
&lt;/h2&gt;

&lt;p&gt;Git dağıtık bir sistem. Varsayılan bir &lt;code&gt;git clone&lt;/code&gt; deponun yalnızca son halini değil, &lt;a href="https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control" rel="noopener noreferrer"&gt;bütün geçmişini&lt;/a&gt; makineye indirir. Uçakta commit atabilmen, sunucu çökse de geçmişin her kopyada durması bu tasarımdan gelir. Varsayım şu: istemci çoğu zaman çevrimdışıdır ve elinde her şeyin tam bir kopyası olmalıdır.&lt;/p&gt;

&lt;p&gt;Anthropic'in istemcileri ise neredeyse hiç çevrimdışı olmuyor. Ajanlar, CI işleri ve geliştirme ortamları hep ağa bağlı. Rakamlar yazıdan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ana dalın geçmişi tek başına 100 GB'a yakın, sıkıştırılmış bir çalışma kopyası 25 GB'ın üstünde.&lt;/li&gt;
&lt;li&gt;CI için günde 25 milyon civarında checkout yapılıyor. Her birini düz git ile çekmek günde 600 PB'ın üstünde trafik demek.&lt;/li&gt;
&lt;li&gt;Yükü dağıtmak için en yoğun dönemde birkaç yüz iç git aynası çalıştırmışlar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yani git'in çevrimdışı çalışma için ödettiği bedeli (her kopyada tam geçmiş) her gün milyonlarca kez ödüyorlar, karşılığında aldıkları şeyi ise hiç kullanmıyorlar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scion neyi farklı yapıyor?
&lt;/h2&gt;

&lt;p&gt;Ağırlığı istemciden sunucuya taşıyor. Geçmiş, blame, commit, merge ve rebase sunucuda yapılıyor; istemci yalnızca kendi değişikliklerini ve sınırlı bir önbellek tutuyor. Bir CI işi tüm depoyu çekmek yerine yalnızca ihtiyaç duyduğu dosyaları istiyor, bazı yazma işleri hiç checkout yapmadan API üzerinden yürüyor.&lt;/p&gt;

&lt;p&gt;Yazıda iki rakam öne çıkıyor: altyapı eski git aynası düzenine göre CPU ve bellekte 200 kat küçülmüş, CI checkout'ları dakikalardan saniyelere inmiş. İkisi de şirketin kendi ölçümü.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git bitti mi?
&lt;/h2&gt;

&lt;p&gt;Hayır, ve bunu ilk söyleyen Anthropic'in kendisi. Yazıda Scion'un genel kullanım için git'in yerini tutmayacağı açıkça yazıyor; sistem tek bir dev depo ve hep çevrimiçi istemciler için tasarlanmış. Açık kaynak yapma planları da yok. Onun yerine fikirlerin bir kısmını GitHub ve GitLab'le birlikte git dünyasına taşımak istiyorlar, örneğin sunucu tarafında Scion'dakine benzer kapsamlı RPC'ler.&lt;/p&gt;

&lt;p&gt;Bu yol yeni de değil. Google yıllardır kendi monorepo'sunu &lt;a href="https://research.google/pubs/why-google-stores-billions-of-lines-of-code-in-a-single-repository/" rel="noopener noreferrer"&gt;Piper&lt;/a&gt; ile, Meta &lt;a href="https://engineering.fb.com/2022/11/15/open-source/sapling-source-control-scalable/" rel="noopener noreferrer"&gt;Sapling&lt;/a&gt; ile yönetiyor ve Anthropic de tekniklerin çoğunu bu iki projeden aldığını yazıyor. Farklı olan, bu kez ölçeği büyüten şeyin binlerce mühendis değil, ajanlar olması.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benim yorumum: asıl yük review'da
&lt;/h2&gt;

&lt;p&gt;Haberde beni en çok düşündüren kısım review oldu. AI'la geliştirmeyle birlikte review'lar çok zorlaştı; tek bir PR'da satırlarca değişiklik oluyor. Review için bile AI kullanılıyor, ama yine de &lt;a href="https://huseyinbozdemir.com/blog/ai-unit-test-mutation-testing" rel="noopener noreferrer"&gt;insan kontrolü şart&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Anthropic'in yaşadığı da bence aynı yükün çok daha büyük ölçekteki hali: kod yazmak ucuzladıkça onu okumak, test etmek ve birleştirmek pahalılaşıyor. Onlar sürüm sistemine kadar inmek zorunda kalmış.&lt;/p&gt;

&lt;p&gt;Git tarafında ise bizi henüz zorlayacak boyutta değil. Kendi yazdığımız bir sürüm kontrol sistemine geçmek de şu an için bana gerçekçi gelmiyor. Fintech şirketleri yeni çıkan bir şey tamamen oturmadan köklü bir değişiklik yapmaz; önce o sistemin denenmesini bekler.&lt;/p&gt;

&lt;p&gt;Senin ekibin için de büyük ihtimalle değişen bir şey yok. Ama ajanların açtığı PR'ları kimin ve nasıl incelediği sorusu, git'in sınırlarından çok daha önce kapına gelecek.&lt;/p&gt;




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

</description>
      <category>git</category>
      <category>ai</category>
      <category>anthropic</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>REA nedir? Ajanla tersine mühendislik</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:26 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/rea-nedir-ajanla-tersine-muhendislik-1l72</link>
      <guid>https://dev.to/huseyinbozdemir/rea-nedir-ajanla-tersine-muhendislik-1l72</guid>
      <description>&lt;p&gt;Ekim'de GitHub'da en hızlı yükselen depo bir yapay zekâ modeli ya da yeni bir framework değil, bir tersine mühendislik aracı oldu. &lt;a href="https://github.com/morluto/rea" rel="noopener noreferrer"&gt;REA (Reverse Engineer Anything)&lt;/a&gt; 23 Ekim itibarıyla 370 binin üzerinde yıldızı geçti; ay başında adını bilen yok gibiydi.&lt;/p&gt;

&lt;p&gt;REA, Claude Code, Codex, Cursor ya da Gemini CLI gibi kodlama ajanlarına tersine mühendislik araçlarını tek bir MCP sunucusu üzerinden bağlayan, MIT lisanslı açık kaynak bir araç. Fikri basit: bir uygulamada beğendiğin bir özellik var, kaynak kodu elinde yok. Ajana "bu özellik nasıl çalışıyor, kanıtıyla göster" diyorsun, ajan da REA'nın araçlarıyla uygulamanın içine bakıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  REA ne yapıyor?
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol), bir ajanın dış araçları çağırmasını sağlayan ortak bir protokol. REA bu protokolle ajana bir dizi inceleme aracı sunuyor ve kurulumu tek komut: &lt;code&gt;npx rea-agents setup&lt;/code&gt;. Komut, seçtiğin ajanlara REA'yı kaydediyor ve ajanın nasıl inceleme yapacağını anlatan talimatları ekliyor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;REA ile bir inceleme nasıl ilerler: Ajan, bir uygulamadaki özelliğin nasıl çalıştığını sorar. Soru MCP araç çağrısı olarak REA'ya gider. REA bilgisayardaki analiz araçlarını çalıştırır: Ghidra, Hopper ya da IDA ile derlenmiş programlar; JavaScript, Electron ve .NET için yerleşik statik analiz; tarayıcı, APK ve ağ kayıtları. Sonuç kanıtları ve bilinmeyenleriyle ajana döner; ajan açıklar, benzerini yazar ve test eder.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Asıl iş REA'nın kendisinde değil, arkasına bağladığı araçlarda. Derlenmiş programlar için bilgisayarında kurulu Ghidra, Hopper ya da IDA'yı kullanıyor; yani kendi decompiler'ı yok. JavaScript ve Electron uygulamalarını, .NET dosyalarını ise ek bir araç olmadan, programı çalıştırmadan okuyabiliyor. Bunların yanında web sitelerini bir tarayıcı üzerinden, Android uygulamalarını JADX ile, kaydedilmiş ağ trafiğini de HAR dosyalarından inceleyebiliyor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5sbclwwoeegyxnibm5c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5sbclwwoeegyxnibm5c.webp" alt="Hopper'da açılmış bir macOS programı: solda assembly kodu ve bölüm bilgileri, sağda dosya ve adres bilgileri, altta REA'nın başlattığı analiz adımlarının çıktısı" width="799" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;REA, Hopper'ı kendisi başlatıp bir programı incelerken. Görsel: &lt;a href="https://github.com/morluto/rea" rel="noopener noreferrer"&gt;REA deposu&lt;/a&gt; (MIT lisansı).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Projenin &lt;a href="https://github.com/morluto/rea#readme" rel="noopener noreferrer"&gt;README'si&lt;/a&gt; iki noktada dürüst. Analiz senin bilgisayarında çalışıyor, ama araçların bulduğu sonuçlar ajana, dolayısıyla ajanın kullandığı model sağlayıcısına gidiyor. Bir de REA her bulguyu kanıtıyla ve "bunu bilmiyorum" dediği yerlerle birlikte döndürüyor; uygulamanın orijinal kaynak kodunu geri getirdiğini iddia etmiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Örneklerde ne var?
&lt;/h2&gt;

&lt;p&gt;Projenin &lt;a href="https://rea.tools/showcase/" rel="noopener noreferrer"&gt;örnek vakaları&lt;/a&gt; neyin mümkün olduğunu iyi gösteriyor. Birinde Notion'ın masaüstü uygulamasında panoya kopyalama özelliği, ekrandan Electron'un iç katmanlarına kadar izleniyor. Diğerinde eski bir oyun olan DX-Ball'da topun sesinin hangi hoparlörden çıkacağını hesaplayan fonksiyon assembly'den C'ye geri yazılıyor; yeniden yazılan kod, orijinal programla 3.205 test durumunda aynı sonucu veriyor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn6kgywlfqny7yo8ymf1n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn6kgywlfqny7yo8ymf1n.webp" alt="REA'nın üç örnek vakasını anlatan çizimler: hoparlörlü bir blok kırma oyunu, bir uygulama penceresinden panoya giden bağlantı ve bir noktanın çevresinde dizilmiş mermiler" width="799" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Örnek vakalar: DX-Ball, Notion ve TH04. Görsel: &lt;a href="https://github.com/morluto/rea" rel="noopener noreferrer"&gt;REA deposu&lt;/a&gt; (MIT lisansı).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Abartı mı?
&lt;/h2&gt;

&lt;p&gt;Kısmen. Ajanları Ghidra'ya bağlamak yeni bir fikir değil; &lt;a href="https://github.com/LaurieWired/GhidraMCP" rel="noopener noreferrer"&gt;GhidraMCP&lt;/a&gt; Mart 2025'ten beri var. &lt;a href="https://infoq.com/news/2026/10/reverse-engineer-anything" rel="noopener noreferrer"&gt;InfoQ'nun haberine&lt;/a&gt; göre Hacker News'te de ilk soru buydu: ajana "Ghidra'yı kur ve kullan" demekten farkı ne? Bazı kullanıcılar REA ile daha iyi sonuç aldığını söylüyor, ama bunu ölçen bağımsız bir karşılaştırma yok.&lt;/p&gt;

&lt;p&gt;REA'nın asıl katkısı kolaylık gibi duruyor: tek kurulum, tek arayüz ve birçok dosya türü. Yıldız sayısı da bir ilgi göstergesi, kaç kişinin gerçekten kullandığını söylemiyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nerede durmalı?
&lt;/h2&gt;

&lt;p&gt;Tersine mühendislik meşru bir iş: güvenlik araştırması, zararlı yazılım analizi, eski bir sistemin nasıl çalıştığını anlamak, başka bir yazılımla uyumlu çalışacak bir şey yazmak. Ama sınırı ince ve REA'nın &lt;a href="https://github.com/morluto/rea/blob/main/DISCLAIMER.md" rel="noopener noreferrer"&gt;sorumlu kullanım metni&lt;/a&gt; bu yüzden açık konuşuyor: yalnızca sahibi olduğun ya da incelemeye yetkin olduğun yazılım; lisans kontrolünü, DRM'i ya da kimlik doğrulamayı atlatmak yok. Tersine mühendislik bazı ülkelerde ve bazı lisans sözleşmelerinde kısıtlı, sorumluluk da kullanıcıda. Hopper ve IDA gibi ticari araçların lisansını da ayrıca alman gerekiyor.&lt;/p&gt;

&lt;p&gt;Bir de şirket ortamında unutulan bir nokta var: incelediğin dosya senin bilgisayarında kalsa bile, ondan çıkan kod parçaları ve bulgular model sağlayıcısına gidiyor. Bir iş ortağının verdiği kapalı bir kütüphaneyi ajana inceletmeden önce bunun sözleşmeye uyup uymadığına bakmak gerekiyor.&lt;/p&gt;

&lt;p&gt;Özetle REA, kaynak kodu olmayan bir şeyi anlamaya çalışırken ajanına doğru araçları veriyor; neyi anlamaya yetkin olduğuna karar vermek yine sana kalıyor.&lt;/p&gt;




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

</description>
      <category>ai</category>
      <category>reverseengineering</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>PHP2Shell nedir, açık nasıl çalışıyor?</title>
      <dc:creator>Hüseyin Bozdemir</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:28:19 +0000</pubDate>
      <link>https://dev.to/huseyinbozdemir/php2shell-nedir-acik-nasil-calisiyor-bia</link>
      <guid>https://dev.to/huseyinbozdemir/php2shell-nedir-acik-nasil-calisiyor-bia</guid>
      <description>&lt;p&gt;PHP2Shell, PHP'nin kendisinde bulunan ve 21 Ekim 2026'da yamalanan iki kritik açığa (CVE-2026-109618 ve CVE-2026-109619) verilen ad. Aynı gün Hollanda hükümeti DigiD'yi (vatandaşların devlet hizmetlerine girdiği kimlik sistemi) ve MijnOverheid'ı &lt;a href="https://www.rijksoverheid.nl/actueel/nieuws/2026/10/21/aantal-websites-rijksoverheid-preventief-offline-na-kwetsbaarheden-php" rel="noopener noreferrer"&gt;kendi isteğiyle kapattı&lt;/a&gt;. &lt;a href="https://tpo.nl/2026/10/21/php2shell-websites-offline-digid-ncsc/" rel="noopener noreferrer"&gt;Meclisin ve ulusal siber güvenlik merkezi NCSC'nin siteleri de&lt;/a&gt; kapandı. Ortada bilinen bir saldırı yoktu; sebep, aynı gün yamalanacak iki PHP açığıydı.&lt;/p&gt;

&lt;p&gt;İki açık da üzerine yazılan uygulamada değil, dilin çalışma zamanında. İkisi de CVSS 4.0'da 9,2 puan aldı ve bazı kurulumlarda giriş yapmamış birinin tek bir HTTP isteğiyle sunucuda kod çalıştırmasına izin veriyor. Düzeltilmiş sürümler (8.5.12, 8.4.27, 8.3.36 ve 8.2.35) &lt;a href="https://www.php.net/archive/2026.php#2026-10-21-5" rel="noopener noreferrer"&gt;21 Ekim 14:00 UTC'de çıktı&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Duyuru aslında 22 Ekim'e planlanmıştı. &lt;a href="https://www.huntress.com/blog/php2shell-leaked-critical-php-vulnerabilities" rel="noopener noreferrer"&gt;Huntress'in aktardığına göre&lt;/a&gt; 20 Ekim sabahı açıkların varlığı Hacker News'teki bir gönderiyle sızınca PHP ekibi sürümü bir gün öne çekti. Türkiye'de Siber Güvenlik Başkanlığı da 21 Ekim'de &lt;a href="https://www.ekonomim.com/sektorler/teknoloji/php-guvenlik-acigi-nedir-nasil-kapatilir-php2shell-alarmi-milyonlarca-site-risk-altinda-mi-haberi-926243" rel="noopener noreferrer"&gt;TR-26-1372 kodlu bildirimle&lt;/a&gt; ulusal çapta tarama başlattı.&lt;/p&gt;

&lt;p&gt;Sonraki saatlerde liste uzadı. &lt;a href="https://abc7ny.com/story/php-vulnerability-2026-websites-shut-down-globally-operators-address-ai-detected-flaw-web-programming-language/19968288/" rel="noopener noreferrer"&gt;Fransa, Danimarka ve Finlandiya'da&lt;/a&gt; devlet siteleri geçici olarak kapandı, &lt;a href="https://globalnews.ca/news/12110881/government-sites-go-dark-vuln-in-code/" rel="noopener noreferrer"&gt;Kanada&lt;/a&gt; da bazı federal sitelerini kapattı. Japonya'da ANA, JR West ve MUFG bankası bazı sayfalarını ya da hizmetlerini durdurdu; Wikimedia da bazı sistemlerini önlem olarak kapattı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Açığı yapay zekâ mı buldu?
&lt;/h2&gt;

&lt;p&gt;Kısmen. Açıkları &lt;a href="https://www.wiz.io/blog/php2shell-php-rce" rel="noopener noreferrer"&gt;Wiz'in araştırma ekibi&lt;/a&gt; buldu; yazıda PHP ekibine, Calif'e ve OpenAI'a keşif, koordinasyon ve yamanın doğrulanmasındaki katkıları için teşekkür ediliyor. Wiz çalışma boyunca kendi aracı Wiz Atlas'ı, Gemini 4 Argon'u ve GPT-5.6 Sol'u kullandığını, Gemini'nin açığın bulunmasına büyük katkı verdiğini yazıyor. Modeller exploit'i farklı PHP sürümlerine, ayarlara ve bellek düzenlerine uyarlamakta da kullanılmış.&lt;/p&gt;

&lt;p&gt;Zaman çizelgesi de dikkat çekici. İlk açık 19 Ağustos'ta bulunmuş, iki gün içinde Drupal ve WordPress'te giriş gerektirmeyen kod çalıştırma doğrulanmış ve 24 Ağustos'ta PHP ekibine bildirilmiş. Wiz'e göre açıklı kod on yıldan uzun süredir PHP'deydi ve 5.4'e kadar geriye giden sürümler etkileniyor.&lt;/p&gt;

&lt;p&gt;Wiz'in altını çizdiği nokta da bu: böyle bir hatayı çalışan bir saldırıya dönüştürmek eskiden çok emek isteyen bir işti ve modeller bu engeli ciddi biçimde düşürdü.&lt;/p&gt;

&lt;h2&gt;
  
  
  Açık nasıl çalışıyor?
&lt;/h2&gt;

&lt;p&gt;PHP'nin klasik modelinde her istek sıfırdan başlar: betik çalışır, cevap döner, isteğe ait her şey silinir. Ama istekleri karşılayan süreç silinmez. PHP-FPM'de ya da Apache mod_php'de aynı işçi süreç (worker) yüzlerce isteği art arda işler ve her isteğin sonunda belleği temizleyip bir sonrakine hazırlanır. İki açık da bu temizliğin varsayımlarını bozuyor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;CVE-2026-109619&lt;/a&gt;'da isteğe ait bir iç durum, gösterdiği bellek serbest bırakıldıktan sonra da yerinde kalıyor. Aynı işçinin aldığı bir sonraki istek, bu durum sıfırlanmadan önce onu kullanabiliyor. Bu klasik bir use-after-free: artık başka bir şeye ait olabilecek bir bellek bölgesine eski bir adresle dokunmak. Saldırgan o bölgeyi kendi isteğiyle doldurabilirse okunan veri de, çalışan kod da onun olur.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;CVE-2026-109618&lt;/a&gt;'de PHP, istek verisini hazırlarken bellek sınırına takılıp yarıda kesiliyor. İsteğin sonundaki temizlik, yarım kalmış bu veriyi geçerli sanıp üzerinde işlem yapıyor.&lt;/p&gt;

&lt;p&gt;Asıl ürkütücü kısım şu: php.net'in duyurusuna göre mod_php'de gösterilen istismar, uygulama kodu hiç çalışmadan tetikleniyor. WordPress'i, eklentileri ya da framework'ü güncellemek bir şey değiştirmiyor; kodunun ne kadar temiz olduğu da. Açık, kodunu çalıştıran şeyin içinde. Önerilen geçici ayarların (&lt;code&gt;post_max_size&lt;/code&gt;, &lt;code&gt;session.upload_progress&lt;/code&gt;) hepsinin isteğin gövdesi okunurken devreye girmesi de bunu destekliyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ne yapmalı?
&lt;/h2&gt;

&lt;p&gt;Tek gerçek çözüm güncellemek. PHP'nin sürüm notları tam bir geçici çözüm olmadığını açıkça söylüyor. Önerilen ayarlar (&lt;code&gt;memory_limit&lt;/code&gt;, &lt;code&gt;post_max_size&lt;/code&gt;, &lt;code&gt;session.upload_progress.enabled = 0&lt;/code&gt;) yalnızca bazı saldırı yollarını daraltıyor ve uygulamanın ezemeyeceği yerde, örneğin &lt;code&gt;php_admin_value&lt;/code&gt; ile verilmeleri gerekiyor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;8.2 ile 8.5 arasındaysan yukarıdaki sürümlere geç. 8.2'nin desteği 31 Aralık 2026'da bitiyor.&lt;/li&gt;
&lt;li&gt;8.1 ve öncesi PHP ekibinden güncelleme almıyor. 7.4, 8.0 ve 8.1 için yalnızca küçük kaynak yamaları yayımlandı; dağıtımının ya da hosting firmanın bunları uygulayıp uygulamadığına bak.&lt;/li&gt;
&lt;li&gt;Sürümü kontrol ederken &lt;code&gt;php -v&lt;/code&gt;'ye güvenme. Komut satırındaki PHP ile web sunucusunun kullandığı PHP-FPM ya da mod_php farklı kurulumlar olabilir; &lt;code&gt;php-fpm -v&lt;/code&gt; ya da geçici bir &lt;code&gt;phpinfo()&lt;/code&gt; sayfası (işin bitince sil) gerçek sürümü gösterir.&lt;/li&gt;
&lt;li&gt;Güncellemeden önce saldırıya uğramış olabilirsin. Huntress'in önerisi, php-fpm ya da httpd sürecinin altından &lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt; gibi süreçlerin başladığı anlara bakmak; web klasöründe yeni dosya, yeni cron işi ve yeni kullanıcı aramak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Siten paylaşımlı hosting'deyse PHP'yi sen değil firma günceller. Panelden sürümü kontrol etmek yine senin işin.&lt;/p&gt;




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

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>news</category>
    </item>
  </channel>
</rss>
