-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
This page explains how the DataVigil packages relate to each other and how data flows from an EF Core SaveChanges call all the way to the audit store.
RzR.DataVigil.Abstractions (interfaces, enums, models)
│
▼
RzR.DataVigil.Core (pipeline, GDPR, options, resolvers)
│
┌────┼──────────────┐
│ │ │
▼ ▼ ▼
EFCore AspNetCore Storage.File
│
├── Storage.EfSqlServer
├── Storage.EfPostgreSql
└── Storage.EfMongoDb
Everything depends on Core, which pulls in Abstractions transitively. The EF-based storage packages reference EFCore for the shared AuditDbContextBase. The file storage provider talks directly to Core since it doesn't use Entity Framework at all.
AspNetCore is completely optional. It exists to resolve user identity and correlation IDs from HttpContext. Without it, the library falls back to Thread.CurrentPrincipal and System.Diagnostics.Activity.
Here's what happens when your application calls SaveChangesAsync() on an intercepted DbContext:
DbContext.SaveChangesAsync()
│
▼
AuditSaveChangesInterceptor.SavingChangesAsync()
│
├── Filters ChangeTracker entries:
│ - Must implement IAuditable
│ - Entity state must be Added, Modified, or Deleted
│ - Not in global exclusions
│ - Not in context-level exclusions (IAuditableContext)
│ - Passes IAuditableEntity.ShouldAudit() if applicable
│
├── ChangeTrackerEntryBuilder.Build() for each entry
│ - Create: all current values (OldValue = null)
│ - Update: only changed properties (old != new)
│ - Delete: all original values (NewValue = null)
│ - Extracts primary key (single or composite)
│ - Removes excluded fields (IAuditableEntity.GetExcludedFields())
│
▼
AuditPipeline.ProcessAsync(AuditTransaction)
│
├── 1. Enriches transaction:
│ UserId, UserName, IpAddress <- IAuditUserResolver
│ Source <- IAuditSourceResolver
│ CorrelationId, TraceId <- IAuditCorrelationProvider
│
├── 2. GDPR storage policies:
│ GdprProcessor.ApplyStoragePolicies(entry)
│ → Exclude: removes property entirely
│ → Mask: "alice@mail.com" → "a***m"
│ → Anonymize: "[ANONYMIZED]"
│ → Hash: SHA-256 hex
│ → Custom: your Func<string,string>
│ Sets GdprStorageState on the transaction
│
├── 3. Persists:
│ IAuditStore.SaveAsync(transaction)
│
▼
Original SaveChanges proceeds normally
The interceptor fires before the actual database save. If the audit pipeline throws, the main save still goes through - audit failures are handled gracefully.
Read auditing is opt-in. There are two mechanisms, one for relational databases and one for MongoDB.
The AuditCommandInterceptor hooks into ReaderExecutedAsync. After a SELECT completes:
- Parses table names from the SQL text (FROM / JOIN clauses)
- Resolves each table to an
IEntityTypevia EF Core metadata - Skips anything that doesn't implement
IAuditable - Extracts entity IDs from WHERE clause parameters
- Optionally captures column names and values
- Adds
AuditEntryobjects to the scopedAuditReadCollector
MongoDB doesn't go through DbCommand, so it uses an AuditMaterializationInterceptor (implements IMaterializationInterceptor). This fires every time EF Core materializes an entity instance. If the entity is IAuditable, a read entry gets collected.
In ASP.NET Core, the UseAuditReadFlush() middleware runs after the response completes. It grabs the AuditReadCollector from DI, wraps all collected entries into a single AuditTransaction, and sends it through AuditPipeline.ProcessAsync().
In non-web scenarios, you'd flush manually or use AuditReadService.LogReadAsync().
GDPR has two independent layers that operate at different points in the lifecycle:
┌──────────────────────────────────┐
│ STORAGE POLICIES │
│ (applied BEFORE persistence) │
│ │
│ Raw value -> Transformed value │
│ Original data never stored │
└──────────────────────────────────┘
│
▼
IAuditStore
(data at rest)
│
▼
┌──────────────────────────────────┐
│ RETRIEVAL POLICIES │
│ (applied WHEN querying) │
│ │
│ Stored value -> Visible value │
│ Role/claim gated per field │
└──────────────────────────────────┘
Storage policies are irreversible. The raw data is gone after transformation. Retrieval policies are dynamic - the same stored value shows up differently depending on who's asking.
The order matters because each registration builds on what came before:
1. AddAuditTrail(options => { ... }) <- creates AuditTrailOptions, registers core services
2. AddAuditTrailEfCore() <- registers interceptors (needs options from step 1)
3. AddAuditTrailSqlServer() <- registers IAuditStore (needs options from step 1)
AddAuditTrailPostgreSqlServer()
AddAuditTrailMongoDb()
AddAuditTrailFileStorage()
4. AddAuditTrailAspNetCore() <- replaces default resolvers with HttpContext-aware ones
5. AddAuditRetentionService() ← optional background purge
Internally, AddAuditTrail calls AuditTrailBuilder.Build() which registers:
-
AuditTrailOptionsas singleton -
GdprPolicyRegistryandGdprProcessoras singletons -
AuditPipelineas scoped (one per request/scope) -
IAuditScopeContextas scoped - Default resolvers (replaced if you call
UseUserResolver<T>()orUseSourceResolver<T>(), or by AspNetCore)
-
AuditPipelineis scoped - each HTTP request or DI scope gets its own instance -
AuditReadCollectoris scoped - read entries accumulate per-request, then flush -
IAuditScopeContextis scoped - allows setting user identity for non-web scenarios -
GdprProcessorandGdprPolicyRegistryare singletons - GDPR rules don't change at runtime -
FileAuditStoreuseslockfor thread-safe file writes -
AuditReadCollector.Collect()is thread-safe (concurrent read interceptors can fire in parallel)
RzR.DataVigil · Source Code · NuGet Packages · Built with .NET Standard 2.1
Getting started
Core features
Reference
Resources