EF Core Repository Pattern tartışmasında önce neyi soyutladığımızı açıklamak gerekir. DbContext değişiklikleri izler ve kaydetme işlemini koordine eder. DbSet ise entity sorgularını ve kayıt işlemlerini sunar. Aynı metotları başka isimlerle tekrar etmek, tek başına yeni bir mimari sınır oluşturmaz.
Bir ürün listesi sorgusunda doğrudan context kullanımı açık ve yeterli olabilir. Buna karşılık bir aggregate’in yüklenme şeklini veya persistence detaylarının application’a taşınmasını sınırlamak istiyorsak iş odaklı bir repository anlamlıdır. İki seçeneğin de maliyeti vardır; “her projede repository” ve “repository her zaman gereksiz” ifadeleri bu bağlamı kaçırır.
EF Core Repository Pattern kararı hangi ihtiyacı karşılamalı?
Repository tartışması çoğu zaman kullanılan ORM üzerinden başlar. EF Core varsa repository gereksizdir veya domain tasarımı varsa repository zorunludur gibi kesin ifadeler, aynı uygulamadaki farklı ihtiyaçları tek karara indirger. Önce tüketicinin neyi bilmemesi gerektiğini belirlemek daha kullanışlıdır. Bir liste endpoint’i filtrelenmiş ve sayfalanmış DTO’lara ihtiyaç duyabilir. Sipariş onaylama işlemi ise aggregate’i kurallarıyla birlikte yüklemek zorunda olabilir. Bu iki işlemin veri erişim contract’ı aynı biçimde olmak zorunda değildir. Okuma sorgusu ile domain değişikliğini aynı generic interface’e yerleştirmek önemli farkları örtebilir.
Bir katalog ekranında aktif ürünlerin adı ve kimliği gösteriliyor olsun. Endpoint’in ihtiyacı açık bir projection’dır. Ürün entity’sinin bütün alanlarını ve ilişkilerini yükleyip sonra mapper ile DTO üretmek gereksiz veri taşımaya yol açabilir. Bu senaryoda doğrudan context kullanımı query’nin ne yaptığına yakından bakmayı kolaylaştırır. Eklenen repository aynı projection’ı içeriyorsa esas faydası sorgu saklama ve tüketici bağımlılığını sınırlandırmadır. Böyle bir sınırı uygulamanın ihtiyaç duyup duymadığı değerlendirilmelidir. Query’nin bir sınıfa taşınması onu otomatik olarak daha hızlı veya daha doğru yapmaz.
Sipariş değiştirme senaryosunda ise yüklenen veri yalnızca gösterilmeyecek; invariant’lar uygulanacaktır. Sipariş satırları eksik yüklenirse toplam veya limit kontrolü yanlış sonuç verebilir. Burada GetForConfirmationAsync gibi anlamı belirli bir işlem, hangi aggregate durumunun sağlandığını açık hale getirebilir. Yine de metodun adı tek başına garanti değildir. Implementasyon gerekli ilişkileri yüklemeli ve testler iş kararının eksik veriyle verilmediğini göstermelidir. Repository, aggregate sınırını korumak için seçiliyorsa contract CRUD fiilleriyle sınırlanmamalıdır. Hangi kullanıma uygun veri döndürüldüğü anlaşılmalıdır.
Tercihim: Basit read tarafında açık projection ve doğrudan context, domain odaklı yükleme veya güçlü persistence sınırı gerektiğinde küçük repository contract’ı. Bu seçimin bedeli, uygulamada bütün feature’ların aynı veri erişim şablonunu kullanmamasıdır. Buna karşılık farklı ihtiyaçlar için uygun sınır kurulabilir. Ekibin tutarlılık ihtiyacı, veri erişiminin farklı provider’larda desteklenmesi veya application testlerinde güvenilir query sonuçlarının stub edilmesi gereksinimi büyürse repository kullanımını genişletebilirim. Değişim gerekçesi yalnızca yaygın bir klasör şemasına uymak olmamalıdır.
Çalışan karşılaştırmada gerçekten ne ölçülüyor?
Örnekte önce SQLite connection açılıyor ve context bu bağlantıyla oluşturuluyor. In-memory database’in yaşam süresi açık bağlantıya bağlı olduğundan connection’ı sorgular tamamlanmadan kapatmıyoruz. Ardından schema oluşturuluyor ve biri aktif, biri pasif iki ürün kaydediliyor. İlk sorgu Where ile aktif ürünü seçiyor, kimlik sırasına koyuyor ve doğrudan DTO’ya dönüştürüyor. İkinci yol aynı iş isteğini IProductReads üzerinden yapıyor. İki sonuç dizisinin eşit olması ve tek ürün dönmesi kontrol ediliyor. Böylece iki erişim biçiminin bu fixture için aynı sonucu ürettiği görülüyor.
Bu karşılaştırma performans benchmark’ı değildir. Stopwatch, tekrarlı koşum veya kontrollü dataset bulunmuyor. Repository’nin daha hızlı ya da yavaş olduğu sonucu çıkarılamaz. Aynı şekilde iki ürün içeren fixture bütün filtre ve sıralama davranışlarını kapsamaz. Örnek yalnızca contract biçiminin değiştiğini, query’nin implementasyonda nerede bulunduğunu ve tüketicinin hangi tipe bağımlı olduğunu gösterir. Mimari argüman, ölçülmemiş hız iddiasına dayanmamalıdır. Gerçek performans ihtiyacında query planı, veri miktarı, kullanılan index ve dönen kolonlar değerlendirilir. Sınıf sayısı bu maliyetlerin güvenilir göstergesi değildir.
DTO yalnızca kimlik ve isim taşır. Sonuç, entity koleksiyonu olmadığı için tüketici ürünün active durumunu değiştirip aynı context üzerinden kaydetmeye kalkamaz. Burada projection’ın kendisi önemli bir sınır sağlar. AsNoTracking ise read niyetini görünür hale getirir; ancak bu örnekte projection entity nesnesi içermediği için zaten entity takibi beklenmez. Tracking davranışını değerlendirirken yalnızca metoda bakmak yerine sonucun hangi nesneleri içerdiğini inceleyin. EF Core’un ayrıntıları için tracking ve no-tracking dokümantasyonu kullanılabilir. Her DTO projection’ının aynı davranışı taşıdığını varsaymayın.
Aynı sorgu, iki erişim şekli
Aşağıdaki örnek .NET 10 ve Microsoft.EntityFrameworkCore.Sqlite 10.0.12 kullanır. dotnet new console --framework net10.0 sonrasında dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.12 komutunu çalıştırın. SQLite connection açık kaldığı sürece in-memory database korunur.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
await using var connection = new SqliteConnection(
"Data Source=:memory:"
);
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<CatalogDb>()
.UseSqlite(connection)
.Options;
await using var db = new CatalogDb(options);
await db.Database.EnsureCreatedAsync();
db.Products.Add(new Product { Name = "Keyboard", Active = true });
db.Products.Add(new Product { Name = "Old mouse", Active = false });
await db.SaveChangesAsync();
var direct = await db
.Products.AsNoTracking()
.Where(x => x.Active)
.OrderBy(x => x.Id)
.Select(x => new ProductDto(x.Id, x.Name))
.ToListAsync();
IProductReads reads = new ProductReads(db);
var wrapped = await reads.ListActiveAsync(CancellationToken.None);
if (!direct.SequenceEqual(wrapped) || wrapped.Count != 1)
throw new InvalidOperationException("Query contract mismatch.");
Console.WriteLine("Both query paths returned the same DTO.");
public sealed record ProductDto(int Id, string Name);
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public bool Active { get; set; }
}
public sealed class CatalogDb(DbContextOptions<CatalogDb> options)
: DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}
public interface IProductReads
{
Task<IReadOnlyList<ProductDto>> ListActiveAsync(
CancellationToken ct
);
}
public sealed class ProductReads(CatalogDb db) : IProductReads
{
public async Task<IReadOnlyList<ProductDto>> ListActiveAsync(
CancellationToken ct
) =>
await db
.Products.AsNoTracking()
.Where(x => x.Active)
.OrderBy(x => x.Id)
.Select(x => new ProductDto(x.Id, x.Name))
.ToListAsync(ct);
}
İlk sorgu context üzerinden DTO projection yapar. İkinci sorgu, application’ın istediği sonucu sunan repository’ye gider. Repository IQueryable döndürmediği için tüketici provider’a bağlı yeni sorgular kuramaz. Bunun bedeli, yeni okuma gereksinimlerinde contract’a metot eklemek ve implementasyonunu sürdürmektir.
IQueryable dışarı açılınca ne korunur?
Generic repository üzerinden Query() sunup application içinde Include, AsNoTracking ve provider’a özel fonksiyonlar kullanıyorsanız EF Core bağımlılığını kaldırmış olmazsınız. Bu tercih yasak değildir; fakat interface’in neyi koruduğunu doğru tanımlamak gerekir. Query translation ve execution davranışı hâlâ provider’a bağlıdır.
Tercihim, basit query tarafında açık projection ve doğrudan context. Karmaşık aggregate yüklemesi veya domain odaklı persistence sınırı gerektiğinde küçük repository contract’ı. Clean Architecture yaklaşımı bu kararı destekleyebilir, ancak aynı klasör şeması her uygulamaya zorunlu değildir.
SaveChanges kimin sorumluluğunda?
Repository içindeki her Add çağrısında kayıt yapmak, iki repository’nin değişikliklerini birlikte koordine etmeyi zorlaştırır. İş birimini application işlemi belirliyorsa aynı scoped context üzerinde değişiklikleri biriktirip uygun noktada kaydetmek daha açık olabilir. Repository’nin read-only olması bu örnekte commit sorumluluğunu zaten kapsam dışında tutar.
Testlerde repository’yi mock etmek, application’ın branching davranışını kontrol etmek için yararlıdır. Aynı test SQL çevirisini, collation’ı, unique constraint’i veya transaction davranışını doğrulamaz. Veritabanı olmadan test etme dokümantasyonu bu sınırın nasıl kurulduğunu anlatıyor.
Örnekte gerçek SQLite provider’ı kullanılır. SQL Server veya PostgreSQL ile aynı davranış garanti edilmez. Özellikle sıralama, tarih ve metin karşılaştırmaları için hedef engine üzerinde entegrasyon testi çalıştırın. Repository kullanımı bu gereksinimi ortadan kaldırmaz; yalnızca application ile persistence arasındaki sözleşmeyi değiştirir.
IQueryable neden güçlü ama geçirgen bir contract’tır?
IQueryable<Product> döndüren repository tüketiciye yeni filtre, sıralama ve projection ekleme esnekliği verir. Bu yararlı olabilir; özellikle query sahipliğini application tarafında tutmak istediğinizde sorgu birleştirmek kolaylaşır. Fakat query henüz çalışmamıştır. Provider’ın desteklediği ifadeler, deferred execution, context yaşam süresi ve cancellation kararı hâlâ tüketicinin gündemindedir. Bu contract’ı persistence bağımsızlığı diye tanımlamak yanlış olur. Interface kullanılmış olsa da application artık provider’ın query davranışıyla ilişki kurar. Bu bağımlılık bilinçli seçilebilir, ancak sağladığı sınır açık ifade edilmelidir.
Sorgunun repository dışında materialize edilmesi hata yerini de etkiler. Metodun döndüğü anda veri erişimi başarılı görünür; gerçek hata ToListAsync sırasında başka katmanda ortaya çıkabilir. Context bu sırada dispose olmuşsa sorgu çalışmaz. Tüketici sayfalama uygulamazsa beklenenden çok veri çekebilir. Güvenlik filtresi kolayca değiştirilebilir veya başka bir sorguyla birleştirilebilir. Bu risklerin hepsi IQueryable’ı yasaklamak için yeterli değildir; fakat contract’ın davranış sorumluluğu genişler. Kim hangi filtreyi garanti ediyor ve sorgunun çalışmasını kim yönetiyor soruları yanıtlanmalıdır.
Materialize edilmiş IReadOnlyList<ProductDto> sonucu ise sorgunun tamamlandığını ve dönen veri biçiminin sınırlandığını gösterir. Tüketici provider’a yeni ifade ekleyemez. Bunun karşılığında her yeni filtre veya farklı liste ihtiyacı için contract’ı büyütmek gerekebilir. Çok genel bir options nesnesiyle bütün query seçeneklerini tekrar üretmeye çalışırsanız repository küçük bir sorgu dili haline gelebilir. Bu noktada doğrudan context veya feature’a ait ayrı query object daha açık olabilir. Esnekliği azaltmanın bedeli de esnekliği açmanın sorumluluğu kadar görünür olmalıdır. Contract tasarımında tek yönlü avantaj hesabı yapmayın.
Generic repository ne zaman ek yük oluşturur?
GetAll, GetById, Add, Update ve Delete metotlarını her entity için sunan bir interface başlangıçta düzenli görünebilir. Ancak uygulamanın önemli kararları çoğu zaman bu işlemlerin kombinasyonunda bulunur. Siparişin onay için yüklenmesi ile sadece özet için okunması aynı GetById davranışını gerektirmeyebilir. Bütün navigation property’leri yüklemek maliyet yaratabilir; hiçbirini yüklememek iş kuralını eksik veriyle çalıştırabilir. Genel metot, bu kararı parametre veya gizli convention ile ifade etmek zorunda kalır. Interface’in sade görünmesi implementasyonun anlaşılır olduğu anlamına gelmez.
Include listeleri, tracking seçenekleri, predicate expression’ları ve pagination parametreleri arttığında abstraction yeniden EF Core’un query yüzeyine yaklaşır. Bu durumda neden doğrudan EF kullanılmadığı açıklanmalıdır. Merkezi tenant filtresi veya auditing gibi gerçek gerekçe varsa bunları açık mekanizmalarla koruyabilirsiniz. Her şeyi generic repository içinde çözmeye çalışmak hem okuma hem yazma davranışını birbirine bağlayabilir. Genel bir repository’nin kaldırılması gerekiyorsa bütün feature’ları bir anda yeniden yazmak zorunlu değildir. Yeni sorguları açık tutup mevcut katmanın gerçekten değer üreten metotlarını aşamalı değerlendirmek daha düşük riskli olabilir.
Unit of Work ve commit sorumluluğunu karıştırmamak
DbContext değişiklikleri izler ve SaveChanges ile kaydetmeyi koordine eder. Bunun üzerine başka bir Unit of Work interface’i eklemek mümkün olsa da hangi ihtiyacı karşıladığı belirlenmelidir. Sadece aynı metoda farklı isim vermek yeni transaction garantisi sağlamaz. İki repository farklı context instance kullanıyorsa ortak Unit of Work adı onları tek atomik işleme dönüştürmez. Lifetime, connection ve transaction düzeni gerçek davranışı belirler. Dolayısıyla uygulamanın bir iş işlemi sırasında kaç context kullandığını ve hangi noktada commit ettiğini görünür tutmak gerekir.
Örneğin sipariş ve audit kaydı aynı iş işleminin parçalarıysa repository’nin her Add metodunda ayrı SaveChanges yapması kısmi kayıt bırakabilir. Application işlemi iki değişikliği aynı context üzerinde biriktirip kaydetmeyi seçebilir. Birden fazla SaveChanges veya context gereksinimi varsa transaction ayrıca düşünülür. Repository’ye commit yetkisi vermek bazı küçük işlerde uygun olabilir; fakat tüketicinin koordinasyon ihtiyacıyla çelişmemelidir. Buradaki read-only contract commit sunmadığı için bu sorumluluğu kapsamına almaz. Okuma abstraction’ının başarılı olması yazma tasarımını doğrulamaz.
Context lifetime ve paralel kullanımın sınırları
Repository interface’inin varlığı context yaşam süresini gizleyebilir. Scoped context alan bir implementasyonu singleton servise taşımak doğru değildir. Aynı context instance üzerinde bağımsız işlemleri paralel çalıştırmak da güvenli varsayılmamalıdır. DbContext lifetime dokümantasyonu yaşam süresi ve thread kullanımı için temel kaynaktır. Bir request içinde aynı repository’ye birden fazla çağrı yapılması, bu çağrıların otomatik olarak paralel çalıştırılabileceği anlamına gelmez. Performans amacıyla Task.WhenAll eklenmeden önce dependency’nin eşzamanlı kullanım kapasitesi kontrol edilmelidir.
Background worker gibi request scope bulunmayan yerde veri erişimi başlatılıyorsa scope veya context oluşturma düzeni açık olmalıdır. Interface’in adı bu düzeni tüketiciden tamamen silemez; implementasyonun doğru lifetime ile resolve edilmesi gerekir. Benzer şekilde lazy sorgu sonucu döndürmek context’in dışarıdan beklenen süresini uzatabilir. Materialize edilmiş DTO bu noktada yaşam süresi bağımlılığını azaltır. Ancak büyük sonuçları bellekte toplama maliyeti vardır. Streaming seçilecekse connection, cancellation ve consumer’ın enumerasyon davranışı contract’a dahil edilmelidir. Veri erişim biçimiyle operasyonel yaşam döngüsünü birlikte değerlendirin.
Test double hangi davranışı gösterebilir?
Bir application işlemi aktif ürün listesi boşsa farklı sonuç üretiyorsa repository stub’ı bu branch’i hızlı biçimde sınayabilir. Stub istediğiniz sonucu döndürür; query’nin nasıl çalıştığı testin konusu değildir. Bu izolasyon faydalıdır. Ancak testin geçmiş olması repository implementasyonundaki filtre, sıralama veya projection’ın doğru olduğunu göstermez. İki test alanının birbirini tamamlaması gerekir. Repository kullanılmayan basit read feature’ında gerçek provider üzerinde query testi de doğrudan davranışı sınayabilir. Araç seçiminden önce hangi hatanın yakalanacağını belirlemek daha değerlidir.
Bellekte koleksiyon üzerinde çalışan fake query ile relational provider’ın davranışı aynı olmayabilir. Metin karşılaştırması, provider fonksiyonları ve constraint’ler ayrışabilir. Bu nedenle production engine için uygun integration test alanı korunmalıdır. EF Core test stratejisi rehberi seçeneklerin maliyetini ve sınırlarını karşılaştırır. Bu makaledeki SQLite örneği gerçek SQLite davranışını gösterir; başka engine’e ilişkin güvence vermez. Testte kullanılan teknolojiyi değiştirmek, testin sorduğu soruyu da değiştirir. Bu fark raporda açık yazılmalıdır.
Güvenlik filtreleri repository kararıyla nasıl ilişkilidir?
Tenant kullanılan katalogda sadece active filtresi yeterli değildir. Kullanıcı kendi tenant’ına ait kayıtları görmelidir. Bu koşul her endpoint’te ayrı yazılıyorsa bir yerde unutulabilir. Repository veya başka merkezi query mekanizması filtreyi koruyabilir; fakat contract’ın filtreyi garanti edip etmediği belli olmalıdır. Bir yönetim ekranının bütün tenant’ları okuma yetkisi varsa normal kullanıcı akışıyla aynı geniş yetkili sorguyu paylaşmak risk yaratır. Soyutlama altında hangi erişim bağlamının taşındığı görülebilmelidir. Veri erişim düzeni authorization kararını rastlantısal hale getirmemelidir.
Silinmiş veya arşivlenmiş kayıtlar için de benzer sorun çıkar. GetById metodunun bu kayıtları dönüp dönmediği kullanıcı davranışını etkiler. Contract sadece teknik kimlikle tanımlanırsa tüketiciler farklı beklenti geliştirir. Hangi okumanın aktif kayıtları, hangisinin yönetim için bütün kayıtları sunduğunu belirtmek gerekir. Güvenlik ve iş filtrelerini aynı genel bool parametrelerle yönetmek okunabilirliği azaltabilir. Daha açık kullanım senaryosu metotları bazen daha çok isim üretir ama review’da kritik davranışı görünür kılar. Bu isimlerin implementasyon ve testle desteklenmesi şarttır.
Repository’ye geçiş ve repository’den sadeleşme
Doğrudan context kullanan uygulamada repository ihtiyacı oluştuğunda bütün query’leri taşımak yerine sorun yaratan bir feature seçin. Tüketicinin hangi EF ayrıntılarına bağlı olduğunu listeleyin. Ardından gerçekten gereken sonucu veya aggregate durumunu tanımlayın. Contract içine istemeden query expression taşımamak için parametrelerin anlamını inceleyin. Mevcut davranışı provider testiyle sabitleyip yeni implementasyonun aynı sonucu ürettiğini doğrulayın. Böylece abstraction eklerken sorgu davranışını da değiştirmiş olmazsınız. Query optimizasyonu gerekiyorsa bunu ayrı gerekçe ve ölçümle ele alın.
Ters yönde, generic repository kaldırılırken de bütün iş kurallarını endpoint’e taşımak gerekmez. Önce yalnızca geçiş yapan read metotlarını değerlendirin. Aggregate yükleme veya merkezi erişim politikası sağlayan metotlar tutulabilir. Kalan query’lerin sahipliğini feature bazında açık hale getirin. İşlem içindeki commit ve transaction noktaları değişiyorsa bu değişikliğe özel test çalıştırın. Dosya sayısı azaldığında bakımın iyileştiğini varsaymak yerine yeni bir özellik üzerinde değişiklik yolunu deneyin. Sadeleşmenin faydası kod gezinmesinde ve davranışın anlaşılmasında görülmelidir.
Kararı review’da somutlaştırmak
EF Core Repository Pattern için bir karar kaydı, korunacak bağımlılığı ve kabul edilen maliyeti birlikte yazmalıdır. Basit ürün listesinde query sahipliğini feature’da tutmak yeterli olabilir. Sipariş aggregate’inin belirli durumla yüklenmesinde repository anlamlı olabilir. Dışarı IQueryable açılıyorsa provider bağımlılığının sürdüğünü kaydedin. Materialize edilmiş sonuç dönüyorsa yeni query ihtiyaçlarının contract değişikliği gerektireceğini belirtin. Bu kayıt farklı tercihlerin neden aynı uygulamada bulunduğunu açıklayabilir. Ekip tutarlılığı aynı interface’i her yere eklemekten çok, aynı soruya aynı gerekçelerle yaklaşmakla sağlanır.
Son review kontrolünde tüketicinin ne bildiğine bakın: Database kolonlarını mı, EF extension metotlarını mı, yoksa belirli iş sonucunu mu görüyor? Sonra hata ve lifetime sorumluluklarına bakın: Sorgu nerede çalışıyor, cancellation kimden geliyor ve commit’i kim yapıyor? Bu sorular açık yanıtlanıyorsa seçilen yaklaşımın sınırı anlaşılırdır. Repository bir amaç haline geldiğinde tartışma sınıf isimlerine sıkışır. İş kullanımını esas aldığınızda ise hem doğrudan context hem küçük repository aynı tasarımda bilinçli ve savunulabilir seçenekler olabilir.
Sayfalama ve büyük sonuçlarda contract tasarımı
Aktif ürünlerin hepsini döndüren örnek, iki kayıtla anlaşılır kalır. Gerçek bir katalogda aynı metodu milyonlarca kayıt için kullanmak uygun olmayabilir. Tüketicinin hangi sıralamayla, kaç kayıt ve hangi devam bilgisiyle çalışacağı tanımlanmalıdır. Sadece page size parametresi eklemek yeterli değildir; sıralamanın kararlı olması ve limitin kontrol edilmesi de gerekir. Repository bu kararları kapsıyorsa onlar contract’ın garanti ettiği davranıştır. Doğrudan context seçiliyorsa aynı kararlar feature’ın sorumluluğunda kalır. İki yaklaşımda da geniş sorgunun kontrolsüz çalışması mimari isimlerle önlenmez.
Örneğin kimlik sıralamasıyla sonraki ürün grubunu isteyen bir endpoint, cursor bilgisine ihtiyaç duyabilir. Bu cursor’ın entity’ye veya provider’a özel bir nesne olması gerekmez. Son görülen sıralama değerini taşıyan API modeli kullanılabilir. Ancak değişen veri altında kayıtların görünme davranışı ayrıca açıklanmalıdır. Generic repository’nin bir GetAll metoduyla bütün pagination ihtiyaçlarını çözmesi beklenmemelidir. Kullanım senaryosunun devam, filtre ve tutarlılık beklentisini açık hale getirmek, query’nin bulunduğu sınıftan daha önemli olabilir. Performans değerlendirmesinde gerçek veri dağılımını ve execution plan’ı inceleyin.
Başarısız query ile boş sonucu ayırmak
Bu davranışları yalnızca interface yorumunda bırakmayın. Boş sonuç, provider hatası ve iptal için ayrı test senaryoları oluşturun; istemciye giden yanıtı ve operasyon kaydını birlikte değerlendirin. Test double kullanıyorsanız bunun gerçek bağlantı kesintisini veya database timeout’unu çalıştırmadığını da testin kapsamına yazın.
Repository implementasyonu her exception’ı yakalayıp boş liste döndürürse tüketici database arızasını normal iş sonucu sanabilir. Bir yönetim ekranı bu durumda katalogda ürün olmadığı mesajını verir; operasyon ekibi ise gerçek bağlantı sorununu göremeyebilir. Contract’ın boş sonuç anlamı net olmalıdır. Teknik hata uygun exception veya açık hata modeliyle taşınabilir. Bu seçimi bütün durumları sessizce başarıya dönüştüren bir catch bloğuyla yapmayın. Hata eşleme, persistence hatasını istemciye aynen açmak zorunda değildir; fakat uygulama içinde nedenin kaybolmaması gerekir.
Cancellation için de benzer ayrım geçerlidir. İptal edilen veri erişimini boş sonuç gibi sunmak, istemcinin işlemi tamamlandı kabul etmesine neden olabilir. Read işlemi ile commit gerçekleşmiş bir write işleminin iptal davranışı aynı değildir. Repository’ye token parametresi eklediğinizde implementasyonun gerçekten ilgili async çağrıya bunu verdiğini kontrol edin. Böylece interface üzerinde destekleniyor görünen davranışın çalışma sırasında da uygulandığını doğrulayabilirsiniz. Contract incelemesinde başarı yolu kadar hata ve iptal yoluna bakmak, abstraction’ın gerçekten hangi sorumluluğu taşıdığını ortaya çıkarır.
