Skip to main content
Sistemde fiziksel silme yoktur (DELETE SQL’i üretilmez). ISoftDeletable taşıyan her aggregate, silindiğinde DeletedAt/DeletedBy damgalanır ve sorgulardan otomatik gizlenir.

ISoftDeletable

EntityBase bu arayüzü taşır; alanlar ve IsDeleted() kontrolü buradan gelir:
public interface ISoftDeletable
{
    DateTime? DeletedAt { get; }
    Guid?     DeletedBy { get; }
    bool IsDeleted();          // DeletedAt.HasValue
}
Bir kayıt “silinmiş” sayılır ancak DeletedAt != null ise. DeletedAt == null olan kayıtlar aktiftir.

Silme nasıl gerçekleşir

Repository Remove/DeleteAsync çağrısı entity’yi EntityState.Deleted’a alır; AuditInterceptor bunu yakalayıp soft-delete’e çevirir:
case EntityState.Deleted:
    entry.State = EntityState.Modified;              // DELETE → UPDATE
    entry.Property(nameof(ISoftDeletable.DeletedAt)).CurrentValue = now;
    entry.Property(nameof(ISoftDeletable.DeletedBy)).CurrentValue = userId;
    break;
Sonuç: DB’de DELETE yerine UPDATE ... SET deleted_at = now, deleted_by = userId çalışır. Detaylar Audit Interceptor sayfasında.

Global query filter

OnModelCreating, ISoftDeletable uygulayan her entity tipine DeletedAt == null filtresini reflection ile otomatik ekler:
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    if (!typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType))
        continue;

    var param  = Expression.Parameter(entityType.ClrType, "e");
    var prop   = Expression.Property(param, nameof(ISoftDeletable.DeletedAt));
    var body   = Expression.Equal(prop, Expression.Constant(null, typeof(DateTime?)));
    var lambda = Expression.Lambda(body, param);

    modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}
Böylece her sorgu (ve include edilen navigation) otomatik olarak WHERE deleted_at IS NULL koşulunu taşır. Ardalis.Specification EF Core’un altındaki IQueryable’ı kullandığı için spec tabanlı sorgularda da filtre aktif kalır.
Pratik sonuç: provisioning, lookup ve uniqueness check akışları soft-deleted kayıtları görmez. Örneğin iptal edilmiş bir üyelik yeniden login olduğunda eski (silinmiş) kayıt görünmediği için yeni bir Citizen oluşturulur.

Silinmişleri görme — IgnoreQueryFilters

Audit raporlama gibi senaryolarda soft-deleted kayıtları dahil etmek için sorgu açıkça filtreyi devre dışı bırakır:
var allIncludingDeleted = await db.Faqs
    .IgnoreQueryFilters()
    .ToListAsync(ct);

Restore — geri alma

Soft-delete’i geri almak DeletedAt/DeletedBy’ı null yapmaktır. RolePermission bunu domain metoduyla yapar:
/// <summary>
/// Soft-delete edilmiş kaydı geri alır.
/// Aynı (RoleId, PermissionId) çifti yeniden atanmak istendiğinde
/// yeni INSERT yerine bu method çağrılır (duplicate PK'den kaçınmak için).
/// </summary>
public void Restore()
{
    DeletedAt = null;
    DeletedBy = null;
}
Restore edilecek (silinmiş) kaydı bulmak için önce IgnoreQueryFilters() ile sorgulanır, sonra Restore() çağrılır.

Unique index sorunu ve partial index çözümü

Soft-delete, naif unique constraint’leri bozar: aynı (role_id, permission_id) çifti bir kez silinip yeniden eklenmek istendiğinde, silinmiş satır hâlâ tabloda durduğu için unique ihlali oluşur. Çözüm, partial unique index’tir — yalnızca aktif (silinmemiş) satırlar için benzersizlik zorlanır:
builder.HasIndex(rp => new { rp.RoleId, rp.PermissionId })
    .IsUnique()
    .HasFilter("deleted_at IS NULL")
    .HasDatabaseName("ix_role_permission_role_id_permission_id_active");
PostgreSQL’de bu CREATE UNIQUE INDEX ... WHERE deleted_at IS NULL üretir. Böylece:
  • Aktif kayıtlar arasında çift benzersizdir.
  • Soft-deleted kayıtlar index dışında kalır → aynı çift yeniden eklenebilir veya Restore() ile geri alınabilir.
Soft-delete’i olan bir tabloda unique kısıt tanımlarken HasFilter("deleted_at IS NULL") eklemeyi unutmayın; yoksa “sil-ve-yeniden-ekle” senaryosu duplicate hatası verir. RolePermission’ın Guid PK’ye geçmesinin (eski composite PK yerine) nedeni de tam olarak budur — bkz RolePermissionsRevamp migration’ı.

İlgili

Audit Interceptor

Deleted → Modified dönüşümünün kaynağı.

DbContext & Repository

Global query filter’ın OnModelCreating’de kurulması.

Migrations

Partial unique index’in migration’larda üretimi.

Data Genel Bakış

Katman rolü ve şemalar.