Observer.Auditing.Contract 1.0.1

dotnet add package Observer.Auditing.Contract --version 1.0.1
                    
NuGet\Install-Package Observer.Auditing.Contract -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Observer.Auditing.Contract" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Observer.Auditing.Contract" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Observer.Auditing.Contract" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Observer.Auditing.Contract --version 1.0.1
                    
#r "nuget: Observer.Auditing.Contract, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Observer.Auditing.Contract@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Observer.Auditing.Contract&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Observer.Auditing.Contract&version=1.0.1
                    
Install as a Cake Tool

Observer.Auditing.Contract

Drop-in change auditing for any .NET service. Captures every insert/update/delete you opt into, stores it locally in the same transaction as the business change, and ships it to the Audit service over gRPC in the background.

No message broker. No added request latency. No lost records if the audit service is down.

handler saves          →  interceptor writes an outbox row in the SAME transaction
                          (request returns here — audit cost ≈ one local INSERT)
background dispatcher  →  gRPC RecordAuditBatch  →  audit service stores it
background registrar   →  gRPC RegisterAuditableEntities  →  registry rows created for you

Two packages:

Package Reference from Contains
ObserverTech.Auditing.Abstractions Domain / Shared / Application IAuditableEntity, IAuditScope, [NoAudit], the enums. Zero dependencies.
Observer.Auditing.Contract Infrastructure Interceptor, outbox, dispatcher, gRPC transport

Setup — five steps

1. Mark your entity base

public abstract class Entity : IAuditableEntity
{
    public Guid ObjectKey { get; set; } = Guid.NewGuid();   // must be CLR-generated
    public bool IsDeleted { get; set; }
}

2. Map and migrate the outbox

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.AddAuditOutbox();      // creates Audit.AuditOutbox
}
dotnet ef migrations add AddAuditOutbox

If you apply a global query filter by looping over every mapped type, skip this one — it has no IsDeleted column.

3. Register, and declare what you audit — in code

services.AddAuditing<ApplicationDbContext>(configuration, audit =>
{
    audit.Audit<Area>();
    audit.Audit<DeviceOwner>().Ignoring(owner => owner.NationalId);
    audit.Audit<Device>();
});

services.AddDbContext<ApplicationDbContext>((serviceProvider, options) => options
    .UseNpgsql(connectionString)
    .AddAuditCapture(serviceProvider));      // no-op when disabled

AddAuditing must come first. A type argument can't be misspelled and survives renames — a config string can't do either.

4. Tell it who the user is

internal sealed class AuditActorProvider(IUserContext user) : IAuditActorProvider
{
    public Guid UserId => Guid.TryParse(user.UserId, out Guid id) ? id : Guid.Empty;
    public string? DisplayName => user.DisplayName;
}

services.AddScoped<IAuditActorProvider, AuditActorProvider>();   // before AddAuditing

Skip this and every change is attributed to the system actor. Optionally add IAuditCorrelationProvider to carry your request correlation id.

5. Configure

"AuditService": {
  "Enabled": true,
  "GrpcAddress": "http://127.0.0.1:5327",   // the audit service's h2c port, NOT its REST port
  "SourceService": "AMS.AVL.Core"           // must match your registry rows
}

That's it. On startup the service announces its entities and the registry rows are created automatically — no SQL.


Declaring business intent

Only when a column diff can't express it:

_auditScope.Set(AuditBusinessAction.Deactivate);
await _unitOfWork.SaveChangesAsync();

Create, Update and Delete are inferred. A soft delete (IsDeleted false → true) is detected as Delete automatically.

Redacting — two levels

Value stored Change recorded Use for
.Ignoring(x => x.NationalId) ✅ name only Personal data you must prove was altered
[NoAudit] on the property Secrets — tokens, credentials, SIM numbers

A save that only touches an Ignoring property is recorded, naming it. A save that only touches a [NoAudit] property records nothing — the entry would betray the change by existing.

Configuration reference

Key Default What it does
Enabled false Kill switch. Nothing is registered when off.
GrpcAddress Audit service h2c endpoint. Required.
SourceService Stamped on every record. Required.
PollInterval 5s How often the outbox drains
BatchSize 200 Rows per batch (server max 500)
SendTimeout 30s Per-call gRPC deadline
MaxValueLength 4000 Per snapshot value, then truncated
RunDispatcher true Set false on all but one replica
OutboxRetention 7d Prune delivered rows after this. null = keep forever
RetentionSweepInterval 1h How often pruning runs
SelfRegister true Announce entities at startup
RegistryRefreshInterval 5m Re-announce + refresh policy
Aggregates [] Legacy string allowlist. Prefer the typed declaration.
DisabledAggregates [] Mute an entity in this environment. Narrowing only.

Outbox retention

The outbox is a queue, not a second copy of the trail. Delivered rows are pruned after OutboxRetention. Pending and dead-lettered rows are never pruned — they're changes with no compliance record yet.

Scaling out

Rows drain in Id order, which preserves per-aggregate order only while one dispatcher runs. On multiple replicas set RunDispatcher: false on all but one. Two dispatchers racing is safe, just wasteful.

Monitoring

Signal Query Alert when
Backlog count(*) WHERE "SentAt" IS NULL > 10k, or oldest > 15 min
Poison rows count(*) WHERE "LastError" IS NOT NULL any — capture produced an unstorable payload
Coverage drift in AuditDB: WHERE "IsRegistered" = false any — a registry row is missing

Troubleshooting

Symptom Cause
Outbox grows, LastError null Can't reach the audit service. Check GrpcAddress is the h2c port.
Audit service logs 307 for gRPC calls UseHttpsRedirection on the gRPC port. Remove it — gRPC clients don't follow redirects.
HTTP_1_1_REQUIRED gRPC pointed at an HTTP/1.1 port. h2c needs its own port.
Startup warns "aggregates that do not exist" Typo in Aggregates, or a renamed class.
Everything recorded as changed, snapshots identical The repository calls DbSet.Update on a tracked entity, discarding EF change detection.
ChangedByName is the empty GUID No IAuditActorProvider registered, or the call was unauthenticated.

Known gaps

  1. No service-account credential. The dispatcher runs with no HttpContext; it needs client-credentials or mTLS before any non-local deployment.
  2. Protos are vendored, not packaged. Scripts/verify-proto-parity.ps1 fails on drift.

Build

dotnet build Observer.Auditing.Contract.slnx
dotnet pack  Observer.Auditing.Contract.slnx -c Release -o nupkg
pwsh Observer.Auditing.Contract/Scripts/verify-proto-parity.ps1
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 143 8/3/2026
1.0.0 119 8/1/2026