Karmaşık oluşturma için factory, tek aggregate’e sığmayan iş kuralları için domain service. IUserFactory ve UserRegistrationService örnekleri.
Aggregate’in internal constructor’ı, oluşturmanın kontrollü olmasını sağlar (bkz. Aggregate’ler). Ama bir kullanıcı oluşturmadan önce “bu e-posta zaten var mı?” gibi kontroller yapmak gerekir — bu kontroller tek bir aggregate örneğinin sorumluluğu değildir. İşte burada factory ve domain service devreye girer.
Factory
Karmaşık oluşturma mantığını kapsüller: uniqueness kontrolü, dış IdP eşleştirme, doğru ctor seçimi. Sonuç: geçerli bir aggregate.
Domain Service
Tek bir aggregate’e doğal olarak ait olmayan iş kurallarını barındırır (genelde birden fazla aggregate’i ya da repository’yi ilgilendirir).
public async Task<User> CreateWithPhoneAsync(FullName? name, Phone? phone, CancellationToken ct){ if (phone == null) throw new DomainException("Telefon numarası boş olamaz."); var result = await _uniquenessChecker.CheckPhoneUniquenessAsync(phone, ct); switch (result) { case UniquenessResult.Success: return new User(name, phone, email: null); // internal ctor — factory içinden erişilebilir case UniquenessResult.AlreadyExists: throw new DomainException("Bu telefon numarası ile zaten aktif bir üyelik bulunmaktadır. Lütfen giriş yapınız."); case UniquenessResult.ExistsButNotVerified: throw new DomainException("Bu telefon numarasına ait bir kayıt mevcut ancak henüz doğrulanmamış. Lütfen sisteme giriş yapıp doğrulama adımını tamamlayın."); default: throw new ArgumentOutOfRangeException(nameof(result)); }}
Factory, User’ı oluşturmadan önceIUserUniquenessChecker’a danışır. internal ctor sayesinde kimse bu kontrolü atlayarak User oluşturamaz — geçersiz durum domain dışına çıkamaz.
ICitizenFactory, IUserFactory ile birebir aynı arayüze sahiptir (vatandaş portalı için). Aynı oluşturma deseni iki ayrı aggregate’e tutarlı şekilde uygulanır.
Bir kullanıcının e-posta/telefon/dış kimlik açısından benzersiz olup olmadığı, tek bir User örneğinin bilemeyeceği bir kuraldır — tüm kullanıcı kümesine bakmak gerekir. Bu yüzden bir domain service’tir.
public interface IUserUniquenessChecker{ Task<UniquenessResult> CheckEmailUniquenessAsync(Email email, CancellationToken ct); Task<UniquenessResult> CheckPhoneUniquenessAsync(Phone phone, CancellationToken ct); Task<(UniquenessResult, User?)> CheckExternalIdentityUniquenessAsync(AuthProviderType provider, string externalId, CancellationToken ct);}public enum UniquenessResult{ Success, // benzersiz, oluşturulabilir AlreadyExists, // doğrulanmış kayıt var ExistsButNotVerified // kayıt var ama kanal doğrulanmamış}
Üç-durumlu UniquenessResult, factory’nin kullanıcıya anlamlı mesaj verebilmesini sağlar: “zaten var, giriş yap” ile “var ama doğrulanmamış, doğrulamayı tamamla” farklı yönlendirmeler gerektirir.
Daha basit bir kayıt yolu sunan, repository üzerinden doğrudan uniqueness kontrolü yapıp User döndüren bir domain service:
public class UserRegistrationService{ private readonly IRepository<User> _userRepository; public async Task<User> RegisterNewUserAsync(FullName? name, Phone? phone = null, Email? email = null, CancellationToken ct = default) { if (phone == null && email == null) throw new DomainException("Kayıt için telefon veya e-posta gereklidir"); if (phone != null) { var existing = await _userRepository.FirstOrDefaultAsync(new UserByPhoneNumberSpecification(phone), ct); if (existing is not null) { if (!existing.IsPhoneNumberVerified) throw new DomainException("Bu telefon numarasına sahip bir kullanıcı zaten mevcut fakat üyelik onayı yapılmamış. Lütfen giriş yapınız"); throw new DomainException("Bu telefon numarasına sahip bir kullanıcı zaten mevcut"); } } // e-posta için benzer kontrol ... return new User(name, phone, email); }}
Bir aggregate’i oluşturmak ek mantık gerektiriyorsa (uniqueness, dış IdP eşleştirme, hangi ctor/overload seçileceği, başlangıç event’leri) factory uygundur. Çıktısı her zaman geçerli bir aggregate’tir.
Domain service kullan — kural tek aggregate'e ait değilse
İş kuralı birden fazla aggregate’i, tüm bir koleksiyonu (uniqueness) veya repository sorgusunu ilgilendiriyorsa domain service’e koyun. Stateless’tır; durumu aggregate’lerde bırakır.
İkisi birlikte
Pratikte UserFactory, oluşturma orkestrasyonunu yapar ve kural değerlendirmesi için IUserUniquenessChecker domain service’ine delege eder. Sorumluluklar net ayrılır: factory “nasıl oluşturulur”, service “izin var mı”.
Ne factory ne de domain service, kaydı persist etmez. Onlar yalnızca geçerli aggregate’i döndürür; repository’ye ekleme ve UnitOfWork.SaveEntitiesAsync çağrısı Application katmanındaki command handler’ın işidir. Böylece domain, transaction sınırından habersiz kalır.