EFEF Core Handbook

UZMAN

Audit Trail Pattern (Tam Implementasyon)

Kim, ne zaman, hangi alanı, eski değerden yeni değere değiştirdi? KVKK, SOX, HIPAA uyumluluğu ve debug için kritik. SaveChanges override veya Interceptor ile her değişiklik otomatik loglanır — entity kodlarına dokunmadan.

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

1. Base Entity & Interface

public interface IAuditable
{
    DateTime CreatedAt { get; set; }
    string CreatedBy { get; set; }
    DateTime? UpdatedAt { get; set; }
    string? UpdatedBy { get; set; }
}

public abstract class AuditableEntity : IAuditable
{
    public DateTime CreatedAt { get; set; }
    public string CreatedBy { get; set; } = null!;
    public DateTime? UpdatedAt { get; set; }
    public string? UpdatedBy { get; set; }
}

// Entity'ler bundan türer
public class Product : AuditableEntity
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public decimal Price { get; set; }
}

2. Current User Servisi

public interface ICurrentUserService
{
    string? UserId { get; }
    string? UserName { get; }
}

// ASP.NET Core implementasyonu
public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _accessor;
    public CurrentUserService(IHttpContextAccessor accessor) => _accessor = accessor;

    public string? UserId => _accessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    public string? UserName => _accessor.HttpContext?.User?.Identity?.Name;
}

3. SaveChanges Override (Basit Audit)

public class AppDbContext : DbContext
{
    private readonly ICurrentUserService _currentUser;

    public AppDbContext(DbContextOptions<AppDbContext> options, ICurrentUserService currentUser)
        : base(options) => _currentUser = currentUser;

    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        var now = DateTime.UtcNow;
        var user = _currentUser.UserName ?? "System";

        foreach (var entry in ChangeTracker.Entries<IAuditable>())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    entry.Entity.CreatedAt = now;
                    entry.Entity.CreatedBy = user;
                    break;

                case EntityState.Modified:
                    entry.Entity.UpdatedAt = now;
                    entry.Entity.UpdatedBy = user;
                    // CreatedAt/By değiştirilemesin
                    entry.Property(e => e.CreatedAt).IsModified = false;
                    entry.Property(e => e.CreatedBy).IsModified = false;
                    break;
            }
        }

        return await base.SaveChangesAsync(ct);
    }
}

4. Detaylı Audit Log (Değişiklik Geçmişi)

// Audit log entity — her değişiklik bir kayıt
public class AuditLog
{
    public long Id { get; set; }
    public string EntityName { get; set; } = null!;
    public string EntityId { get; set; } = null!;
    public string Action { get; set; } = null!;       // Insert, Update, Delete
    public string? Changes { get; set; }               // JSON: eski→yeni değerler
    public string UserId { get; set; } = null!;
    public DateTime Timestamp { get; set; }
}

// SaveChanges'ta audit log oluşturma
private List<AuditLog> CreateAuditLogs()
{
    var logs = new List<AuditLog>();
    var user = _currentUser.UserId ?? "System";

    foreach (var entry in ChangeTracker.Entries()
        .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
    {
        var log = new AuditLog
        {
            EntityName = entry.Entity.GetType().Name,
            EntityId = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey())
                            ?.CurrentValue?.ToString() ?? "",
            Action = entry.State.ToString(),
            UserId = user,
            Timestamp = DateTime.UtcNow
        };

        if (entry.State == EntityState.Modified)
        {
            var changes = new Dictionary<string, object?>();
            foreach (var prop in entry.Properties.Where(p => p.IsModified))
            {
                changes[prop.Metadata.Name] = new
                {
                    Old = prop.OriginalValue,
                    New = prop.CurrentValue
                };
            }
            log.Changes = JsonSerializer.Serialize(changes);
        }

        logs.Add(log);
    }

    return logs;
}

5. EF Configuration

public class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
{
    public void Configure(EntityTypeBuilder<AuditLog> builder)
    {
        builder.ToTable("AuditLogs");
        builder.HasKey(a => a.Id);
        builder.Property(a => a.EntityName).HasMaxLength(100).IsRequired();
        builder.Property(a => a.EntityId).HasMaxLength(50).IsRequired();
        builder.Property(a => a.Action).HasMaxLength(10).IsRequired();
        builder.Property(a => a.Changes).HasColumnType("nvarchar(max)");
        builder.Property(a => a.UserId).HasMaxLength(100).IsRequired();

        builder.HasIndex(a => new { a.EntityName, a.EntityId });
        builder.HasIndex(a => a.Timestamp);
        builder.HasIndex(a => a.UserId);
    }
}
CREATE TABLE [AuditLogs] (
    [Id]         BIGINT IDENTITY(1,1) NOT NULL,
    [EntityName] NVARCHAR(100) NOT NULL,
    [EntityId]   NVARCHAR(50)  NOT NULL,
    [Action]     NVARCHAR(10)  NOT NULL,
    [Changes]    NVARCHAR(MAX) NULL,
    [UserId]     NVARCHAR(100) NOT NULL,
    [Timestamp]  DATETIME2     NOT NULL,
    CONSTRAINT [PK_AuditLogs] PRIMARY KEY ([Id])
);

CREATE INDEX [IX_AuditLogs_Entity] ON [AuditLogs] ([EntityName], [EntityId]);
CREATE INDEX [IX_AuditLogs_Timestamp] ON [AuditLogs] ([Timestamp]);
CREATE INDEX [IX_AuditLogs_UserId] ON [AuditLogs] ([UserId]);
CREATE TABLE audit_logs (
    id          BIGINT GENERATED ALWAYS AS IDENTITY,
    entity_name VARCHAR(100) NOT NULL,
    entity_id   VARCHAR(50)  NOT NULL,
    action      VARCHAR(10)  NOT NULL,
    changes     JSONB        NULL,          -- JSONB: indexlenebilir, sorgulanabilir!
    user_id     VARCHAR(100) NOT NULL,
    timestamp   TIMESTAMPTZ  NOT NULL,
    CONSTRAINT pk_audit_logs PRIMARY KEY (id)
);

CREATE INDEX ix_audit_logs_entity ON audit_logs (entity_name, entity_id);
CREATE INDEX ix_audit_logs_timestamp ON audit_logs (timestamp);
CREATE INDEX ix_audit_logs_user_id ON audit_logs (user_id);

-- Bonus: JSONB changes üzerine GIN index (hangi alanlar değişti sorguları için)
CREATE INDEX ix_audit_logs_changes ON audit_logs USING gin (changes);

Örnek AuditLog verisi:

Id EntityName EntityId Action Changes UserId Timestamp
1 Product 42 Modified {"Price":{"Old":999,"New":1299}} ahmet@firma.com 2025-03-15 14:30:00
2 Product 43 Added null elif@firma.com 2025-03-15 15:00:00
3 Order 101 Deleted null admin@firma.com 2025-03-16 09:00:00