Skip to main content
Value object (VO), kimliği olmayan, yalnızca taşıdığı değere göre eşitlenen ve oluşturulduktan sonra değişmeyen (immutable) bir nesnedir. İki Email("a@b.com") örneği eşittir; çünkü onları ayıran bir kimlik yoktur — yalnızca değerleri vardır. VO’lar domain modelinde iki kritik iş görür:
  1. Geçersiz durumu temsil edilemez kılar. Email nesnesi varsa, o e-posta zaten regex’ten geçmiştir. Aggregate içinde tekrar doğrulamaya gerek yoktur.
  2. Anlam taşır. string phone yerine Phone; ham veri değil, kuralları kendi içinde taşıyan bir kavram.

Tasarım: değişmezlik + ctor validasyonu

Tüm VO’lar ValueObject tabanından türer ve şu deseni izler:
  • Doğrulama constructor’da (veya statik fabrika metodunda) yapılır; ihlalde DomainException fırlatılır.
  • Alanlar get-only’dir; nesne oluştuktan sonra değişmez.
  • GetAtomicValues() eşitliğe katkıda bulunan alanları sıralar.
  • EF Core materializasyonu için protected parametresiz ctor bulunur.
public sealed class FullName : ValueObject
{
    public string Value { get; }

    protected FullName() {}                 // EF Core için
    public FullName(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new DomainException("Ad Soyad boş olamaz.");

        var normalized = Normalize(value);  // whitespace + Unicode FormC
        if (normalized.Length < 2 || normalized.Length > 200)
            throw new DomainException("Ad Soyad uzunluğu geçersiz.");
        if (!IsValid(normalized))           // Unicode harf + boşluk + . - '
            throw new DomainException("Ad Soyad geçersiz karakterler içeriyor.");

        Value = normalized;
    }

    protected override IEnumerable<object> GetAtomicValues() { yield return Value; }
}
FullName bilinçli olarak adı ad/soyad diye bölmez — kültürler arası isim yapıları (çoklu soyad, mononim, patronimik) farklıdır. Tek bir normalize edilmiş değer tutulur.
Bazı VO’lar public ctor yerine statik fabrika metodu kullanır (özellikle üretim/hash gibi yan etki içerenler):
public class OtpCode : ValueObject
{
    public const int MinCode = 1000, MaxCode = 9999;
    public string Value { get; }

    internal OtpCode(string value) => Value = value;

    public static OtpCode Create(string code)
    {
        if (string.IsNullOrWhiteSpace(code) || !int.TryParse(code, out var v) || v < MinCode || v > MaxCode)
            throw new DomainException("Hatalı OTP kodu");
        return new OtpCode(code);
    }

    public static OtpCode Generate()
        => new OtpCode(new Random().Next(MinCode, MaxCode + 1).ToString());
}

VO kataloğu

SharedKernel/ValueObjects/ altındaki başlıca value object’ler:
VOKural / Davranış
FullName2–200 karakter, Unicode harf + . - ', whitespace ve FormC normalizasyonu.
EmailRFC benzeri regex doğrulaması.
PhoneMobilePhoneUtility ile doğrulama + formatlama. ToNetGsmFormat() NetGSM için ham GSM döndürür.
OtpCode4 hane (1000–9999). Create(string) doğrular, Generate() rastgele üretir.
RefreshTokenInfoDüz token saklanmaz; SHA256 hash’i tutulur. Create(plain, expiresAt), Matches(plain).
TotpConfigurationInfoTOTP yapılandırması (immutable). Configure/Enable/Disable/MarkUsed; CanAccept(step) replay koruması.
TotpSecretŞifrelenmiş TOTP sırrı. Create(encrypted).
TCKimlikNo11 hane + checksum algoritması. IsForeign() (99/98/97 ile başlayan).
HESHES kodu format doğrulaması.
AgeDoğum tarihinden yaş/gün/ay/yıl hesaplar (IFormattable).
GeoLocationEnlem (-90..90) / boylam (-180..180) doğrulaması.
ImageImageMagick (Magick.NET) ile resize + watermark; byte[] saklar.
FileDosya içeriği + metadata VO’su.

