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

# Audit Interceptor

> AuditInterceptor — CreatedAt/By, UpdatedAt/By ve soft-delete'in otomatik yazımı

`AuditInterceptor`, `SaveChanges` öncesinde change tracker'daki her entity'nin audit alanlarını otomatik doldurur. Audit ve soft-delete tek bir interceptor'da birleştirilmiştir — ayrı bir soft-delete interceptor'ı yoktur.

```csharp theme={null}
public sealed class AuditInterceptor : SaveChangesInterceptor
{
    private readonly IAuditUserContext _userContext;

    public AuditInterceptor(IAuditUserContext userContext) => _userContext = userContext;
    // ...
}
```

## Hook noktaları

Hem async hem sync `SaveChanges` yakalanır; ikisi de aynı `ApplyAuditFields` metodunu çağırır:

```csharp theme={null}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
    DbContextEventData eventData, InterceptionResult<int> result,
    CancellationToken cancellationToken = default)
{
    if (eventData.Context is not null)
        ApplyAuditFields(eventData.Context);

    return base.SavingChangesAsync(eventData, result, cancellationToken);
}
```

## ApplyAuditFields

Her `EntityBase` entry'sinin state'ine göre ilgili alanlar set edilir:

```csharp theme={null}
private void ApplyAuditFields(DbContext context)
{
    var now    = DateTime.UtcNow;
    var userId = _userContext.UserId;

    foreach (var entry in context.ChangeTracker.Entries<EntityBase>())
    {
        switch (entry.State)
        {
            case EntityState.Added:
                entry.Property(nameof(IAuditableEntity.CreatedAt)).CurrentValue = now;
                entry.Property(nameof(IAuditableEntity.CreatedBy)).CurrentValue = userId;
                break;

            case EntityState.Modified:
                entry.Property(nameof(IAuditableEntity.UpdatedAt)).CurrentValue = now;
                entry.Property(nameof(IAuditableEntity.UpdatedBy)).CurrentValue = userId;
                break;

            case EntityState.Deleted:
                // Fiziksel silme yerine soft-delete
                entry.State = EntityState.Modified;
                entry.Property(nameof(ISoftDeletable.DeletedAt)).CurrentValue = now;
                entry.Property(nameof(ISoftDeletable.DeletedBy)).CurrentValue = userId;
                break;
        }
    }
}
```

| Entity state | Yapılan                                                                                              |
| ------------ | ---------------------------------------------------------------------------------------------------- |
| `Added`      | `CreatedAt = UtcNow`, `CreatedBy = userId`                                                           |
| `Modified`   | `UpdatedAt = UtcNow`, `UpdatedBy = userId`                                                           |
| `Deleted`    | State `Modified`'a çevrilir; `DeletedAt = UtcNow`, `DeletedBy = userId` (fiziksel DELETE'e dönüşmez) |

<Note>
  `Deleted` → `Modified` dönüşümü soft-delete'in kalbidir: EF Core `DELETE` SQL'i yerine `UPDATE` üretir. Detaylar [Soft Delete](/data/soft-delete) sayfasında.
</Note>

Tüm zaman damgaları `DateTime.UtcNow`'dır. `now` ve `userId` döngü öncesi bir kez okunur, böylece tek `SaveChanges` içindeki tüm satırlar tutarlı değer alır.

## Aktör kimliği — IAuditUserContext

`CreatedBy`/`UpdatedBy`/`DeletedBy`'a yazılan değer `IAuditUserContext.UserId`'den gelir:

```csharp theme={null}
public interface IAuditUserContext
{
    Guid UserId { get; }
}
```

Bilinçli olarak **nullable değil** (`Guid`, `Guid?` değil): HTTP context'i olmayan akışlarda (background job, seeder, design-time) implementasyon `SystemActor.Id` döner. Yani audit kolonları asla boş kalmaz.

Design-time'da kullanılan no-op implementasyon:

```csharp theme={null}
class NoAuditUserContext : IAuditUserContext
{
    public Guid UserId => SystemActor.Id;
}
```

## Neden OnConfiguring'de eklenir?

`AuditInterceptor` **scoped**'tur — her request'te taze bir `IAuditUserContext` alması gerekir (güncel kullanıcı kimliği). Bu yüzden interceptor `AddDbContext`'in options builder'ına **eklenmez**; DbContext constructor'ı üzerinden inject edilip `OnConfiguring`'de eklenir:

```csharp theme={null}
// DependencyInjection.cs
services.AddScoped<AuditInterceptor>();   // scoped — her request'te taze IAuditUserContext

services.AddDbContext<DiyanetCleanArchitectureDbContext>((sp, options) =>
{
    options.UseNpgsql(...).UseSnakeCaseNamingConvention();
    // AuditInterceptor scoped olduğu için buraya EKLENMIYOR.
});
```

```csharp theme={null}
// DiyanetCleanArchitectureDbContext.cs
public DiyanetCleanArchitectureDbContext(
    DbContextOptions<DiyanetCleanArchitectureDbContext> options,
    IMediator mediator, IEventBus eventBus, AuditInterceptor auditInterceptor) : base(options)
{
    _auditInterceptor = auditInterceptor ?? throw new ArgumentNullException(nameof(auditInterceptor));
    // ...
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (_auditInterceptor != null)
        optionsBuilder.AddInterceptors(_auditInterceptor);

    base.OnConfiguring(optionsBuilder);
}
```

<Warning>
  Scoped bir interceptor'ı `AddDbContext` options'a (singleton internal service provider) eklemek EF Core'un **internal service provider cache**'ini bozar ve scoped bağımlılığı doğru çözemez. `OnConfiguring` yolu bu sorunu aşar; DbContext'in iki constructor'ı (parametresiz design-time + DI runtime) bunun için vardır.
</Warning>

## Akış özeti

```mermaid theme={null}
flowchart LR
    Save["SaveChanges()"] --> Hook[AuditInterceptor.SavingChanges]
    Hook --> Loop{ChangeTracker.Entries&lt;EntityBase&gt;}
    Loop -->|Added| C[CreatedAt/By = now / userId]
    Loop -->|Modified| U[UpdatedAt/By = now / userId]
    Loop -->|Deleted| D[State → Modified<br/>DeletedAt/By = now / userId]
    C --> DB[(UPDATE/INSERT)]
    U --> DB
    D --> DB
```

## İlgili

<CardGroup cols={2}>
  <Card title="Soft Delete" href="/data/soft-delete">
    Deleted → Modified dönüşümü ve global query filter.
  </Card>

  <Card title="DbContext & Repository" href="/data/dbcontext">
    SaveEntitiesAsync ve interceptor'ın çağrı sırası.
  </Card>

  <Card title="Migrations" href="/data/migrations">
    Design-time NoAuditUserContext kullanımı.
  </Card>

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