EfCore.AuditLog
1.0.0
dotnet add package EfCore.AuditLog --version 1.0.0
NuGet\Install-Package EfCore.AuditLog -Version 1.0.0
<PackageReference Include="EfCore.AuditLog" Version="1.0.0" />
<PackageVersion Include="EfCore.AuditLog" Version="1.0.0" />
<PackageReference Include="EfCore.AuditLog" />
paket add EfCore.AuditLog --version 1.0.0
#r "nuget: EfCore.AuditLog, 1.0.0"
#:package EfCore.AuditLog@1.0.0
#addin nuget:?package=EfCore.AuditLog&version=1.0.0
#tool nuget:?package=EfCore.AuditLog&version=1.0.0
EfCore.AuditLog – Channel / Queue Sink Support
This library provides an EF Core SaveChanges interceptor that captures audit entries and writes them to a separate audit database. Two write modes are supported:
- Direct (default): each SaveAsync call writes entries directly to the audit DB via IDbContextFactory<AuditDbContext>.
- Channel (recommended for throughput): entries are enqueued into a bounded Channel and a background worker batches and persists them.
Important: the library is provider-agnostic and does not depend on any specific database provider (Postgres, SQL Server, etc.). Consumers register the audit database provider when calling AddEfCoreAudit by passing an Action<DbContextOptionsBuilder> – this keeps the library independent of EF Core providers.
Why this is safe
- EfCore.AuditLog.csproj contains only EF Core and Microsoft.Extensions packages (no Npgsql or SqlServer provider packages). That means the library itself does not force any DB provider.
- AddEfCoreAudit requires the caller to supply an Action<DbContextOptionsBuilder> (configureAuditDb). The consumer chooses the provider inside that action (UseNpgsql, UseSqlServer, UseSqlite, etc.). This keeps the library independent of any specific provider.
How consumers pick a provider (examples)
- PostgreSQL:
services.AddEfCoreAudit(o => o.UseNpgsql(Configuration.GetConnectionString("AuditDb")), auditOptions);
- SQL Server:
services.AddEfCoreAudit(o => o.UseSqlServer(Configuration.GetConnectionString("AuditDb")), auditOptions);
- SQLite:
services.AddEfCoreAudit(o => o.UseSqlite(Configuration.GetConnectionString("AuditDb")), auditOptions);
You can also use the IConfiguration overload added in this library to bind AuditOptions from configuration and still supply the provider in the configure action:
// binds AuditOptions from Configuration.GetSection("Audit") and forwards to the main registration
services.AddEfCoreAudit(Configuration, o => {
// pick provider here
// o.UseNpgsql(...);
// o.UseSqlServer(...);
});
Configuration
Add the library to your project and register it in DI using one of the AddEfCoreAudit overloads.
- Direct mode (default)
In Program.cs / Startup.cs (consumer chooses provider):
// PostgreSQL example
services.AddEfCoreAudit(o => {
o.UseNpgsql(Configuration.GetConnectionString("AuditDb"));
});
// SQL Server example
services.AddEfCoreAudit(o => {
o.UseSqlServer(Configuration.GetConnectionString("AuditDb"));
});
// Provider-agnostic placeholder
services.AddEfCoreAudit(o => {
// o.UseYourProvider(Configuration.GetConnectionString("AuditDb"));
});
- Channel mode
Create an AuditOptions instance or configure via overload:
var auditOptions = new AuditOptions {
Mode = AuditOptions.AuditModeType.Channel,
BatchSize = 200,
FlushInterval = TimeSpan.FromSeconds(2),
MaxQueueLength = 20000,
DropWhenFull = false
};
// Bind provider as above
services.AddEfCoreAudit(o => {
// o.UseNpgsql(Configuration.GetConnectionString("AuditDb"));
// o.UseSqlServer(...);
}, auditOptions);
How it works
- In Channel mode the library registers a Channel<AuditEntry> and a ChannelAuditSink which enqueues audit entries.
- A hosted AuditChannelWorker reads the channel, batches entries according to BatchSize and FlushInterval, and writes them with IDbContextFactory<AuditDbContext>.
- If DropWhenFull is false the channel is configured with FullMode=Wait (producers block until space). If true, FullMode=DropOldest and overflowed entries are dropped.
Integrating into XAF applications (Blazor / Web API)
Below are concrete samples showing how to wire the audit infrastructure into XAF Blazor (Server) and XAF Web API projects. The important points:
- call services.AddEfCoreAudit(...) in ConfigureServices and pass a provider-specific configuration action
- attach AuditSaveChangesInterceptor to your application DbContext registration via options.AddInterceptors(...)
- optionally register an ambient IOperationContext to provide Actor and CorrelationId
Installation
Install the NuGet package:
# .NET CLI
dotnet add package EfCore.AuditLog
# Package Manager Console
Install-Package EfCore.AuditLog
# PackageReference (add to .csproj)
<PackageReference Include="EfCore.AuditLog" Version="1.0.0" />
You'll also need to install your preferred database provider:
# For PostgreSQL
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
# For SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
# For SQLite
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
Configuration
appsettings.json example
{
"ConnectionStrings": {
"ConnectionString": "Host=localhost;Database=appdb;Username=app;Password=pass",
"ConnectionString_audit": "Host=localhost;Database=auditdb;Username=audit;Password=pass"
},
"Audit": {
"Mode": "Channel",
"BatchSize": 200,
"FlushInterval": "00:00:02",
"MaxQueueLength": 20000,
"DropWhenFull": false
}
}
You can read the Audit section and map it to AuditOptions when registering AddEfCoreAudit (examples below).
XAF – Blazor (Server) integration (Startup.cs)
This matches the typical XAF Blazor Server Startup pattern used in this solution.
public void ConfigureServices(IServiceCollection services)
{
// optional: ambient operation context (no HttpContext required)
services.AddSingleton<IOperationContext, OperationContext>();
// load options from configuration (example)
var auditOptions = Configuration.GetSection("Audit").Get<AuditOptions>();
// register audit DB and sink (consumer chooses provider here)
services.AddEfCoreAudit(o => {
var auditConn = Configuration.GetConnectionString("ConnectionString_audit") ?? Configuration.GetConnectionString("ConnectionString");
// Example provider registrations (pick one):
// o.UseNpgsql(auditConn);
// o.UseSqlServer(auditConn);
// o.UseSqlite(auditConn);
}, auditOptions);
services.AddXaf(Configuration, builder => {
builder.Modules
// ... add modules
builder.ObjectSpaceProviders
.AddSecuredEFCore(options => { options.PreFetchReferenceProperties(); })
.WithDbContext<MyAppDbContext>((serviceProvider, options) => {
options.UseConnectionString(Configuration.GetConnectionString("ConnectionString"));
// attach interceptor (registered by AddEfCoreAudit)
var interceptor = serviceProvider.GetService<AuditSaveChangesInterceptor>();
if (interceptor != null)
{
options.AddInterceptors(interceptor);
}
})
.AddNonPersistent();
});
}
Notes for Blazor Server
- The interceptor buffers audit entries during SavingChanges and flushes after SaveChanges completes to avoid blocking the synchronization context.
- Channel mode further decouples request latency from audit DB writes.
XAF – Web API integration (Startup.cs)
Use the same registration approach in your Web API Startup.
public void ConfigureServices(IServiceCollection services)
{
// optional: ambient operation context
services.AddSingleton<IOperationContext, OperationContext>();
var auditOptions = Configuration.GetSection("Audit").Get<AuditOptions>();
services.AddEfCoreAudit(o => {
var auditConn = Configuration.GetConnectionString("ConnectionString_audit") ?? Configuration.GetConnectionString("ConnectionString");
// Example provider registrations (pick one):
// o.UseNpgsql(auditConn);
// o.UseSqlServer(auditConn);
}, auditOptions);
services.AddXafWebApi(builder => {
builder.ObjectSpaceProviders
.AddSecuredEFCore(options => { options.PreFetchReferenceProperties(); })
.WithDbContext<MyAppDbContext>((serviceProvider, options) => {
options.UseConnectionString(Configuration.GetConnectionString("ConnectionString"));
// attach interceptor
var interceptor = serviceProvider.GetService<AuditSaveChangesInterceptor>();
if (interceptor != null)
{
options.AddInterceptors(interceptor);
}
})
.AddNonPersistent();
// ... rest of Web API configuration
}, Configuration);
}
Notes for Web API
- Web API apps are already running on an async environment; when using Channel mode the background worker persists audits without blocking request threads.
- If you do not register IOperationContext, the interceptor will still emit audit entries but Actor/CorrelationId will be null.
OperationContext (optional)
To supply Actor and CorrelationId without HttpContext you can register a small ambient context (AsyncLocal-based):
public interface IOperationContext
{
string? Actor { get; }
string? CorrelationId { get; }
IDisposable Use(string? actor = null, string? correlationId = null);
}
public sealed class OperationContext : IOperationContext
{
private sealed class State { public string? Actor; public string? CorrelationId; }
private static readonly AsyncLocal<State?> _state = new();
public string? Actor => _state.Value?.Actor;
public string? CorrelationId => _state.Value?.CorrelationId;
public IDisposable Use(string? actor = null, string? correlationId = null)
{
var prev = _state.Value;
var next = new State { Actor = actor ?? prev?.Actor, CorrelationId = correlationId ?? prev?.CorrelationId };
_state.Value = next;
return new Revert(() => _state.Value = prev);
}
private sealed class Revert : IDisposable { private readonly Action _undo; private bool _done; public Revert(Action undo) => _undo = undo; public void Dispose() { if (_done) return; _undo(); _done = true; } }
}
Usage in request handler / controller / service:
using (opCtx.Use(actor: "user@example.com", correlationId: Guid.NewGuid().ToString("n")))
{
// Save changes on your XAF object space / DbContext – audit entries will include actor & correlation id
}
Recommendations and troubleshooting
- For development you can use EnsureCreated on the AuditDbContext factory to create the audit database on startup.
- If you see the application freeze in Blazor Server: ensure you do not call async APIs synchronously – the interceptor implementation in this library avoids sync-over-async and uses post-save buffering or Channel mode.
- Tune BatchSize and FlushInterval to balance latency vs throughput.
- Monitor logs for the background worker to detect persistent failures when writing audits.
Web API Controller Usage
The library includes an IAuditService that can be injected into your controllers to query audit entries. Here's a complete example:
Startup Configuration (XAF Web API)
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAuthenticationTokenProvider, JwtTokenProviderService>();
// Make IHttpContextAccessor available for interceptor to capture actor
services.AddHttpContextAccessor();
// Register audit infrastructure (audit DB and interceptor)
var mainConn = Configuration.GetConnectionString("ConnectionString");
var auditConn = Configuration.GetConnectionString("ConnectionString_audit") ?? mainConn;
var auditOptions = Configuration.GetSection("Audit").Get<AuditOptions>() ?? new AuditOptions();
if (string.IsNullOrEmpty(auditOptions.SourceApp))
{
auditOptions.SourceApp = HostEnvironment.ApplicationName;
}
services.AddEfCoreAudit(options =>
{
if (!string.IsNullOrEmpty(auditConn))
{
options.UseNpgsql(auditConn);
}
}, auditOptions);
services.AddXafWebApi(builder =>
{
builder.ConfigureOptions(options =>
{
options.BusinessObject<Customer>();
});
builder.ObjectSpaceProviders
.AddSecuredEFCore(options => { options.PreFetchReferenceProperties(); })
.WithDbContext<MyAppDbContext>((serviceProvider, options) =>
{
options.UseConnectionString(Configuration.GetConnectionString("ConnectionString"));
// Attach the audit interceptor to capture changes
try
{
var interceptor = serviceProvider.GetRequiredService<AuditSaveChangesInterceptor>();
options.AddInterceptors(interceptor);
}
catch
{
// ignore if interceptor not registered
}
})
.AddNonPersistent();
builder.Security
.UseIntegratedMode(options =>
{
options.RoleType = typeof(PermissionPolicyRole);
options.UserType = typeof(ApplicationUser);
})
.AddPasswordAuthentication();
}, Configuration);
services.AddControllers();
services.AddAuthentication().AddJwtBearer(/* JWT config */);
}
Controller Implementation
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class AuditController : ControllerBase
{
private readonly IAuditService _auditService;
public AuditController(IAuditService auditService)
{
_auditService = auditService;
}
// GET api/audit - Query audit entries with filtering
[HttpGet]
public async Task<IActionResult> Query(
[FromQuery] string? sourceApp,
[FromQuery] string? tableName,
[FromQuery] string? operation,
[FromQuery] string? actor,
[FromQuery] string? correlationId,
[FromQuery] DateTime? fromUtc,
[FromQuery] DateTime? toUtc,
[FromQuery] string? fullText,
[FromQuery] int skip = 0,
[FromQuery] int take = 50,
[FromQuery] string? orderBy = "Timestamp",
[FromQuery] bool desc = true,
CancellationToken ct = default)
{
var queryOptions = new AuditQueryOptions
{
SourceApp = sourceApp,
TableName = tableName,
Actor = actor,
CorrelationId = correlationId,
FromUtc = fromUtc,
ToUtc = toUtc,
FullText = fullText
};
if (!string.IsNullOrEmpty(operation))
{
if (Enum.TryParse<AuditOperation>(operation, true, out var op))
queryOptions.Operation = op;
}
var paging = new PagingOptions { Skip = skip, Take = take, OrderBy = orderBy, Descending = desc };
var result = await _auditService.QueryAsync(queryOptions, paging, ct);
return Ok(result);
}
// GET api/audit/{id} - Get specific audit entry by ID
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id, CancellationToken ct = default)
{
var entry = await _auditService.GetByIdAsync(id, ct);
if (entry == null) return NotFound();
return Ok(entry);
}
// POST api/audit/batch - Save multiple audit entries (admin endpoint)
[HttpPost("batch")]
[Authorize(Roles = "Admin")] // Protect this endpoint appropriately
public async Task<IActionResult> SaveBatch([FromBody] IEnumerable<AuditEntryDto> models, CancellationToken ct = default)
{
await _auditService.SaveManyAsync(models, ct);
return Accepted();
}
}
How Actor is Captured in Web API
The library automatically captures the actor (authenticated user) from the Web API request context:
HttpContextAccessor Registration: The library requires
IHttpContextAccessorto be registered:services.AddHttpContextAccessor();Actor Resolution: The
AuditSaveChangesInterceptorautomatically extracts the actor from the current HTTP request using this priority order:User.Identity.Name(primary identity name)ClaimTypes.NameIdentifierclaim"sub"claim (JWT subject)ClaimTypes.Emailclaim
JWT Integration: For JWT-authenticated APIs, the actor is typically extracted from:
{ "sub": "user@example.com", "name": "John Doe", "email": "user@example.com" }Example API Request:
POST /api/customers Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Content-Type: application/json { "name": "New Customer", "email": "customer@example.com" }The audit entry will automatically capture:
- Actor:
"user@example.com"(from JWT claims) - Operation:
"Insert" - TableName:
"Customers" - Timestamp: Current UTC time
- Changes: JSON diff of the new entity
- Actor:
Example API Queries
Query recent changes by a specific user:
GET /api/audit?actor=user@example.com&take=20&desc=true
Filter by table and operation:
GET /api/audit?tableName=Customers&operation=Update&fromUtc=2023-01-01T00:00:00Z
Search audit entries:
GET /api/audit?fullText=important+customer&take=10
Connection String Configuration
{
"ConnectionStrings": {
"ConnectionString": "Host=localhost;Database=appdb;Username=app;Password=pass",
"ConnectionString_audit": "Host=localhost;Database=auditdb;Username=audit;Password=pass"
},
"Audit": {
"Mode": "Channel",
"BatchSize": 200,
"FlushInterval": "00:00:02",
"MaxQueueLength": 20000,
"DropWhenFull": false
}
}
The audit system will automatically capture all changes made through your XAF Web API endpoints, providing complete traceability of who changed what and when.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.20)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.20)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 173 | 9/24/2025 |
Initial release with EF Core interceptor-based audit logging, channel processing, and XAF integration.