Skip to main content
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.
SharedKernel/
├── SeedWork/      → EntityBase, Entity, ValueObject, Enumeration, DomainEvent,
│                    IAggregateRoot, IRepository, IUnitOfWork, IAuditableEntity...
├── ValueObjects/  → FullName, Email, Phone, OtpCode, TCKimlikNo, TotpSecret...
├── Enumerations/  → OtpType, AuthProviderType, City...
└── Utils/         → Check, GuidFactory, MobilePhoneUtility...

EntityBase<TId>

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() { /* ... */ }
}

Entity : EntityBase<Guid>

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.

IAggregateRoot

Boş bir marker arayüzüdür. Tutarlılık sınırının kökünü işaretler; IRepository<T> yalnızca IAggregateRoot türlerini kabul eder.
public interface IAggregateRoot {}

ValueObject

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

DomainEvent

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; }
    // ...
}

Enumeration

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

GuidFactory

Kimlik üretimini tek noktada toplar. Hep Version 7 (zaman sıralı) GUID üretir — bu, B-tree index’lerde daha iyi yerellik (locality) sağlar.
public static class GuidFactory
{
    public static Guid New() => Guid.CreateVersion7(DateTime.UtcNow);
}
Aggregate ctor’larında doğrudan kullanılır: this.Id = GuidFactory.New();

DomainException

Domain kuralı ihlallerinde fırlatılan istisna tipi. API katmanında ProblemDetails ile HTTP 400’e map edilir.
public class DomainException : Exception
{
    public DomainException() { }
    public DomainException(string message) : base(message) { }
    public DomainException(string message, Exception innerException) : base(message, innerException) { }
}

Check — guard yardımcısı

Argüman doğrulamasını okunabilir tek satırlara indirir; ihlalde DomainException (gerekirse iç ArgumentException ile) fırlatır.
Check.NotNull(plan, nameof(plan));
Check.NotNullOrEmpty(providerUserId, nameof(providerUserId));
Check.HasValue(id, nameof(id));          // default(T) değilse geçer
Check.NotEmpty(collection, nameof(collection));
Check.Positive(adet, nameof(adet));
Çocuk entity ctor’unda tipik kullanım:
public UserOtpChallenge(User user, Guid id, OtpCode code, OtpType type, DateTime expiryDate)
{
    Check.NotNull(user, nameof(user));
    Check.HasValue(id, nameof(id));
    Check.NotNull(code, nameof(code));
    // ...
}

Audit, soft-delete ve tenant arayüzleri

Bu arayüzler EntityBase<TId> tarafından otomatik uygulanır; değerleri EF Core’daki tek AuditInterceptor doldurur (detay Veri Katmanı).
ArayüzÜyelerAmaç
IAuditableEntityCreatedAt/UpdatedAt, CreatedBy/UpdatedByYazma izi (kim, ne zaman).
ISoftDeletableDeletedAt, DeletedBy, IsDeleted()Fiziksel silme yerine mantıksal silme.
IMayHaveTenantGuid? OrganizationIdOpsiyonel tenant aidiyeti.
ITenantGuid OrganizationIdZorunlu tenant aidiyeti.
public interface ISoftDeletable
{
    DateTime? DeletedAt { get; }
    Guid?     DeletedBy { get; }
    bool IsDeleted();
}

Repository ve UnitOfWork arayüzleri

Domain, persistence’tan habersizdir; yalnızca arayüzleri tanımlar. Implementasyonlar Infrastructure katmanındadır (EFRepository, CachedRepository).
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.

Sonraki adımlar

Aggregate'ler

Bu yapı taşlarının User aggregate’inde nasıl bir araya geldiği.

Value Object'ler

ValueObject üzerine kurulu hazır VO kataloğu.

Domain Event'ler

DomainEvent + INotification’ın dispatch akışı.

Veri Katmanı

AuditInterceptor, soft-delete filter ve SaveEntitiesAsync detayları.