Telefon — NetGSM formatı

public class Phone : ValueObject
{
    public string Value { get; protected set; }
    public Phone(string number)
    {
        if (!MobilePhoneUtility.Instance.IsValidNumber(number))
            throw new DomainException("Geçersiz telefon numarası");
        Value = MobilePhoneUtility.Instance.Format(number);
    }

    public string ToNetGsmFormat()  // +90 542 123 45 67 -> 5421234567
    {
        var digits = new string(Value.Where(char.IsDigit).ToArray());
        return digits.StartsWith("90") ? digits.Substring(2) : digits;
    }
}

Refresh token — düz değer asla saklanmaz

public sealed class RefreshTokenInfo : ValueObject
{
    public string TokenHash { get; private set; }
    public DateTime ExpiresAt { get; private set; }
    public bool IsExpired => DateTime.UtcNow >= ExpiresAt;

    public static RefreshTokenInfo Create(string plainToken, DateTime expiresAt)
        => new(Hash(plainToken), expiresAt);   // SHA256

    public bool Matches(string plainToken) => TokenHash == Hash(plainToken);
}
RefreshTokenInfo düz token’ı hiçbir zaman saklamaz; yalnızca SHA256 hash’ini tutar. Doğrulama Matches(plain) ile hash karşılaştırması üzerinden yapılır. DB sızıntısında token’lar açığa çıkmaz.

EF Core ile eşleme — kısaca

Value object’ler ayrı tablo değil, sahibi olan entity’nin kolonlarına açılır. EF Core’da bu, HasConversion (tek değerli VO için) veya ComplexProperty/OwnsOne (çok alanlı VO için) ile yapılır:
// Tek değerli VO → tek kolon
builder.Property(u => u.Email)
       .HasConversion(e => e.Value, v => new Email(v));
Eşleme detayları (hangi VO HasConversion, hangisi owned type olarak çıkarılıyor) Veri Katmanı dokümanındadır. Domain tarafı persistence’tan habersizdir — VO’lar EF Core’a göre değil, iş kuralına göre tasarlanır.

Enumeration örnekleri

Sabit kümeler enum yerine Enumeration ile modellenir (bkz. SharedKernel). Kimlik doğrulama ve yetkilendirmede sık kullanılanlar:
public class OtpType : Enumeration
{
    public static OtpType Sms           = new(1, nameof(Sms));
    public static OtpType Email         = new(2, nameof(Email));
    public static readonly OtpType Authenticator = new(3, nameof(Authenticator));
}

public class AuthProviderType : Enumeration
{
    public static AuthProviderType Google   = new(1, nameof(Google));
    public static AuthProviderType Meta     = new(2, nameof(Meta));
    public static AuthProviderType Apple    = new(3, nameof(Apple));
    public static AuthProviderType Keycloak = new(4, nameof(Keycloak));
}

public class Role : Enumeration
{
    public static Role SuperAdmin = new(1, nameof(SuperAdmin));
    public static Role Admin      = new(2, nameof(Admin));
    public static Role Staff      = new(3, nameof(Staff));
    public static Role ReadOnly   = new(4, nameof(ReadOnly));
}
EnumerationDeğerler
OtpTypeSms=1, Email=2, Authenticator=3
AuthProviderTypeGoogle=1, Meta=2, Apple=3, Keycloak=4
RoleSuperAdmin=1, Admin=2, Staff=3, ReadOnly=4
UserStatusDraft=0, Active=1, Suspended=2, Banned=3, Pending=4

Sonraki adımlar

Aggregate'ler

VO’ların User ve diğer aggregate’lerde kullanımı.

SharedKernel

ValueObject ve Enumeration taban sınıfları.

Veri Katmanı

VO → kolon eşlemesi ve HasConversion detayları.

Domain Event'ler

VO’ları payload olarak taşıyan event’ler.