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}} |
[email protected] | 2025-03-15 14:30:00 |
| 2 | Product | 43 | Added | null | [email protected] | 2025-03-15 15:00:00 |
| 3 | Order | 101 | Deleted | null | [email protected] | 2025-03-16 09:00:00 |