> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Soft Delete

> ISoftDeletable, global query filter, restore ve partial unique index

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:

```csharp theme={null}
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**:

```csharp theme={null}
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](/data/audit-interceptor) sayfasında.

## Global query filter

`OnModelCreating`, `ISoftDeletable` uygulayan **her** entity tipine `DeletedAt == null` filtresini reflection ile otomatik ekler:

```csharp theme={null}
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.

<Note>
  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.
</Note>

## 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:

```csharp theme={null}
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:

```csharp theme={null}
/// <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:

```csharp theme={null}
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.

<Tip>
  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'ı.
</Tip>

## İlgili

<CardGroup cols={2}>
  <Card title="Audit Interceptor" href="/data/audit-interceptor">
    Deleted → Modified dönüşümünün kaynağı.
  </Card>

  <Card title="DbContext & Repository" href="/data/dbcontext">
    Global query filter'ın OnModelCreating'de kurulması.
  </Card>

  <Card title="Migrations" href="/data/migrations">
    Partial unique index'in migration'larda üretimi.
  </Card>

  <Card title="Data Genel Bakış" href="/data/overview">
    Katman rolü ve şemalar.
  </Card>
</CardGroup>
