Skip to main content

Auditing with Audit.NET

dotnet add package NodaTime
dotnet add package Audit.NET
dotnet add package Audit.EntityFramework.Core
dotnet add package EFCore.NamingConventions
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime
AuditLog.cs
public class AuditLog {  
public Guid Id { get; set; }
public Instant AuditTime { get; set; }
public EventEntry? AuditData { get; set; }
public string? EntityType { get; set; }
public string? TablePk { get; set; }
public string? AuditAction { get; set; }
public string? AuditUser { get; set; }
}
AuditLogEntityConfiguration.cs
public class AuditLogEntityConfiguration : EntityMappingConfiguration<AuditLog>
{
public override void Map(EntityTypeBuilder<AuditLog> builder)
{
builder
.HasKey(p => p.Id);

builder
.Property(p => p.AuditData)
.HasColumnType("jsonb");
}
}
AuditNetExtensions.cs
public static IHost UseAuditNet(this IHost host)  
{
Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated,
scope =>
{
try
{
var contextAccessor = host.Services.GetRequiredService<IHttpContextAccessor>();
var claim = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier);
scope.Event.Environment.UserName = claim?.Value;
} catch
{
// ignored
}
});
return host;
}

public static IServiceCollection AddAuditNet(this IServiceCollection services, params Type[] types)
{
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.AuditTypeMapper(t => typeof(AuditLog))
.AuditEntityAction<AuditLog>((ev, entry, entity) =>
{
entity.AuditData = entry;
entity.EntityType = entry.EntityType.Name;
entity.AuditTime = Instant.FromDateTimeUtc(ev.EndDate!.Value.ToUniversalTime());
entity.AuditUser = ev.Environment.UserName;
entity.AuditAction = entry.Action;
entity.TablePk = entry.PrimaryKey.First().Value.ToString()!;
}) .IgnoreMatchedProperties(true)
);

// the following configuration avoid death loops if an exception is thrown during the audit process
Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
{
if (scope.Event.Environment.Exception != null)
scope.Discard();
});

Audit.EntityFramework.Configuration.Setup()
.ForContext<AppDbContext>(config => config
.IncludeEntityObjects(false)
.AuditEventType("{context}:{database}"))
.UseOptIn()
.IncludeAny(p => types.ToList().Contains(p));

return services;
}

Configure as Startup

Program.cs
builder.Services  
.AddConfiguredDbContext<AppDbContext>(connectionString)
.AddAuditNet(
typeof(User),
typeof(Organisation),
typeof(Subscription),
typeof(Identity),
typeof(UserInvite),
typeof(Invite)
);

// ...

app.UseAuditNet();