Audit trail
Use when adding automatic audit trail (CreatedAt, UpdatedBy) via EF Core interceptors.From its SKILL.md
npx -y skills add FaysilAlshareef/dotnet-ai-kit --skill audit-trailAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
3.9 KB, as published. Nobody here has run it
Audit Trail
IAuditable Interface
namespace {Company}.{Domain}.Domain.Interfaces;
public interface IAuditable
{
DateTime CreatedAt { get; set; }
DateTime? UpdatedAt { get; set; }
string? CreatedBy { get; set; }
string? UpdatedBy { get; set; }
}
Entity Implementation
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; }
}
SaveChanges Interceptor
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);
}
}
Soft Delete
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;
Global Query Filter for Soft Delete
// In DbContext.OnModelCreating
modelBuilder.Entity<Order>().HasQueryFilter(o => !o.IsDeleted);
Registration
services.AddDbContext<ApplicationDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<AuditableEntityInterceptor>());
});
services.AddScoped<AuditableEntityInterceptor>();
Anti-Patterns
| 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 |
Detect Existing Patterns
grep -r "IAuditable\|CreatedAt\|UpdatedAt" --include="*.cs"
grep -r "SaveChangesInterceptor\|SavingChangesAsync" --include="*.cs"
Adding to Existing Project
- Create
IAuditableinterface in Domain - Implement on entities that need auditing
- Create interceptor in Infrastructure
- Register interceptor in DbContext setup