Skip to main content
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).

IUserFactory — kullanıcı oluşturma fabrikası

User’ın ctor’u internal olduğu için, dışarıdan oluşturmanın tek meşru yolu factory’dir. Arayüz, tüm kayıt senaryolarını kapsar:
public interface IUserFactory
{
    Task<User> CreateWithPhoneAsync(FullName? name, Phone? phone, CancellationToken ct);
    Task<User> CreateWithEmailAsync(FullName? name, Email? email, CancellationToken ct);
    Task<User> GetOrCreateFromGoogleAsync(FullName name, Email? email, string googleUserId, CancellationToken ct);
    Task<User> GetOrCreateFromMetaAsync(FullName name, Email? email, string metaUserId, CancellationToken ct);
    Task<User> GetOrCreateFromKeycloakAsync(FullName? name, Email? email, string keycloakUserId, CancellationToken ct);
}
İki tür metot vardır:
  • CreateWith... — sıfırdan kayıt (telefon veya e-posta ile). Uniqueness başarısızsa DomainException fırlatır.
  • GetOrCreateFrom... — dış IdP (Google/Meta/Keycloak) login akışı. Mevcut kullanıcı varsa bağlar, yoksa oluşturur.

Uniqueness kontrolü ile oluşturma

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 önce IUserUniquenessChecker’a danışır. internal ctor sayesinde kimse bu kontrolü atlayarak User oluşturamaz — geçersiz durum domain dışına çıkamaz.

Dış IdP akışı — GetOrCreateFromExternalAsync

Google ve Meta ortak bir özel yardımcıyı kullanır. Eşleştirme önceliği üç adımdır:
1

Provider + sub eşleşmesi

(provider, externalId) ikilisi daha önce bağlanmışsa, o kullanıcı bulunur ve linkLogin ile display name tazelenir.
2

E-posta eşleşmesi

Aynı e-posta başka bir IdP’den gelmiş olabilir. Varsa mevcut kullanıcıya bu provider da eklenir (multi-provider) ve e-posta doğrulanmış sayılır.
3

Yeni kullanıcı

Hiçbir eşleşme yoksa yeni User oluşturulur, IdP bağlanır, e-posta varsa VerifyEmail() çağrılır.
private async Task<User> GetOrCreateFromExternalAsync(
    AuthProviderType provider, string externalId, FullName name, Email? email,
    Action<User, string> linkLogin, CancellationToken ct)
{
    // 1) sub eşleşmesi
    var (bySubResult, bySubUser) = await _uniquenessChecker.CheckExternalIdentityUniquenessAsync(provider, externalId, ct);
    if (bySubResult == UniquenessResult.AlreadyExists && bySubUser is not null)
    {
        linkLogin(bySubUser, externalId);
        return bySubUser;
    }

    // 2) email eşleşmesi — başka IdP'den gelmiş olabilir
    if (email is not null)
    {
        var byEmailResult = await _uniquenessChecker.CheckEmailUniquenessAsync(email, ct);
        if (byEmailResult == UniquenessResult.AlreadyExists)
        {
            var existing = await _userRepository.FirstOrDefaultAsync(new UserByEmailSpecification(email), ct);
            if (existing is not null) { linkLogin(existing, externalId); existing.VerifyEmail(); return existing; }
        }
        // ExistsButNotVerified → DomainException
    }

    // 3) Yeni kullanıcı
    var newUser = new User(name, phone: null, email);
    if (email is not null) newUser.VerifyEmail();
    linkLogin(newUser, externalId);
    return newUser;
}
linkLogin, provider’a göre değişen bağlama davranışını parametre olarak alır:
public Task<User> GetOrCreateFromGoogleAsync(FullName name, Email? email, string googleUserId, CancellationToken ct)
    => GetOrCreateFromExternalAsync(
        AuthProviderType.Google, googleUserId, name, email,
        linkLogin: (u, sub) => u.AddOrUpdateGoogleLogin(sub, displayName: null),
        ct: ct);
ICitizenFactory, IUserFactory ile birebir aynı arayüze sahiptir (vatandaş portalı için). Aynı oluşturma deseni iki ayrı aggregate’e tutarlı şekilde uygulanır.

Domain Service: IUserUniquenessChecker

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.

Domain Service: UserRegistrationService

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);
    }
}

Ne zaman factory, ne zaman domain service?

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.
İş 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.
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.

Sonraki adımlar

Aggregate'ler

internal ctor ve User davranış metotları.

Domain Event'ler

Oluşturmada yayılan UserCreatedDomainEvent.

SharedKernel

IRepository, IReadRepository ve specification altyapısı.

Domain'e genel bakış

Katmanın rolü ve klasör yapısı.