Ürün aradığımızda kayıt bulunmayabilir. Database bağlantısı kesildiğinde de veri alamayız, fakat iki durumda aynı şey olmuyor. İlkinde arama tamamlanmış ve sonuç boş gelmiş olabilir. İkincisinde işlemi tamamlayamadık. Sonucu nasıl taşıyacağımıza karar verirken önce çağıran kodun bu farkla ne yapacağını belirlemek gerekiyor.
Beklenen iş sonuçlarını açık bir modelle vermeyi, beklenmeyen teknik arızayı uygun üst handler’a taşımayı tercih ederim. Bu ayrım, hatanın ele alınacağı katmanı da belirler. Böylece her application metodunda geniş bir catch (Exception) açıp aynı log ve response kodunu tekrar etmeyiz.
Result Pattern ve Exception için önce hata anlamını ayırmak
Bir ürün arama işlemi üç farklı sonuç üretebilir. Ürün bulunduğunda normal veri döner. Ürün kimliği geçerli olduğu halde kayıt yoksa uygulama eksik sonuçla karşılaşır. Database bağlantısı kurulamadığında ise arama tamamlanamaz. İkinci ve üçüncü durum istemciye veri döndürmüyor olsa da aynı anlamı taşımaz. Kullanıcının başka kimlik denemesi ikinci durumda anlamlı olabilir. Üçüncü durumda başka kimlik denemek altyapı sorununu çözmez. Hata modelini sadece “başarılı değil” alanıyla kurmak, caller’ın doğru karar vermesi için gereken ayrımı kaybettirebilir.
Bir sipariş onaylama işleminde de stok yetersizliği, geçersiz durum geçişi ve ödeme sağlayıcısının erişilememesi farklı sonuçlardır. Beklenen business reddi kullanıcının değiştirebileceği bir seçimi gösterebilir. Teknik arıza ise operasyonel müdahale veya kontrollü tekrar gerektirebilir. Her duruma exception atmak normal iş sonucunu hata alarmına dönüştürebilir. Her exception’ı Result’a çevirmek de beklenmeyen arızayı sıradan business failure gibi gösterebilir. İki uç yaklaşım yerine sonucun anlamını ve tüketicinin ne yapacağını belirlemek gerekir. Kullanılan tip bu anlamı açık taşımalıdır.
Tercihim: Caller’ın gerçekten farklı davranacağı beklenen iş sonuçlarını açık bir modelle sunmak, beklenmeyen teknik arızaların merkezi exception sınırına ulaşmasına izin vermek. Bu seçimin bedeli iki akışın birlikte anlaşılması ve transport mapping’in tutarlı yönetilmesidir. Buna karşılık her metodun bütün exception tiplerini bir string error’a dönüştürmesi gerekmez. Basit bir lookup yalnızca nullable sonuçla yeterli olabilir. Birden fazla iş sonucu ve bunlara özgü davranış varsa Result modeli değer kazanır. Karar, pattern’i uygulamanın her çağrısına yaymak için değil anlamı korumak için verilmelidir.
Hata kodu, mesaj ve HTTP status farklı sorumluluklardır
product.not_found gibi kararlı kod, istemcinin anlamlı branch seçmesine yardımcı olabilir. Kullanıcıya gösterilecek metin ise dil ve ürün deneyimine bağlı değişebilir. HTTP status code transport sözleşmesidir. Bu üç bilgiyi tek exception mesajından çıkarmaya çalışmak kırılgan hale gelir. Metin değiştiğinde istemci davranışı bozulabilir veya localization code’u etkileyebilir. Application sonucu business anlamını taşırken endpoint bunu uygun HTTP yanıtına eşleyebilir. Domain modeline 404 veya 409 sabiti yerleştirmek, aynı işlemin HTTP dışında kullanılmasını gereksiz yere transport bilgisine bağlayabilir.
Bir bulunamama sonucu her kullanımda 404 olmak zorunda değildir. Ana kaydı arayan GET endpoint’i için uygun olabilir. Sipariş oluşturma request’inde referans verilen ürün bulunamıyorsa request doğrulama contract’ı farklı yanıt seçebilir. Ayrıca hassas bir kaydın varlığını yetkisiz kullanıcıya açmamak için erişim politikası gerekir. Dolayısıyla mapping business sonucunun bağlamına göre yapılmalıdır. Ortak mapper bütün endpoint’lere tek kör kural dağıtmamalıdır. Merkezi tutarlılık yararlıdır; bağlamı silen genelleme ise yanlış veya bilgi sızdıran davranış üretebilir.
Sonucun kendi tutarlılığını korumak
Bir Result nesnesinde hem value hem error bulunabiliyorsa caller hangi alanın geçerli olduğunu yorumlamak zorunda kalır. Aşağıdaki .NET 10 console örneği, iki durumu ayrı record tipleriyle ifade eder. NuGet bağımlılığı yoktur. Mevcut Result Pattern yazısındaki generic model yerine kullanılabilecek daha küçük bir seçenektir.
var products = new Dictionary<int, Product>
{
[1] = new(1, "Keyboard"),
};
Lookup<Product> Find(int id) =>
products.TryGetValue(id, out var p)
? new Lookup<Product>.Found(p)
: new Lookup<Product>.Missing("product.not_found");
var status = Find(42) switch
{
Lookup<Product>.Found => 200,
Lookup<Product>.Missing => 404,
_ => throw new InvalidOperationException("Unsupported result."),
};
if (status != 404)
throw new Exception("Missing result mapping failed.");
Console.WriteLine($"Missing product maps to {status}.");
public sealed record Product(int Id, string Name);
public abstract record Lookup<T>
{
private Lookup() { }
public sealed record Found(T Value) : Lookup<T>;
public sealed record Missing(string Code) : Lookup<T>;
}
Ürün bulunamadığında GET endpoint’i 404 seçebilir. State çatışmasında 409, geçersiz istekte 400 veya sözleşmenin belirlediği başka sonuç kullanılabilir. Bu eşlemeyi endpoint ya da ortak transport mapper’da tutabiliriz. Domain’in HTTP kodunu bilmesi gerekmez. Aynı iş job veya mesaj tüketicisinden çağrıldığında da o kanalın kendi tamamlanma davranışı seçilebilir; iş sonucunun anlamı korunur.
Hangi exception yakalanmalı?
Bilinen concurrency çatışmasını yakalayıp kullanıcıya karar verebileceği bir sonuç sunabiliriz. Ama timeout veya beklenmeyen provider hatasını Missing’e çevirirsek arama yapılamamasını “ürün yok” diye anlatmış oluruz. Caller da yanlış retry veya ekran davranışı seçebilir. Hangi hatayı neden eşlediğimiz catch’in bulunduğu yerde anlaşılmalı.
İptalde de bu ayrımı korumak gerekiyor. İstek sonlandığında OperationCanceledException üzerinden taşınan akışı rastgele failure Result’a dönüştürmek tercih edeceğim bir yaklaşım değil. Token’ın hangi kapsamda iptal edildiği ve işlemin commit noktasına ulaşıp ulaşmadığı önemlidir. İptal edilmiş response, kalıcı işlemin geri alındığını kanıtlamaz.
Log ve client farklı bilgiye ihtiyaç duyar
Client’a database adı, connection string, stack trace veya exception’ın ham mesajını göndermeyin. Teknik ayrıntıları erişimi kontrollü log’da tutup response’a güvenli bir problem tanımı ve correlation bilgisi ekleyebilirsiniz. Aynı exception’ı her katmanda loglamak ise tek arızayı birçok olay gibi gösterebilir.
ASP.NET Core hata yönetimi dokümantasyonu, merkezi handler ve ProblemDetails seçeneklerini anlatıyor. Handler kullanmak bütün hataları 200 yanıt içinde döndürmek anlamına gelmez; HTTP contract korunmalıdır.
Bir işlemin başarısızlığı beklenebilir diye bütün çağrıları Result zincirine dönüştürmek zorunlu değildir. Basit query, nullable sonuç ve endpoint’te açık 404 eşlemesiyle yeterli olabilir. Result modelini, caller’ın gerçekten ayırması gereken sonuçlar olduğunda eklemek daha okunaklıdır.
Örnekteki kapalı sonuç ailesini okumak
Lookup<T> abstract record’dur ve constructor’ı private tutulur. İçindeki Found ve Missing iki ayrı durumu temsil eder. Found, veri taşır. Missing, kararlı bir hata kodu taşır. Aynı nesnede hem error hem value bulundurmadığımız için caller ikisinin hangisinin geçerli olduğunu yorumlamak zorunda kalmaz. Bu düzen, genel bir Result nesnesindeki IsSuccess, Value ve Error alanlarının çelişkili kombinasyonlarını azaltır. Bununla birlikte Found içine null gibi istenmeyen değerlerin verilmesini önlemek gerekiyorsa ek contract kararı gerekebilir; örnek bütün tip-level garantileri uygulamaz.
Switch ifadesi Found için 200, Missing için 404 seçer. Desteklenmeyen bir sonuç gelirse exception atılır. Bu son branch, yeni bir sonuç türü eklendiğinde tüketicilerin otomatik olarak doğru mapping yapacağını garanti etmez. C# tarafındaki bu modelde bütün kullanım yerlerinin compile-time exhaustive kontrolü sağlandığını iddia etmiyoruz. Yeni state eklendiğinde mapper testleri güncellenmelidir. Örnek 42 kimliğini arayıp Missing sonucunun 404’e eşlendiğini kontrol eder. Gerçek HTTP host açmadığı için response header veya serialization davranışını sınamaz. Sayısal status eşlemesi ile gerçek endpoint testi farklı kapsamdır.
Dictionary sabit ürün verisi için kullanılır ve arama TryGetValue üzerinden yapılır. Ürün bulunmaması exception oluşmadan normal branch’e dönüşür. Bu yöntem lookup semantiğini açık gösterir. Database veya dış servis bulunduğunda sorgu hataları ayrıca oluşabilir. Bunları aynı Missing sonucuna çevirmek yanlış anlam üretir. Örneğin provider timeout verdiğinde kullanıcıya “ürün yok” demek, başarısız aramanın nedenini gizler. Modelin küçük olması hata politikasının eksik bırakılması anlamına gelmez; normal iş sonucu ile arama tamamlanamaması ayrımı korunmalıdır.
Result modelini büyütürken gereksiz genellemeden kaçınmak
Lookup örneğinde iki durum yeterlidir. Sipariş onaylama için daha fazla sonuç gerekebilir: Onaylandı, zaten onaylanmış, stok yetersiz veya state çatışması gibi. Bunların her biri caller’ın farklı davranmasını gerektiriyorsa açık model değerlidir. Ancak bütün sistemdeki yüzlerce error code’u tek enum’a koymak farklı feature’ları birbirine bağlayabilir. Her application işlemi sadece ihtiyaç duyduğu anlamları taşıyabilir. Ortak transport error biçimi bulunması bütün business sonuçlarının aynı generic tipe sığdırılmasını gerektirmez. Sınırın kapsamı ve tüketici sayısı tasarımın büyüklüğünü belirlemelidir.
Fluent Result zincirleri bazı akışlarda okunabilirliği artırabilir. Başarı verisinin sonraki adıma taşındığı ve belirli error’larda kısa devre yapılan işlemlerde faydalı olabilir. Fakat basit bir query’yi çok sayıda Bind, Map ve extension metotla ifade etmek review’da davranışı takip etmeyi zorlaştırabilir. Eklenen helper’ların exception ve cancellation semantiğini de bilmek gerekir. Normal bir if veya switch ile anlatılabilen kullanım için büyük bir framework eklemek şart değildir. Araç seçimi, iş akışının açık görülmesini desteklemelidir. Kısalan satır sayısı tek başına bakım iyileşmesi değildir.
Hangi exception yerel olarak ele alınmalı?
Bir persistence işleminin belirli concurrency hatası business çatışma sonucuna eşlenebilir. Burada uygulama gerçekten caller’ın karar verebileceği bir durum tanımlamıştır. Buna karşılık bilinmeyen bütün exception tiplerini conflict’e çevirmek doğru değildir. Bağlantı hatası, mapping hatası veya kodlama hatası farklı nedenlerdir. Catch kapsamını mümkün olduğunca bilinen davranışa göre seçmek gerekir. Bir exception yakalandığında hangi kurtarma veya eşleme kararının verildiğini açıklayabilmeliyiz. Yalnızca metodun signature’ında Result döndüğü için catch açmak, arızayı yorumlamadan gizlemeye yol açabilir.
Alt katman arızayı çözemiyor ve uygun business sonucuna çeviremiyorsa exception’ın yukarı taşınması anlamlı olabilir. Merkezi handler güvenli HTTP yanıtını ve log politikasını uygular. Bu sınırda bütün hatalara aynı public mesaj verilebilir; operasyon kaydında ise teşhis nedeni korunur. Bir exception’ı yeni tipe sarıyorsanız asıl nedeni kaybetmeyin. Yeniden fırlatma biçiminin stack trace üzerindeki etkisini de göz önünde bulundurun. Microsoft’un exception rehberi bu temel davranışları açıklar. Exception mekanizması teşhis bilgisinin kaybolması için kullanılmamalıdır.
Cancellation normal failure’a dönüştürülmemeli
Request iptal edildiğinde ilgili token ile çalışan async işlem durabilir. Bu durum kullanıcının business seçiminin reddedildiği anlamına gelmez. OperationCanceledException bütün teknik hatalarla aynı catch içinde Result’a çevrilirse normal iptal, hata metriğini ve istemci yorumunu bozabilir. Hangi token’ın iptal olduğu da önemlidir. Request token, işlem timeout’u ve host shutdown token’ı farklı nedenler taşıyabilir. API’nin bu durumlarda yanıt verip veremeyeceği bağlantının durumuna bağlıdır. İptal sinyalinin anlamını response status üzerinden tek bir varsayıma indirgemeyin.
Yazma işlemi commit edildikten sonra iptal gözlenebilir. Caller yanıt alamayıp tekrar denediğinde iş etkisi zaten gerçekleşmiş olabilir. Dolayısıyla cancellation sonucu verinin geri alındığını garanti etmez. Result modelinde Cancelled diye yeni state açmak da bu garanti sorununu kendiliğinden çözmez. İşlem kimliği, durum sorgulama veya idempotency davranışı gerekebilir. İptal ile rollback’i aynı kavram olarak ele almak tekrarlanan yazma riskini büyütür. Buradaki lookup örneği sadece bellekte okuma yaptığı için böyle bir commit veya request lifecycle davranışını uygulamaz.
HTTP contract içinde başarı ile reddi ayrı göstermek
Her response’a 200 verip body içinde success:false dönmek istemci ve infrastructure davranışını etkileyebilir. Cache, monitor, retry veya SDK çoğu zaman HTTP status’a bakar. Body’de error bulunması bu mekanizmaların hatayı doğru yorumlayacağını göstermez. Uygun status ve güvenli body modeli birlikte seçilmelidir. Business sonucunun transport mapping’i endpoint seviyesinde açık tutulabilir. Bunun yanında bazı iş contract’ları farklı semantik gerektirebilir; seçilen düzen belgelenmeli ve tüketicilerle uyumlu olmalıdır. Bir error wrapper kullanmak HTTP’nin anlamını otomatik olarak değiştirmez.
Validation hatası, erişim reddi, bulunamama ve state çatışması için ayrı mapping düşünülebilir. Beklenmeyen exception merkezi handler’da güvenli problem yanıtına dönüşebilir. Response’a exception’ın ham mesajını koymak database adı, SQL veya başka iç ayrıntılar açabilir. Correlation kimliği istemci ile operation kaydını ilişkilendirebilir. Bu kimliğin varlığı istemcinin bütün log’a erişebileceği anlamına gelmez. Hata biçimi hem tüketicinin işlem yapmasına hem support sürecine yardımcı olmalıdır. Gereksiz ayrıntı yerine kararlı code, güvenli açıklama ve uygun bağlam seçin.
Log seviyesini business sonuçtan ayırmak
Ürün bulunmaması bazı endpoint’lerde sık beklenen sonuçtur. Her Missing için error log üretmek alarm gürültüsü yaratabilir. Stok yetersizliği ürün veya operasyon metriği olabilir; database bağlantı hatası başka bir sinyaldir. Bu nedenle log seviyesi sonucunun başarısız görünmesine değil anlamına dayanmalıdır. Aynı exception’ı repository, service ve endpoint’te tekrar loglamak tek olayın birkaç hata gibi görünmesine yol açabilir. Hangi sınırın teşhis kaydının sahibi olduğu belirlenmelidir. Bir exception üst katmana aktarılıyorsa her katmanda aynı kaydı tekrar üretmek zorunlu değildir.
Business sonuçların da hiçbir iz bırakmaması gerekmeyebilir. Örneğin çok yüksek state conflict oranı kullanıcı akışında veya concurrency tasarımında sorun gösterebilir. Bu durum error log yerine uygun metric ve context ile izlenebilir. Etiketlerde yüksek cardinality oluşturan kimlikleri kontrolsüz kullanmak operation maliyetini artırabilir. Hassas request payload’ını log’a taşımak da gereksiz risk oluşturur. Hangi verinin gerçekten teşhis için gerektiğini seçmek gerekir. Sonuç modelinin içine ayrıntılı teknik exception koyup sonra otomatik serialize etmek, bu ayrımı bozabilir.
State’in hata sonrası ne olacağını test etmek
Bir metot Result failure dönse bile daha önce yaptığı mutation’lar kalabilir. “Result dönüyor, exception atmıyor” ifadesi state güvenliği garantisi değildir. Sipariş satırını değiştirdikten sonra limit kontrolü başarısızsa nesne ara state’te kalabilir. Validation ile mutation sırası, transaction davranışı ve harici etkiler birlikte tasarlanmalıdır. Beklenen business reddinde hiçbir değişiklik yapılmaması gerekiyorsa bunu test edin. Kısmi değişiklik business sürecinin parçasıysa sonucu açık ifade edin. Hata mekanizması, işlem atomikliğinin yerine geçmez; bu iki sorumluluk ayrı değerlendirilmelidir.
Exception tarafında da aynı soru geçerlidir. Hata atıldığında kaynaklar dispose ediliyor mu, local transaction tamamlanmamışsa geri alınıyor mu ve object state tekrar kullanılabilir mi? Her exception’ın “işlem hiç olmadı” anlamına geldiğini varsaymayın. Uzak sistem veya belirsiz commit varsa daha karmaşık sonuç oluşabilir. Result, exception ve compensation kararları iş akışına göre eşleşmelidir. Küçük bir lookup için bu karmaşıklığı taşımak gereksiz olabilir; yan etkili sipariş işlemi için göz ardı etmek ise doğru değildir. Contract kapsamını kullanım senaryosuyla sınırlamak bu farkı görünür tutar.
Testleri model, mapping ve gerçek endpoint olarak ayırmak
Model testinde Found ve Missing durumlarının doğru üretilmesini kontrol edebilirsiniz. Mapping testinde her desteklenen durumun uygun transport yanıtına çevrildiğini sınayabilirsiniz. Gerçek endpoint testi ise serialization, status ve header davranışını gösterir. Console örneğinde yalnızca Missing mapping kontrolü bulunur. Bu doğrulamayı tam ASP.NET Core integration testi gibi sunmuyoruz. API’ye uygulanırken host ve handler registration davranışı ayrıca kontrol edilmelidir. Yeni business sonuç eklendiğinde hem mapper hem client beklentisi gözden geçirilmelidir. Modelin derlenmesi bütün tüketicilerin yeni durumu anlamlandırdığını kanıtlamaz.
Exception handler testinde istemciye stack trace veya ham teknik mesaj sızmadığını kontrol edin. Bir correlation bilgisi veriliyorsa operation kaydıyla ilişkisi uygun fixture’da sınanabilir. Cancellation için iptal edilen token ile gerçek async akış kullanılabilir; yalnızca exception nesnesi oluşturmak bütün request lifecycle’ı göstermez. Test double kullanılmışsa onun hangi hata zamanlamasını temsil ettiğini belirtin. Gerçek provider timeout veya bağlantı kopması çalıştırılmadığında o alan doğrulanmış sayılmaz. Test planının amacı bütün başarısızlıkları aynı wrapper’da göstermek değil, doğru anlamı koruyan davranışı kanıtlamaktır.
Mevcut exception ağırlıklı API’ye geçiş
Başlangıçta Missing ürün için exception atılıyorsa önce o sonucun normal bir iş durumu olup olmadığını değerlendirin. İlgili endpoint ve istemci contract’ını testle sabitleyin. Sonra query sonucu için nullable veya küçük Lookup modeli kullanabilirsiniz. Merkezi exception handler’ı tamamen kaldırmak gerekmez; beklenmeyen arızalar yine o sınıra ulaşır. Bu değişiklik sırasında teknik hataları aynı Missing’e çevirmemek kritik noktadır. Refactoring ile HTTP contract değişikliğini aynı anda yapıyorsanız tüketicinin etkisini ayrıca belirtin. İç modelin sadeleşmesi dış davranışın sessizce değişmesini haklı çıkarmaz.
Her application metoduna generic Result eklenmiş bir sistemde sadeleşme de benzer yapılabilir. Önce caller’ın sadece success veya null ayrımı yaptığı basit query’leri bulun. Kullanılmayan error çeşitlemeleri ve sadece parametre aktaran helper’lar kaldırılabilir. Ancak birden fazla anlamlı business sonucu taşıyan contract korunmalıdır. Amaç Result Pattern’i bütünüyle silmek veya exception’ı bütünüyle yasaklamak değildir. İhtiyacı olmayan abstraction’ı azaltıp karar gereken sonuçları görünür tutmaktır. Bu ölçütle hem küçük nullable query hem açık Result ailesi aynı uygulamada tutarlı biçimde kullanılabilir.
Sonuç contract’ı değiştiğinde client uyumluluğu
Yeni bir business error code eklemek server tarafında küçük değişiklik olabilir. İstemci yalnızca bilinen kodları ele alıyorsa yeni kod geldiğinde uygun fallback gerekir. Bilinmeyen sonucu başarı kabul etmek tehlikelidir; bütün bilinmeyen durumları aynı teknik hata gibi göstermek de kullanıcı akışını bozabilir. Client contract’ında kararlı code ile kullanıcı mesajının ayrılması bu nedenle önemlidir. Kod değişimi, HTTP status değişimi ve response shape değişimi farklı uyumluluk etkileri taşır. Server testlerinin yanında tüketici beklentisini de değerlendirin. Bir record alanı eklenmesinin bütün istemcilerce sorunsuz kabul edileceğini varsaymayın.
İşlem bir job veya mesaj consumer tarafından çağrıldığında HTTP mapping kullanılmaz. Consumer Missing veya Conflict sonucunda mesajı tamamlayabilir, iş reddini kaydedebilir veya başka akış başlatabilir. Teknik arızada ise kontrollü retry uygulanabilir. Her failure’ı otomatik yeniden denemek, kalıcı business reddini sürekli kuyruğa sokabilir. Bu yüzden application sonucu transport’tan ayrıldığında yalnızca test kolaylığı sağlanmaz; farklı giriş kanalları aynı iş anlamını kendi completion politikasıyla ele alabilir. Bu politika açık olmalıdır. Merkezi bir retry helper, business sonucunu anlamadan bütün işlemleri tekrar çalıştırmamalıdır.
Contract dokümantasyonunda her sonucun kalıcı state hakkında ne söylediğini belirtin. “Reddedildi” hiçbir değişiklik yapılmadı anlamına geliyorsa bu garanti testle korunmalıdır. “Bekliyor” yan etki sürecinin tamamlanmadığını anlatıyorsa client buna göre durum sorgulamalıdır. Teknik arıza sırasında tamamlanma belirsizse uygun işlem kimliği yardımcı olabilir. Sonuç isimlerinin çekici veya genel olması yeterli değildir; kullanıcı ve tüketici açısından kesin anlamı bulunmalıdır. Böylece yeni Result tipi eklemek, mevcut iş davranışını açıklayan bir değişiklik haline gelir.
Review’da aranan son davranış
Yeni hata durumunu eklerken önce caller’ın ne yapacağını belirleyin. Kullanıcı girdisini değiştirebilir mi, işlem tekrar denenebilir mi, yoksa sonucu sorgulamak mı gerekir? Ardından hata sonrası state’i ve dış etkileri kontrol edin. Mapper’ın doğru HTTP kodunu ve güvenli gövdeyi verdiğini, operasyon kaydının da asıl nedeni koruduğunu sınayın. Bu kontroller sayesinde Result veya exception seçimi yalnız metot imzasında kalmaz; tüketicinin güvenebildiği bir davranış sözleşmesine dönüşür.
