Audit trail with IAuditable interface and EF Core interceptor. Automatic CreatedAt/UpdatedAt/CreatedBy/UpdatedBy population.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
namespace {Company}.{Domain}.Domain.Interfaces;
public interface IAuditable
{
DateTime CreatedAt { get; set; }
DateTime? UpdatedAt { get; set; }
string? CreatedBy { get; set; }
string? UpdatedBy { get; set; }
}
namespace {Company}.{Domain}.Domain.Entities;
public class Order : IAuditable
{
public Guid Id { get; private set; }
public string CustomerName { get; private set; } = default!;
public decimal Total { get; private set; }
// IAuditable
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
}
namespace {Company}.{Domain}.Infrastructure.Interceptors;
public sealed class AuditableEntityInterceptor(
IHttpContextAccessor httpContextAccessor,
TimeProvider timeProvider)
: SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
if (eventData.Context is null) return base.SavingChangesAsync(eventData, result, ct);
var now = timeProvider.GetUtcNow().UtcDateTime;
var userId = httpContextAccessor.HttpContext?.User
.FindFirstValue(ClaimTypes.NameIdentifier);
foreach (var entry in eventData.Context.ChangeTracker
.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.CreatedBy = userId;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = now;
entry.Entity.UpdatedBy = userId;
break;
}
}
return base.SavingChangesAsync(eventData, result, ct);
}
}
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
string? DeletedBy { get; set; }
}
// In interceptor, handle EntityState.Deleted:
case EntityState.Deleted when entry.Entity is ISoftDeletable softDeletable:
entry.State = EntityState.Modified;
softDeletable.IsDeleted = true;
softDeletable.DeletedAt = now;
softDeletable.DeletedBy = userId;
break;
// In DbContext.OnModelCreating
modelBuilder.Entity<Order>().HasQueryFilter(o => !o.IsDeleted);
services.AddDbContext<ApplicationDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<AuditableEntityInterceptor>());
});
services.AddScoped<AuditableEntityInterceptor>();
| Anti-Pattern | Correct Approach |
|---|---|
| Setting audit fields manually in handlers | Use interceptor |
Using DateTime.UtcNow directly | Inject TimeProvider |
| Hard-deleting audited entities | Use soft delete |
Audit fields with public set on domain entities | Acceptable — IAuditable needs setters for interceptor |
grep -r "IAuditable\|CreatedAt\|UpdatedAt" --include="*.cs"
grep -r "SaveChangesInterceptor\|SavingChangesAsync" --include="*.cs"
IAuditable interface in Domain