Capture entity changes and audit identity for .NET developers who supply their own audit-log publisher.
Use a .NET 10 console project. Install the packages, then replace Program.cs with the example.
dotnet new console -f net10.0 -n EfCoreDemo
cd EfCoreDemo
dotnet add package DKNet.EfCore.AuditLogs
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.12
using DKNet.EfCore.Abstractions.Entities;
using DKNet.EfCore.AuditLogs;
using DKNet.EfCore.Hooks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
connection.Open();
var services = new ServiceCollection();
services.AddLogging();
services.AddDbContextWithHook<DemoDb>(o => o.UseSqlite(connection));
services.AddEfCoreAuditLogs<DemoDb, ConsolePublisher>();
services.AddCurrentUserProvider<DemoDb, DemoUser>();
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
await using var db = scope.ServiceProvider.GetRequiredService<DemoDb>();
await db.Database.EnsureCreatedAsync();
db.Add(new Item { Name = "Coffee" });
await db.SaveChangesAsync(); // Created: Item, by user-42
public sealed class DemoDb(DbContextOptions<DemoDb> options) : DbContext(options)
{
public DbSet<Item> Items => Set<Item>();
}
public sealed class Item : IAuditedProperties
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string CreatedBy { get; set; } = "";
public DateTimeOffset CreatedOn { get; set; }
public string? UpdatedBy { get; set; }
public DateTimeOffset? UpdatedOn { get; set; }
}
public sealed class DemoUser : ICurrentUserProvider
{
public string? GetCurrentUser() => "user-42";
}
public sealed class ConsolePublisher : IAuditLogPublisher
{
public Task PublishAsync(IEnumerable<AuditLogEntry> logs, CancellationToken cancellationToken = default)
{
foreach (var log in logs)
Console.WriteLine($"{log.Action}: {log.EntityName}, by {log.UpdatedBy ?? log.CreatedBy}");
return Task.CompletedTask;
}
}
Run with dotnet run.
EfCoreAuditHook and publishers resolve in the context DI scope. The hook keeps pending batches in memory by context instance; the package supplies no durable store.
Capture runs before EF writes, and publishing runs after a successful save. Publisher calls are awaited and exceptions are logged and swallowed. An after-save callback can still run inside a caller-owned transaction; this is not a durable delivery guarantee.