Skip to main content
Bir aggregate, birlikte tutarlı kalması gereken entity ve value object’lerin oluşturduğu sınırdır. Sınırın tepesinde tek bir aggregate root durur; dışarıdan tüm erişim ve değişiklik bu root üzerinden geçer. Bu sayede iş kuralları (invariant) tek bir yerde garanti altına alınır.
Temel kural: Çocuk entity’ler dışarıya doğrudan açılmaz. User.Sessions koleksiyonuna yeni oturum eklemek için user.StartSession(...) çağrılır — user.Sessions.Add(...) değil. Repository yalnızca root için vardır.

User aggregate’i — derinlemesine

User, back-office (personel) kullanıcısını temsil eden en zengin aggregate’tir. Tüm desenleri tek örnekte gösterir.
public class User : Entity, IAggregateRoot
{
    public long ReferenceNumber { get; protected set; }
    public FullName? FullName { get; private set; }
    public Phone?    Phone    { get; private set; }
    public Email?    Email    { get; private set; }

    public bool IsPhoneNumberVerified { get; private set; }
    public bool IsEmailVerified       { get; private set; }

    public TotpConfigurationInfo? Totp { get; private set; }
    public virtual int StatusId { get; private set; }
    public virtual UserStatus Status => UserStatus.FromValue<UserStatus>(StatusId);

    public int TokenVersion { get; private set; } = 1; // RBAC token invalidation

    public virtual ICollection<UserSession> Sessions { get; private set; } = new HashSet<UserSession>();
    public virtual ICollection<UserOtpChallenge> OtpChallenges { get; private set; } = new HashSet<UserOtpChallenge>();
    public virtual ICollection<UserIdentityProvider> IdentityProviders { get; private set; } = new HashSet<UserIdentityProvider>();
    public virtual ICollection<UserRole> UserRoles { get; private set; } = new HashSet<UserRole>();
    // ...
}

Value object alanları

FullName, Phone, Email birer value object’tir (nullable — kullanıcı telefon veya e-posta ile kaydolabilir). Totp ise TotpConfigurationInfo VO’sudur. Setter’lar private’tır; değişim yalnızca davranış metotları üzerinden olur.

Çocuk entity’ler

Çocuk entityRolü
UserSessionRefresh token tabanlı oturum (cihaz + IP bilgisi ile).
UserOtpChallengeSMS/E-posta OTP doğrulama denemesi (deneme/yeniden gönderim sayacı).
UserIdentityProviderDış IdP bağlantısı (Google/Meta/Apple/Keycloak).
UserRoleRol atama kaydı (opsiyonel TenantId ile).

internal ctor — factory zorunluluğu

Constructor internal’dır. Bu, User’ın yalnızca aynı assembly içindeki factory (UserFactory) veya domain service (UserRegistrationService) tarafından oluşturulabilmesini garanti eder. Böylece uniqueness gibi kurallar atlanamaz.
protected User() { }
internal User(FullName? fullName, Phone? phone = null, Email? email = null) : this()
{
    this.Id = GuidFactory.New();

    if (phone == null && email == null)
        throw new DomainException("Kullanıcı kaydı için en az bir telefon ya da e-posta tanımlanmalıdır");

    this.FullName = fullName;
    this.Phone    = phone;
    this.Email    = email;
    this.StatusId = UserStatus.Draft.Id;

    AddDomainEvent(new UserCreatedDomainEvent(Id, fullName, Phone, Email));
}
protected User() parametresiz ctor EF Core’un materializasyonu için gereklidir; internal ctor ise iş kurallarını barındırır. Oluşturma anında UserCreatedDomainEvent kuyruğa eklenir — kayıt persist edildikten sonra dispatch edilir.

UserStatus — durum makinesi

public class UserStatus : Enumeration
{
    public static UserStatus Draft     = new(0, nameof(Draft));     // kanal henüz doğrulanmadı
    public static UserStatus Active    = new(1, nameof(Active));    // en az bir kanal doğrulandı, login serbest
    public static UserStatus Suspended = new(2, nameof(Suspended)); // kullanıcı kendi dondurdu
    public static UserStatus Banned    = new(3, nameof(Banned));    // admin tamamen engelledi
    public static UserStatus Pending   = new(4, nameof(Pending));   // dış IdP geldi, admin onayı bekliyor
}
Durum, davranışı kapılayan hesaplanan property’lerle okunur:
public bool CanLogin      => Status == UserStatus.Active;
public bool CanRequestOtp => Status == UserStatus.Draft || Status == UserStatus.Active;
public bool IsBlocked     => Status == UserStatus.Banned || Status == UserStatus.Suspended;

