Tamper-evident audit logging and compliance platform for .NET
MillWorks.AuditCore is a comprehensive audit logging framework for .NET applications that enforces data integrity at the storage layer through cryptographic hash chains and HMAC signatures. Built for organizations operating under HIPAA, FERPA, SOC 2, GDPR, and IRB requirements, it provides tamper-evident logging, field-level encryption, and automated compliance validation -- capabilities that are typically spread across multiple commercial products. The library integrates with Entity Framework Core as a SaveChanges interceptor, capturing every entity change with zero modifications to existing application code.
| Component | Requirement |
|---|---|
| .NET | .NET 10.0+ |
| Entity Framework Core | 10.0+ |
| SQL Server | 2016+ (or Azure SQL) |
| Redis (optional) | 6.0+ — required only for distributed locking and Redis dead letter queue |
| Azure Blob Storage (optional) | Required only for archival |
Versioning policy: This project follows Semantic Versioning 2.0. The public API surface consists of the builder API (AddMillWorksAudit), the IAuditProvider contract, and all types in the MillWorks.AuditCore.Abstractions package.
| Package | Purpose |
|---|---|
MillWorks.AuditCore |
Primary install — batteries included, pulls in EF Core + ASP.NET Core wiring |
MillWorks.AuditCore.Abstractions |
Pure .NET — models, DTOs, interfaces. No EF or ASP.NET dependencies. Reference this from shared libraries or non-web hosts. |
MillWorks.AuditCore.EntityFramework |
EF Core data layer without ASP.NET host dependencies |
MillWorks.AuditCore.Services |
Business logic — compliance, encryption, dead letter queue, query/reporting |
MillWorks.AuditCore.Providers |
Entity-specific audit enrichment via IAuditProvider |
Most consumers install only MillWorks.AuditCore. The other packages are available for advanced scenarios (e.g., referencing Abstractions from a shared domain library that should not depend on ASP.NET).
EF Core SaveChangesInterceptor automatically captures create, update, and delete operations across all tracked entities. Entities are diffed at the property level, recording old values, new values, and changed property lists. No attribute decoration or manual logging calls required -- opt out with [NoAudit] when needed.
Every audit event is linked into a cryptographic hash chain. Each record's SHA-256 hash incorporates the previous record's hash, forming an append-only ledger that detects insertion, deletion, or modification of any record in the sequence. Chain integrity can be verified on demand or on a schedule. Tamper alerts are recorded as security events.
AES-256-GCM encryption for sensitive audit fields, applied transparently through EF Core value converters. Mark properties with [EncryptedField] or [SensitiveData(AutoEncrypt = true)]. Key management supports Azure Key Vault for cloud deployments or file-based key storage for DMZ and air-gapped environments. Per-field key derivation ensures compromise of one field does not expose others.
Built-in validators for seven regulatory standards: GDPR (Articles 17, 25, 30, 32), HIPAA (45 CFR Part 164), FERPA (34 CFR Part 99), SOC 2 (Trust Services Criteria), ISO 27001 (Annex A), PCI-DSS, and STIG. Validators inspect the audit log for required controls and produce structured compliance reports with pass/fail per rule, severity, regulation references, and remediation recommendations.
Enforcement modes control what happens when a non-compliant operation is intercepted:
| Mode | Behavior |
|---|---|
Advisory (default) |
Log a warning. The operation proceeds normally. |
AuditOnly |
Log a warning and create an AuditSecurityEvent record. The operation proceeds. |
Enforce |
Block the operation by throwing a ComplianceViolationException. The exception includes the standard name, entity type, user ID, and regulation reference (e.g., "34 CFR §99.30"). Callers should catch this exception and return an appropriate error response. |
Audit events that fail to persist (database timeout, transient fault) are captured in a dead letter queue rather than lost. A background processor automatically retries failed events with configurable retry policies.
All three providers ship in v1.0:
| Provider | Backing Store | Best For |
|---|---|---|
InMemory |
ConcurrentDictionary |
Development and testing |
FileSystem |
JSON files with semaphore locking | Single-instance production without Redis |
Redis |
Sorted sets with 30-day expiry | Multi-instance production deployments |
Redis-based distributed locking ensures hash chain consistency across multiple application instances writing audit events concurrently. Falls back to in-memory locking for single-instance deployments.
Completed audit records can be archived to Azure Blob Storage with integrity verification. Archives are checksummed and can be restored on demand. Background archival runs on a configurable schedule with retention policies.
Per-entity audit enrichment through the IAuditProvider interface. Register providers for specific entity types to control which actions are audited, add domain-specific metadata, and mask sensitive properties before they reach the audit log.
Full-text search, date-range filtering, entity trail reconstruction, user activity timelines, event type distribution, and top-user reports. Compliance reports can be generated per standard for any date range. All query services use AsNoTracking() for read performance.
Install the primary package (pulls in all dependencies):
dotnet add package MillWorks.AuditCorevar builder = WebApplication.CreateBuilder(args);
builder.Services.AddMillWorksAudit(audit =>
{
audit.Options.ApplicationName = "MyApp";
audit.UseEntityFramework(ef =>
{
ef.ConnectionString = builder.Configuration
.GetConnectionString("DefaultConnection")!;
ef.EnsureDatabaseCreated = true;
ef.Schema = "audit";
});
audit.UseSecurity(security =>
{
security.EnableTamperDetection = true;
});
});
var app = builder.Build();
app.UseMillWorksAudit();
app.Run();Inject IAuditLogger anywhere in your application:
public class OrderService(IAuditLogger auditLogger)
{
public async Task PlaceOrderAsync(Order order)
{
// ... business logic ...
await auditLogger.LogAsync("Order.Placed", new
{
OrderId = order.Id,
Total = order.Total,
ItemCount = order.Items.Count
});
}
}For multi-step operations, use audit scopes to accumulate context:
await using var scope = auditLogger.CreateScope("Order.Fulfillment", order);
scope.SetCustomField("WarehouseId", warehouse.Id);
// ... pick, pack, ship ...
scope.SetCustomField("TrackingNumber", trackingNumber);
scope.SetCustomField("ShippedAt", DateTimeOffset.UtcNow);
// Event is persisted with all fields when the scope disposesAny entity saved through a DbContext that has the audit interceptor registered will be captured automatically:
dbContext.Products.Add(new Product { Name = "Widget", Price = 9.99m });
await dbContext.SaveChangesAsync();
// AuditEvent is created with Action = Created, entity name, key values,
// and a full snapshot of the new property values -- no code changes needed.IAuditProvider is the primary extensibility point for per-entity audit behavior. Implement this interface to control what gets audited and how audit events are enriched for a given entity type.
public interface IAuditProvider
{
/// The entity type name this provider handles (e.g., "Patient", "FinancialRecord")
string EntityType { get; }
/// Create an audit event for the given action and entity state
Task<AuditEvent> CreateAuditEventAsync(string action, object? entity, object? oldValues = null);
/// Return false to suppress auditing for a specific action/entity combination
Task<bool> ShouldAuditAsync(string action, object entity);
/// Add domain-specific metadata to an audit event after creation
Task EnrichAuditEventAsync(AuditEvent auditEvent, object? entity);
/// Compute a property-level diff between old and new entity state
Dictionary<string, object?> GetChanges(object? oldValues, object? newValues);
}A BaseAuditProvider abstract class provides default implementations for change tracking, HTTP context integration (IP address, user agent, request ID), and reflection-based property comparison. Most providers only need to override EntityType and optionally ShouldAuditAsync:
public class PatientAuditProvider : BaseAuditProvider
{
public PatientAuditProvider(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor) { }
public override string EntityType => "Patient";
public override Task<bool> ShouldAuditAsync(string action, object entity)
{
// Audit all actions on Patient entities
return Task.FromResult(true);
}
}Register providers in the builder:
audit.RegisterProviders(registry =>
{
registry.AddProvider<PatientAuditProvider>("Patient");
registry.AddProvider<FinancialRecordAuditProvider>("FinancialRecord");
});The [NoAudit] attribute can be applied at two levels:
- Entity level (
[NoAudit] class TempCache { ... }) — the entire entity type is excluded from automatic audit capture. - Property level (
[NoAudit] public string InternalNotes { get; set; }) — the property is excluded from change tracking and will not appear in old/new value diffs.
[NoAudit] applies only to the decorated target. It does not cascade to navigation properties or owned entities. If you need to exclude a related entity, apply [NoAudit] to that entity's class directly.
Abstractions (pure .NET -- no EF, no ASP.NET dependencies)
Models, DTOs, Interfaces, Enums, Constants, Requests, Responses
|
EntityFramework (EF Core data layer)
DbContext, Entities, Interceptor, Repositories, Migrations, Value Converters
|
Providers (entity-specific audit enrichment)
IAuditProvider, BaseAuditProvider, per-entity implementations
|
Services (business logic)
Compliance validators, Tamper detection, Encryption, Dead letter queue,
Query/Search/Report, Archival, Maintenance, Mapping
|
AspNetCore (application entry point)
MillWorksAuditBuilder, ServiceCollectionExtensions, Middleware, Options
Abstractions is a pure .NET library with no framework dependencies. It can be referenced from console applications, background workers, Azure Functions, or any .NET host without pulling in ASP.NET Core or Entity Framework.
Each layer depends only on the layers below it. The AspNetCore package is the top-level integration point that wires everything together through dependency injection.
The full builder API:
builder.Services.AddMillWorksAudit(audit =>
{
// Application identity
audit.Options.ApplicationName = "MyApp";
audit.Options.Environment = "Production"; // Default: "Production"
audit.Options.EnableDigitalSignatures = true;
audit.Options.HmacKey = builder.Configuration["Audit:HmacKey"]!;
// Entity Framework storage (required)
audit.UseEntityFramework(ef =>
{
ef.ConnectionString = "Server=...";
ef.Schema = "audit"; // SQL Server schema (default: "audit")
ef.MigrateOnStartup = true; // Apply EF migrations on startup (default: false)
ef.EnsureDatabaseCreated = false; // Use EnsureCreated for dev (default: true)
ef.MigrationTimeoutSeconds = 120; // Default: 300
});
// Security and tamper detection
audit.UseSecurity(security =>
{
security.EnableTamperDetection = true; // Default: true
security.UseRedisLocking = true; // Default: false
security.RedisConnectionString = "localhost:6379";
});
// Compliance validation
audit.UseCompliance(compliance =>
{
compliance.Standards.Add(ComplianceStandard.HIPAA);
compliance.Standards.Add(ComplianceStandard.FERPA);
compliance.Standards.Add(ComplianceStandard.SOC2);
compliance.EnableAutomaticValidation = true; // Default: true
compliance.DataRetentionDays = 2555; // 7 years for HIPAA (default: 365)
compliance.EnforcementMode = ComplianceEnforcementMode.Enforce; // Default: Advisory
});
// Archival to Azure Blob Storage
audit.UseArchival(archival =>
{
archival.Provider = ArchivalProvider.AzureBlob;
archival.ConnectionString = "<azure-storage-connection>";
archival.ContainerName = "audit-archives";
archival.EnableBackgroundArchival = true;
archival.RetentionDays = 365;
archival.ArchivalIntervalHours = 24;
});
// Resilience and dead letter queue
audit.UseResilience(resilience =>
{
resilience.EnableDeadLetterQueue = true; // Default: true
resilience.DeadLetterProvider = DeadLetterProvider.FileSystem; // Default: InMemory
resilience.EnableBackgroundProcessor = true; // Default: true
resilience.MaxRetries = 3; // Default: 3
});
// Field-level encryption (Azure Key Vault)
audit.UseFieldEncryption("https://my-vault.vault.azure.net/");
// Or file-based encryption for air-gapped environments
// audit.UseFieldEncryptionWithFileStorage("/secure/keys", "<master-key>");
// Custom per-entity audit providers
audit.RegisterProviders(registry =>
{
registry.AddProvider<PatientAuditProvider>("Patient");
registry.AddProvider<FinancialRecordAuditProvider>("FinancialRecord");
});
});Security note: The HMAC key is a cryptographic secret. Do not hard-code it in source or check it into version control. Use .NET User Secrets for local development, and a secrets manager (Azure Key Vault, AWS Secrets Manager, environment variables) in deployed environments.
| Option | Default | Behavior |
|---|---|---|
EnsureDatabaseCreated |
true |
Calls EnsureCreated() on startup — creates the schema if the database does not exist. Suitable for development. |
MigrateOnStartup |
false |
Applies EF Core migrations on startup. Use this in production for schema evolution. |
If both are set to false, the application assumes the audit schema already exists. The first audit write will throw a DbUpdateException if the tables are missing. For production deployments, either enable MigrateOnStartup or apply migrations as part of your deployment pipeline (dotnet ef database update).
| Standard | Scope | What the Validator Checks |
|---|---|---|
| GDPR | EU data protection | Records of processing (Art. 30), right to erasure support (Art. 17), data protection by design (Art. 25), security of processing (Art. 32), user identification in audit trails |
| HIPAA | US health information | Audit controls (SS 164.312(b)), access controls, integrity controls, transmission security, person/entity authentication, emergency access procedures, automatic logoff tracking |
| FERPA | US education records | Access logging for education records, consent verification, directory information handling, legitimate educational interest tracking, disclosure logging |
| SOC 2 | Trust Services Criteria | Logical access controls, system operations monitoring, change management tracking, risk mitigation evidence, availability and processing integrity |
| ISO 27001 | Information security | Annex A control coverage: access control, cryptography, operations security, communications security, incident management, business continuity, compliance evidence |
| PCI-DSS | Payment card data | Cardholder data access tracking, authentication monitoring, network access logging, system component change tracking, security event alerting |
| STIG | DoD security baselines | Security-relevant event logging, access control enforcement, audit record content requirements, timestamp accuracy, audit storage capacity monitoring |
All tables are created under a configurable SQL Server schema (default: audit).
| Table | Purpose | Key Columns |
|---|---|---|
AuditEvents |
Primary audit event store | Id, EventType, EntityName, Action, UserId, JsonData, StartDate, CorrelationId |
AuditLogs |
Entity change log with old/new values | Id, EntityName, EntityId, Action, OldValues, NewValues, ChangedProperties, UserId |
AuditIntegrity |
Hash chain records for tamper detection | Id, AuditEventId, EventHash, PreviousHash, SequenceNumber, HmacSignature |
AuditArchiveRecords |
Metadata for archived audit batches | Id, ArchiveId, BlobPath, EventCount, Checksum, ArchivedAt, RestoredAt |
AuditSecurityEvents |
Security-relevant events and tamper alerts | Id, EventType, Severity, Description, SourceIp, DetectedAt |
Append-only entities (AuditEvents, AuditIntegrity, AuditSecurityEvents) do not carry update/delete audit columns to avoid unnecessary storage overhead.
See CONTRIBUTING.md for development setup, coding standards, and pull request guidelines.
This project is licensed under the MIT License. See the LICENSE file for details.
If you use MillWorks.AuditCore in academic work or grant-funded research, please cite it:
Carlberg, J. (2025). MillWorks.AuditCore: Tamper-evident audit logging and compliance platform for .NET. https://github.com/jesserules/millworks.auditcore