-
Notifications
You must be signed in to change notification settings - Fork 0
Advanced Topics
This page covers scenarios beyond the basic web API setup: worker services, manual audit entries, custom resolvers, data retention, and other less common configuration paths.
No HttpContext exists in background services or console applications, so the ASP.NET Core package doesn't apply here. You set the user identity yourself and push audit transactions through the pipeline by hand.
<PackageReference Include="RzR.DataVigil.Core" />
<PackageReference Include="RzR.DataVigil.Storage.File" />
<!-- or any other storage package -->Don't install RzR.DataVigil.AspNetCore — it depends on HttpContext.
Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddAuditTrail(options =>
{
options.UseSourceResolver<WorkerSourceResolver>();
options.Storage
.UseFile(Path.Combine(Directory.GetCurrentDirectory(), "audit-logs"))
.WithRetention(30);
});
services.AddAuditTrailFileStorage();
services.AddAuditRetentionService();
services.AddHostedService<MyWorker>();
});In each scope, grab IAuditScopeContext and call SetUser():
using var scope = _scopeFactory.CreateScope();
var scopeContext = scope.ServiceProvider.GetRequiredService<IAuditScopeContext>();
scopeContext.SetUser(new AuditUserInfo
{
UserId = "worker-batch-job",
UserName = "BatchProcessor",
IpAddress = "127.0.0.1"
});The IAuditScopeContext is scoped, so the user identity is tied to the DI scope lifetime. When the scope disposes, the user is cleared.
var pipeline = scope.ServiceProvider.GetRequiredService<AuditPipeline>();
var transaction = new AuditTransaction
{
Id = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Entries = new List<AuditEntry>
{
new AuditEntry
{
Id = Guid.NewGuid(),
EntityName = "Order",
EntityId = "42",
Action = AuditAction.Update,
Properties = new List<AuditEntryProperty>
{
new AuditEntryProperty
{
PropertyName = "Status",
PropertyType = "System.String",
OldValue = "Pending",
NewValue = "Shipped"
}
}
}
}
};
await pipeline.ProcessAsync(transaction, cancellationToken);ProcessAsync enriches the transaction with user/source/correlation info, runs GDPR rules, and persists through IAuditStore.
Even in a web app, sometimes you need to audit something that doesn't go through SaveChanges. Maybe it's a stored procedure call, an external API request, or a business event. The approach is the same as in a worker - build an AuditTransaction and hand it to AuditPipeline:
var pipeline = serviceProvider.GetRequiredService<AuditPipeline>();
var transaction = new AuditTransaction
{
Id = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Entries = new List<AuditEntry>
{
new AuditEntry
{
Id = Guid.NewGuid(),
EntityName = "Payment",
EntityId = "PAY-001",
Action = AuditAction.Create,
Properties = new List<AuditEntryProperty>
{
new AuditEntryProperty
{
PropertyName = "Amount",
PropertyType = "System.Decimal",
OldValue = null,
NewValue = "250.00"
}
}
}
}
};
var result = await pipeline.ProcessAsync(transaction, cancellationToken);In an ASP.NET Core context, the pipeline will still pick up user identity from HttpContext and correlation IDs from headers - you just skip the EF interceptor part.
The default resolver (DefaultUserResolver) uses IAuditScopeContext -> Thread.CurrentPrincipal -> anonymous. With AddAuditTrailAspNetCore(), it gets replaced by AspNetCoreUserResolver which reads from HttpContext.User.
If neither fits your needs, write your own:
public class MyUserResolver : IAuditUserResolver
{
private readonly ICurrentUserService _currentUser;
public MyUserResolver(ICurrentUserService currentUser)
=> _currentUser = currentUser;
public IResult<AuditUserInfo> Resolve()
{
return Result<AuditUserInfo>.Success(new AuditUserInfo
{
UserId = _currentUser.Id,
UserName = _currentUser.DisplayName,
Roles = _currentUser.Roles
});
}
}Register it:
services.AddAuditTrail(options =>
{
options.UseUserResolver<MyUserResolver>();
});The default returns "Unknown". In most cases you'll want to override this with your application name:
public class ApiSourceResolver : IAuditSourceResolver
{
public IResult<string> Resolve()
=> Result<string>.Success("OrderService-v2");
}
services.AddAuditTrail(options =>
{
options.UseSourceResolver<ApiSourceResolver>();
});There's no UseCorrelationProvider<T>() shortcut on AuditTrailOptions. The AspNetCore package handles this by registering AspNetCoreCorrelationProvider. For custom scenarios, register your implementation directly:
services.AddScoped<IAuditCorrelationProvider, MyCorrelationProvider>();| Resolver | Without AspNetCore | With AspNetCore |
|---|---|---|
| User |
IAuditScopeContext -> Thread.CurrentPrincipal -> anonymous |
IAuditScopeContext -> HttpContext.User
|
| Correlation | Activity.Current?.Id |
X-Correlation-Id -> X-Request-Id -> Activity.Current?.Id
|
| Trace | Activity.Current?.TraceId |
HttpContext.TraceIdentifier -> Activity.Current?.TraceId
|
| Source | "Unknown" |
"Unknown" (override recommended) |
Audit tables grow over time. The retention feature lets you cap how long records are kept.
services.AddAuditTrail(options =>
{
options.Storage
.UseSqlServer(connectionString)
.WithRetention(90); // keep 90 days
});
services.AddAuditRetentionService();AuditRetentionService is a BackgroundService that:
- Runs every 24 hours
- Creates a DI scope, resolves
IAuditStore - Calls
PurgeBeforeAsync(DateTimeOffset.UtcNow - RetentionDays) - If
RetentionDaysis null, does nothing - Catches exceptions internally so it never crashes the host
You can also trigger a purge yourself at any time:
var store = serviceProvider.GetRequiredService<IAuditStore>();
await store.PurgeBeforeAsync(DateTimeOffset.UtcNow.AddDays(-30));This deletes all transactions (and their entries/properties via cascade) older than the cutoff.
Each AuditTransaction has a Metadata property (IDictionary<string, string>) for custom key-value pairs. When building transactions manually, you can attach any extra context:
var transaction = new AuditTransaction
{
Id = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
Metadata = new Dictionary<string, string>
{
["RequestPath"] = "/api/orders",
["BatchId"] = "BATCH-2026-04-16"
},
Entries = { ... }
};In EF-backed storage, the Metadata dictionary is serialized as JSON in a single column.
By default, audit tables are created under the audit schema. Change it if needed:
options.Storage.Schema = "my_audit_schema";This affects all three tables (AuditTransactions, AuditEntries, AuditEntryProperties). Make sure to set it before running migrations.
RzR.DataVigil · Source Code · NuGet Packages · Built with .NET Standard 2.1
Getting started
Core features
Reference
Resources