ronnythedev/dotnet-clean-architecture-skills/18-dotnet-audit-trail

Claude Code and agent skill by ronnythedev/dotnet-clean-architecture-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Generates audit trail infrastructure for entities. Implements IAuditable interface, EF Core SaveChanges interceptor, and automatic population of CreatedAt, UpdatedAt, CreatedBy, and UpdatedBy fields.

Install

npx vskill@latest install ronnythedev/dotnet-clean-architecture-skills/18-dotnet-audit-trail

SKILL.md

Audit Trail Generator

Overview

Automatic audit trail tracking for entities:

  • IAuditable interface - Standard audit fields
  • SaveChanges interceptor - Automatic field population
  • User context integration - Track who made changes
  • Soft delete support - Track deletions without removing

Quick Reference

ComponentPurpose
IAuditableInterface for auditable entities
AuditableEntityBase class with audit fields
AuditSaveChangesInterceptorAuto-populates audit fields
SoftDeletableInterface for soft delete

Audit Structure

/Domain/Abstractions/
├── IAuditable.cs
├── ISoftDeletable.cs
└── AuditableEntity.cs
/Infrastructure/
├── Interceptors/
│ └── AuditSaveChangesInterceptor.cs
└── ApplicationDbContext.cs

Template: Audit Interfaces

csharp
// src/{name}.domain/Abstractions/IAuditable.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Interface for entities that track creation and modification metadata
/// </summary>
public interface IAuditable
{
/// <summary>
/// UTC timestamp when the entity was created
/// </summary>
DateTime CreatedAtUtc { get; }
/// <summary>
/// ID of the user who created the entity
/// </summary>
Guid? CreatedBy { get; }
/// <summary>
/// UTC timestamp when the entity was last modified
/// </summary>
DateTime? UpdatedAtUtc { get; }
/// <summary>
/// ID of the user who last modified the entity
/// </summary>
Guid? UpdatedBy { get; }
}
csharp
// src/{name}.domain/Abstractions/ISoftDeletable.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Interface for entities that support soft delete
/// </summary>
public interface ISoftDeletable
{
/// <summary>
/// Whether the entity has been soft deleted
/// </summary>
bool IsDeleted { get; }
/// <summary>
/// UTC timestamp when the entity was deleted
/// </summary>
DateTime? DeletedAtUtc { get; }
/// <summary>
/// ID of the user who deleted the entity
/// </summary>
Guid? DeletedBy { get; }
}

Template: Auditable Entity Base Class

csharp
// src/{name}.domain/Abstractions/AuditableEntity.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Base class for entities that track audit information
/// </summary>
public abstract class AuditableEntity : Entity, IAuditable, ISoftDeletable
{
// ═══════════════════════════════════════════════════════════════
// AUDIT FIELDS (IAuditable)
// ═══════════════════════════════════════════════════════════════
public DateTime CreatedAtUtc { get; private set; }
public Guid? CreatedBy { get; private set; }
public DateTime? UpdatedAtUtc { get; private set; }
public Guid? UpdatedBy { get; private set; }
// ═══════════════════════════════════════════════════════════════
// SOFT DELETE FIELDS (ISoftDeletable)
// ═══════════════════════════════════════════════════════════════
public bool IsDeleted { get; private set; }
public DateTime? DeletedAtUtc { get; private set; }
public Guid? DeletedBy { get; private set; }
protected AuditableEntity() : base()
{
}
protected AuditableEntity(Guid id) : base(id)
{
}
// ═══════════════════════════════════════════════════════════════
// AUDIT METHODS (called by interceptor or manually)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Sets creation audit fields. Called automatically by interceptor.
/// </summary>
internal void SetCreatedAudit(DateTime utcNow, Guid? userId)
{
CreatedAtUtc = utcNow;
CreatedBy = userId;
}
/// <summary>
/// Sets modification audit fields. Called automatically by interceptor.
/// </summary>
internal void SetModifiedAudit(DateTime utcNow, Guid? userId)
{
UpdatedAtUtc = utcNow;
UpdatedBy = userId;
}
/// <summary>
/// Soft deletes the entity
/// </summary>
public virtual void SoftDelete(DateTime utcNow, Guid? userId)
{
if (IsDeleted)
{
return;
}
IsDeleted = true;
DeletedAtUtc = utcNow;
DeletedBy = userId;
}
/// <summary>
/// Restores a soft-deleted entity
/// </summary>
public virtual void Restore()
{
IsDeleted = false;
DeletedAtUtc = null;
DeletedBy = null;
}
}

Template: SaveChanges Interceptor