Davranış metotları

Aggregate’in tüm yetenekleri açık niyetli (intention-revealing) metotlardır. Birkaç örnek:
public void StartSession(RefreshTokenInfo refreshToken, bool isTrustedDevice, DeviceInfo device, ClientIpInfo clientIp)
{
    if (this.IsBlocked)
        throw new DomainException("Kullanıcı durumu oturum açmaya uygun değil.");

    var session = new UserSession(GuidFactory.New(), this, refreshToken, isTrustedDevice, device, clientIp);
    this.Sessions.Add(session);
}
RefreshSession, mevcut token’ı eşleştirip yeni token’a döndürür (rotation).
public void AddOtpChallenge(OtpCode code, OtpType type)
{
    if (CanRequestOtp == false)
        throw new DomainException("Kullanıcı durumu giriş yapmaya uygun değil.");

    var validityMinutes = type == OtpType.Sms ? 1 : 2;
    var challenge = new UserOtpChallenge(this, GuidFactory.New(), code, type, DateTime.UtcNow.AddMinutes(validityMinutes));
    OtpChallenges.Add(challenge);

    AddDomainEvent(new UserOtpGeneratedDomainEvent(this, code, type, this.Phone, this.Email));
}
VerifyOtp/VerifyChallenge doğru kodda ilgili kanalı doğrular ve UserPhoneNumberVerifiedDomainEvent / UserEmailVerifiedDomainEvent yayar.
public bool IsTotpEnabled => Totp is { IsEnabled: true };

public void ConfigureTotp(TotpSecret secret) => this.Totp = TotpConfigurationInfo.Configure(secret);
public void EnableTotp()  { if (Totp == null) throw new DomainException("TOTP konfigüre edilmemiş"); Totp = Totp.Enable(); }
public void VerifyTotp(long timeStep)
{
    if (Totp is null) throw new DomainException("TOTP tanımlı değil");
    if (!Totp.CanAccept(timeStep)) throw new DomainException("Kod tekrar kullanılamaz");
    Totp = Totp.MarkUsed(timeStep); // replay koruması
}
TotpConfigurationInfo immutable bir VO’dur; her geçiş yeni bir örnek döndürür.
public void AssignRole(int roleId, Guid? tenantId, Guid assignedBy)
{
    if (!Enumeration.InRange<Role>(roleId))
        throw new DomainException($"Geçersiz rol: {roleId}");

    if (UserRoles.Any(r => r.RoleId == roleId && r.TenantId == tenantId && !r.IsDeleted()))
        return; // idempotent

    UserRoles.Add(new UserRole(GuidFactory.New(), Id, roleId, tenantId, assignedBy));
    AddDomainEvent(new UserRoleAssignedDomainEvent(Id, roleId, tenantId, assignedBy));
}
InvalidateTokens(), TokenVersion’ı artırarak mevcut tüm JWT’leri geçersiz kılar.
public void AddOrUpdateExternalProvider(AuthProviderType providerType, string providerUserId, string? displayName)
{
    var existing = IdentityProviders.FirstOrDefault(x =>
        x.ProviderType == providerType && x.ProviderUserId == providerUserId);

    if (existing is not null) { existing.UpdateDisplayNameIfProvided(displayName); return; } // idempotent

    IdentityProviders.Add(new UserIdentityProvider(GuidFactory.New(), Id, providerType, providerUserId, displayName));
}
ActivateOnExternalLogin(), başarılı dış login’de Draft/Pending kullanıcıyı Active’e geçirir; Banned/Suspended’e dokunmaz, Active ise idempotent çıkar.
Davranış metotlarında tekrar eden iki desen dikkat çeker: idempotency (aynı işlem tekrar çağrılırsa sessizce geçer) ve guard + DomainException (kural ihlalinde anlamlı hata). Yeni metot yazarken her ikisini de göz önünde bulundurun.

