Tenantry Pro
Audit Logging
Use this package to record EF Core entity changes (insert, update, delete) with the current tenant ID, timestamps, and changed property values.
What Tenantry.Pro.EfCore Provides
TenantAuditInterceptor<TKey>— EF CoreSaveChangesInterceptorthat records changesIAuditStore— interface for persisting or dispatching audit entries- Default
IAuditStoreimplementation that writes structured log entries viaILogger AuditEntry— immutable record with table, action, PK, old/new values, tenant ID, timestamppro.AddAuditLogging(opts => ...)— registers the interceptor and default store in DIoptions.UseAuditLogging<TKey>(sp)— wires the interceptor into aDbContext
Requirements
Tenantry.Pro.EfCoreNuGet package- Any EF Core provider (not SqlServer-specific)
Registration
using Microsoft.EntityFrameworkCore;
using Tenantry.Pro;
using Tenantry.Pro.EfCore;
using Tenantry.Pro.EfCore.Extensions;
// 1. Register Tenantry with audit logging
builder.Services.AddTenantry<string>(tenant =>
{
tenant.ResolveFromHeader("X-Tenant-Id");
tenant.UseStore<MyTenantStore>();
tenant.UsePro(pro =>
{
pro.WithLicence(builder.Configuration["Tenantry:Licence"]!);
pro.AddAuditLogging(opts =>
{
// Optionally exclude entity types from audit:
opts.ExcludeTypes.Add(typeof(OutboxMessage));
});
});
});
// 2. Wire the interceptor into your DbContext
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("AppDb")!);
options.UseAuditLogging<string>(sp); // adds TenantAuditInterceptor<TKey>
});Providing a custom audit store
The default store logs each audit entry as a structured Information log message.
To persist entries to a database, event stream, or external audit service, implement
IAuditStore and register it after pro.AddAuditLogging():
builder.Services.AddSingleton<IAuditStore, MyDatabaseAuditStore>();public sealed class MyDatabaseAuditStore(AuditDbContext db) : IAuditStore
{
public async Task SaveAsync(
IReadOnlyList<AuditEntry> entries, CancellationToken cancellationToken = default)
{
foreach (var entry in entries)
{
db.AuditLogs.Add(new AuditLog
{
TableName = entry.TableName,
Action = entry.Action.ToString(),
PrimaryKey = entry.PrimaryKey,
TenantId = entry.TenantId,
Timestamp = entry.Timestamp,
OldValues = JsonSerializer.Serialize(entry.OldValues),
NewValues = JsonSerializer.Serialize(entry.NewValues),
});
}
await db.SaveChangesAsync(cancellationToken);
}
}AuditEntry reference
| Property | Description |
|---|---|
TableName | EF Core table name (or CLR type name as fallback) |
Action | Insert, Update, or Delete |
PrimaryKey | Comma-separated PK value(s); null if no key defined |
OldValues | Property values before the change (empty for inserts) |
NewValues | Property values after the change (empty for deletes) |
ChangedProperties | Names of properties that changed |
Timestamp | UTC time the entry was recorded |
TenantId | Current tenant ID; null when no tenant scope is active |
Limitations
- Audit entries are recorded after a successful
SaveChangescall. Changes that fail and are rolled back are not audited. - EF Core's
ChangeTrackeruses reflection; this interceptor is not AOT or trim compatible. The analyzer will warn at build time — this is expected for migration and audit features. - If
IAuditStore.SaveAsyncthrows, the exception is caught and logged. The originalSaveChangesresult is returned normally — audit failures are non-fatal.
See also
- Background jobs & non-HTTP hosts —
AuditEntry.TenantIdisnullfor work that runs without a tenant scope; open a scope first if you want jobs audited under a tenant. - Troubleshooting — the AOT/trimming analyzer warnings are expected here.