csharp
// src/{name}.infrastructure/Interceptors/AuditSaveChangesInterceptor.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using {name}.application.abstractions.authentication;
using {name}.domain.abstractions;
namespace {name}.infrastructure.interceptors;
/// <summary>
/// Interceptor that automatically populates audit fields on SaveChanges
/// </summary>
public sealed class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
private readonly IUserContext _userContext;
public AuditSaveChangesInterceptor(IUserContext userContext)
{
_userContext = userContext;
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is not null)
{
UpdateAuditFields(eventData.Context);
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
if (eventData.Context is not null)
{
UpdateAuditFields(eventData.Context);
}
return base.SavingChanges(eventData, result);
}
private void UpdateAuditFields(DbContext context)
{
var utcNow = DateTime.UtcNow;
var userId = GetCurrentUserId();
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
SetCreatedAudit(entry, utcNow, userId);
break;
case EntityState.Modified:
SetModifiedAudit(entry, utcNow, userId);
break;
}
}
// Handle soft delete
foreach (var entry in context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
// Convert hard delete to soft delete
entry.State = EntityState.Modified;
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SoftDelete(utcNow, userId);
}
}
}
}
private void SetCreatedAudit(EntityEntry<IAuditable> entry, DateTime utcNow, Guid? userId)
{
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SetCreatedAudit(utcNow, userId);
}
else
{
// For entities implementing IAuditable but not inheriting AuditableEntity
entry.Property(nameof(IAuditable.CreatedAtUtc)).CurrentValue = utcNow;
entry.Property(nameof(IAuditable.CreatedBy)).CurrentValue = userId;
}
}
private void SetModifiedAudit(EntityEntry<IAuditable> entry, DateTime utcNow, Guid? userId)
{
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SetModifiedAudit(utcNow, userId);
}
else
{
entry.Property(nameof(IAuditable.UpdatedAtUtc)).CurrentValue = utcNow;
entry.Property(nameof(IAuditable.UpdatedBy)).CurrentValue = userId;
}
}
private Guid? GetCurrentUserId()
{
try
{
return _userContext.IsAuthenticated ? _userContext.UserId : null;
}
catch
{
// User context may not be available in background jobs
return null;
}
}
}

Template: EF Core Configuration for Audit Fields

csharp
// src/{name}.infrastructure/Configurations/AuditableEntityConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.abstractions;
namespace {name}.infrastructure.configurations;
/// <summary>
/// Base configuration for auditable entities
/// Apply using: builder.ApplyConfiguration(new AuditableEntityConfiguration<YourEntity>());
/// </summary>
public class AuditableEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
where TEntity : AuditableEntity
{
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
// ═══════════════════════════════════════════════════════════════
// AUDIT FIELDS
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.CreatedAtUtc)
.HasColumnName("created_at_utc")
.IsRequired();
builder.Property(e => e.CreatedBy)
.HasColumnName("created_by");
builder.Property(e => e.UpdatedAtUtc)
.HasColumnName("updated_at_utc");
builder.Property(e => e.UpdatedBy)
.HasColumnName("updated_by");
// ═══════════════════════════════════════════════════════════════
// SOFT DELETE FIELDS
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.IsDeleted)
.HasColumnName("is_deleted")
.HasDefaultValue(false)
.IsRequired();
builder.Property(e => e.DeletedAtUtc)
.HasColumnName("deleted_at_utc");
builder.Property(e => e.DeletedBy)
.HasColumnName("deleted_by");
// ═══════════════════════════════════════════════════════════════
// GLOBAL QUERY FILTER (Soft Delete)
// ═══════════════════════════════════════════════════════════════
builder.HasQueryFilter(e => !e.IsDeleted);
// ═══════════════════════════════════════════════════════════════
// INDEXES
// ═══════════════════════════════════════════════════════════════
builder.HasIndex(e => e.CreatedAtUtc)
.HasDatabaseName($"ix_{typeof(TEntity).Name.ToLower()}_created_at");
builder.HasIndex(e => e.IsDeleted)
.HasDatabaseName($"ix_{typeof(TEntity).Name.ToLower()}_is_deleted")
.HasFilter("is_deleted = true");
}
}

Template: Using Auditable Entity

csharp
// src/{name}.domain/Users/User.cs
using {name}.domain.abstractions;
using {name}.domain.users.events;
namespace {name}.domain.users;
public sealed class User : AuditableEntity
{
public string Name { get; private set; } = string.Empty;
public Email Email { get; private set; } = null!;
public Guid OrganizationId { get; private set; }
public bool IsActive { get; private set; }
private User() : base()
{
}
private User(
Guid id,
string name,
Email email,
Guid organizationId)
: base(id)
{
Name = name;
Email = email;
OrganizationId = organizationId;
IsActive = true;
}
public static Result<User> Create(
string name,
string email,
Guid organizationId)
{
var emailResult = Email.Create(email);
if (emailResult.IsFailure)
{
return Result.Failure<User>(emailResult.Error);
}
var user = new User(
Guid.NewGuid(),
name,
emailResult.Value,
organizationId);

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
64
Repository forks
11
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/ronnythedev/dotnet-clean-architecture-skills/18-dotnet-audit-trail/badge)](https://verified-skill.com/skills/ronnythedev/dotnet-clean-architecture-skills/18-dotnet-audit-trail)

Something wrong with this skill or its page? Tell us.