DDD yapı taşları: EntityBase, ValueObject, Enumeration, DomainEvent, guard altyapısı ve repository arayüzleri.
DiyanetCleanArchitecture.Domain.SharedKernel projesi, tüm domain modelinin üzerine inşa edildiği temel yapı taşlarını içerir. Burada somut iş kuralı yoktur; yalnızca soyut taban sınıflar, arayüzler ve yardımcılar bulunur.
Tüm entity’lerin tabanı. Kimlik, audit izi ve soft-delete alanlarını tek yerde toplar. Setter’lar protected’tır — değer yalnızca aggregate davranışı veya interceptor üzerinden değişir.
public abstract class EntityBase<TId> : EntityBase, IEntityBase<TId>, IAuditableEntity, ISoftDeletable{ public virtual TId Id { get; protected set; } // IAuditableEntity — interceptor doldurur public virtual DateTime? CreatedAt { get; protected set; } public virtual DateTime? UpdatedAt { get; protected set; } public virtual Guid? CreatedBy { get; protected set; } public virtual Guid? UpdatedBy { get; protected set; } // ISoftDeletable — interceptor doldurur public virtual DateTime? DeletedAt { get; protected set; } public virtual Guid? DeletedBy { get; protected set; } public virtual bool IsDeleted() => DeletedAt is not null; public virtual bool IsTransient() => Id.Equals(default(TId)); // ... Equals / GetHashCode / == / != kimlik bazlı}
IsDeleted() — DeletedAt dolu ise kayıt mantıksal olarak silinmiştir. Global query filter (DeletedAt == null) bu kayıtları sorgulardan otomatik dışlar.
IsTransient() — henüz kalıcı kimliği olmayan (DB’ye yazılmamış) nesneyi belirtir. Equals/GetHashCode kimlik üzerinden çalışır; iki transient entity asla eşit kabul edilmez.
EntityBase (generic olmayan taban) domain event kuyruğunu tutar:
public abstract class EntityBase : IEntityBase{ private List<INotification> _domainEvents; public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly(); public void AddDomainEvent(INotification eventItem) { /* listeye ekler */ } public void RemoveDomainEvent(INotification eventItem) { /* ... */ } public void ClearDomainEvents() { /* ... */ }}
Pratikte aggregate ve çocuk entity’lerin neredeyse tamamı Guid kimliklidir, bu yüzden hazır bir kısayol vardır:
public abstract class Entity : EntityBase<Guid>{ public override bool IsTransient() => base.IsTransient() || CreatedAt == default;}
Entity için IsTransient, CreatedAt set edilene kadar nesneyi geçici sayar — böylece henüz persist edilmemiş (event’i daha dispatch edilmemiş) nesneler eşitlik kıyaslamasında doğru davranır.
Kimliği olmayan, değerine göre eşitlenen nesnelerin tabanı. Türetilen sınıf yalnızca GetAtomicValues() ile eşitliğe katkıda bulunan alanları sıralar; Equals, GetHashCode ve operatörler taban tarafından sağlanır.
public abstract class ValueObject{ protected abstract IEnumerable<object> GetAtomicValues(); public override bool Equals(object obj) { if (obj == null || obj.GetType() != GetType()) return false; var other = (ValueObject)obj; // GetAtomicValues() sırayla karşılaştırılır } public override int GetHashCode() => GetAtomicValues().Select(x => x?.GetHashCode() ?? 0).Aggregate((x, y) => x ^ y);}
Örnek bir VO yalnızca atomik değerlerini bildirir:
public sealed class FullName : ValueObject{ public string Value { get; } // ... ctor validasyonu ... protected override IEnumerable<object> GetAtomicValues() { yield return Value; }}
Tüm domain event’lerin tabanı. Her event Version 7 (zaman sıralı) GUID kimliği ve UTC zaman damgası alır. INotification ile birlikte kullanıldığında MediatR pipeline’ına girer.
public abstract class DomainEvent{ public string Type => this.GetType().Name; public readonly Guid Id; public readonly Guid CorrelationID; public readonly DateTime CreatedAt; public DomainEvent() { Id = Guid.CreateVersion7(DateTime.UtcNow); CreatedAt = DateTime.UtcNow; } public DomainEvent(Guid correlationID) : this() => this.CorrelationID = correlationID;}
Somut bir event hem DomainEvent’ten türer hem INotification’ı uygular:
public class UserCreatedDomainEvent : DomainEvent, INotification{ public Guid UserId { get; } public FullName? FullName { get; } // ...}
C# enum’larının yetersiz kaldığı yerlerde kullanılan, davranış ve veri taşıyabilen tip-güvenli sabit küme deseni. Id ve Name taşır; lookup metodları ile değer/isim dönüşümü yapılır.
public abstract class Enumeration : IComparable{ public int Id { get; private set; } public string Name { get; private set; } protected Enumeration(int id, string name) { Id = id; Name = name; } public static IEnumerable<T> GetAll<T>() where T : Enumeration; public static T FromValue<T>(int id) where T : Enumeration; // bulunamazsa DomainException public static T FromName<T>(string name) where T : Enumeration; // bulunamazsa DomainException public static bool InRange<T>(int value) where T : Enumeration; // geçerli mi?}
Örnek tanım:
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)); public Role(int id, string name) : base(id, name) { }}
Aggregate’lerde sık görülen desen: DB’de int StatusId saklanır, domain tarafında ise hesaplanan bir property enumeration’a çevirir — public UserStatus Status => UserStatus.FromValue<UserStatus>(StatusId);
public interface IRepository<T> : IRepositoryBase<T> where T : class, IAggregateRoot{ IUnitOfWork UnitOfWork { get; }}public interface IReadRepository<T> : IReadRepositoryBase<T> where T : class, IEntityBase {}public interface IUnitOfWork : IDisposable{ Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default);}
IRepository<T> yalnızca IAggregateRoot türlerini kabul eder. Çocuk entity’ler için doğrudan repository yoktur — onlara her zaman aggregate root üzerinden erişilir. Bu, tutarlılık sınırını derleme zamanında zorlar.
IRepositoryBase<T> ve IReadRepositoryBase<T>, Ardalis.Specification kütüphanesinden gelir; specification tabanlı sorgulama sağlar (FirstOrDefaultAsync(new UserByEmailSpecification(email), ct) gibi). Yazma akışında SaveChanges doğrudan değil, UnitOfWork.SaveEntitiesAsync üzerinden yapılır — bu metod kaydetme sonrası domain event’leri dispatch eder.