Skip to content

Advanced Topics

RzR edited this page Apr 20, 2026 · 1 revision

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.


Worker services and console apps

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.

Package selection

<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.

DI registration

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>();
    });

Setting the user

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.

Pushing transactions manually

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.


Manual audit entries (without EF Core)

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.


Custom resolvers

User resolver

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>();
});

Source resolver

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>();
});

Correlation provider

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>();

Default behavior summary

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)

Data retention

Audit tables grow over time. The retention feature lets you cap how long records are kept.

Configure

services.AddAuditTrail(options =>
{
    options.Storage
        .UseSqlServer(connectionString)
        .WithRetention(90); // keep 90 days
});

services.AddAuditRetentionService();

How it works

AuditRetentionService is a BackgroundService that:

  • Runs every 24 hours
  • Creates a DI scope, resolves IAuditStore
  • Calls PurgeBeforeAsync(DateTimeOffset.UtcNow - RetentionDays)
  • If RetentionDays is null, does nothing
  • Catches exceptions internally so it never crashes the host

Manual purge

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.


Transaction metadata

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.


Schema configuration (SQL Server / PostgreSQL)

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.

Clone this wiki locally