skills/data/audit-trail/SKILL.md
Use when adding automatic audit trail (CreatedAt, UpdatedBy) via EF Core interceptors.
npx skillsauth add faysilalshareef/dotnet-ai-kit audit-trailInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 Domaindata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.