Citizen aggregate — User’a paralel

Citizen, vatandaş portalının kullanıcısıdır ve User ile neredeyse birebir aynı yapıya sahiptir (FullName/Phone/Email, OTP/TOTP/oturum, dış IdP, CitizenStatus). Ama ayrı bir aggregate’tir — farklı tablolar, farklı Keycloak realm’i ve farklı iş kuralları taşır. Ek olarak CitizenProfileImageInfo (profil fotoğrafı) içerir.
public class Citizen : Entity, IAggregateRoot
{
    public FullName? FullName { get; private set; }
    public CitizenProfileImageInfo? ProfileImage { get; private set; }
    public virtual CitizenStatus Status => CitizenStatus.FromValue<CitizenStatus>(StatusId);
    public virtual ICollection<CitizenSession> Sessions { get; private set; }
    // ... User ile paralel ...
}
İki aggregate’i kasıtlı olarak ayrı tutmak, personel ve vatandaş kimlik akışlarının bağımsız evrilmesini sağlar. Ortak ihtiyaçlar value object düzeyinde (FullName, Phone…) paylaşılır.

Diğer aggregate’ler — özet

Kurum/tenant. Branch (şube) ve StaffMember (personel) çocuk entity’leri; StaffPermission izin kayıtları. AddBranch(...) gibi root metotları ile yönetilir.
RBAC rol–izin eşlemesi. Hangi rolün hangi PermissionCode’a sahip olduğunu tutar.
Bağış başvurusu bir state machine’dir: OnBasvuru → Incelemede → ... Her geçiş BagisBasvuruStatusHistory kaydı ve domain event üretir (IncelemeyeAl, Onayla, Reddet, IptalEt). BagisBasvuruPlan başvuru şablonudur.
Etkinlik başvurusu; bağış ile paralel state machine. Plan kontenjanı dolunca EtkinlikBasvuruPlanKontenjanDolduDomainEvent yayılır, iptalde kontenjan geri verilir.
Destek talebi. Çocuklar: SupportTicketComment, SupportTicketAttachment, SupportTicketStatusHistory. Atama ve durum değişiminde event yayar.
AdminNotification: SSE ile yayınlanan admin bildirimi. LegalDocument: yasal metin + LegalDocumentVersion. Center: merkez lokasyonu. SiteSettings: tekil site ayarı. District: int kimlikli sabit ilçe referans verisi.

Tüm aggregate tablosu

AggregateÇocuk entity’ler / VO’larEnumeration’lar
UserUserSession, UserOtpChallenge, UserIdentityProvider, UserRoleUserStatus
CitizenCitizenSession, CitizenOtpChallenge, CitizenIdentityProvider, CitizenProfileImageInfoCitizenStatus
OrganizationBranch, StaffMember, StaffPermissionOrganizationStatus, BranchStatus, StaffStatus, StaffRole
RolePermissionPermission, PermissionCodesRole
AnnouncementAnnouncementImageInfo
BagisBasvuruBagisBasvuruStatusHistory, BagisOdemeInfoBagisBasvuruStatus, OdemeStatus
BagisBasvuruPlanBagisPlanImageInfoBagisBasvuruPlanType
EtkinlikBasvuruEtkinlikBasvuruStatusHistoryEtkinlikBasvuruStatus
EtkinlikBasvuruPlanEtkinlikPlanImageInfoEtkinlikBasvuruPlanType
SupportTicketSupportTicketComment, SupportTicketAttachment, SupportTicketStatusHistory, DiagnosticContextSupportTicketStatus/Priority/Type/Source/ReporterType
AdminNotificationAdminNotificationReadNotificationCategory, NotificationSeverity
LegalDocumentLegalDocumentVersionLegalDocumentType
Center
SiteSettings
DistrictCity

Sonraki adımlar

Value Object'ler

Aggregate alanlarında kullanılan VO kataloğu.

Domain Event'ler

AddDomainEvent ile yayılan olayların dispatch akışı.

Factory ve Servisler

internal ctor’lu aggregate’ler nasıl oluşturulur.

SharedKernel

Entity, Enumeration ve guard altyapısı.