Appouse.Observer 1.2.2

dotnet add package Appouse.Observer --version 1.2.2
                    
NuGet\Install-Package Appouse.Observer -Version 1.2.2
                    
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="Appouse.Observer" Version="1.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Appouse.Observer" Version="1.2.2" />
                    
Directory.Packages.props
<PackageReference Include="Appouse.Observer" />
                    
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 Appouse.Observer --version 1.2.2
                    
#r "nuget: Appouse.Observer, 1.2.2"
                    
#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 Appouse.Observer@1.2.2
                    
#: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=Appouse.Observer&version=1.2.2
                    
Install as a Cake Addin
#tool nuget:?package=Appouse.Observer&version=1.2.2
                    
Install as a Cake Tool

Appouse.Observer

Automatic, Detailed Audit Trail for .NET Applications

NuGet License: MIT

Appouse.Observer is a comprehensive audit trail library for .NET that automatically tracks every change to your database entities — both from your application code and from direct database operations. No marker interfaces required.

Supported Frameworks

Target EF Core Status
.NET 9 8.x ✅ Full support
.NET 8 8.x ✅ Full support
.NET 6 6.x ✅ Full support
.NET 5 5.x ✅ Full support
.NET Standard 2.0 3.1.x ✅ Full support (via ObserverDbContextBase)

✨ Features

  • Hybrid Auditing — Tracks changes via EF Core interceptors or SaveChanges override (application-level) AND database triggers (DB-level)
  • Multi-Framework — Supports .NET Standard 2.0, .NET 5, .NET 6, .NET 8, .NET 9
  • Multi-Database — PostgreSQL, SQL Server, Oracle, MySQL
  • Column-Level Tracking — Records old and new values for every changed column
  • Rich Metadata — Captures who, when, what, and how:
    • Application User, DB User
    • Method/Endpoint name
    • Activity ID, Trace Identifier, Correlation ID
    • Client IP, User Agent
  • Attribute-Based Configuration[AuditInclude], [AuditIgnore], [AuditOverride], [AuditMask]
  • Opt-In / Opt-Out Strategy — Choose to audit all entities by default or only selected ones
  • Custom Fields — Inject IAuditContextAccessor and set arbitrary fields, tags, and comments per scope
  • Sensitive Data Masking[AuditMask] automatically masks values (e.g., ****1234)
  • Immutable Audit Log — Append-only tables with DELETE/UPDATE protection at both EF Core and DB trigger level
  • Retention Policies — Automatic cleanup of old records via background service (global + per-table)
  • Zero-Code Audit — OptOut mode audits all entities by default — no attributes needed
  • Auto-Setup — Audit tables and triggers are automatically created at startup
  • Message Queue Publishing — Publish audit events to RabbitMQ, Kafka, Azure Service Bus via MassTransit
  • Transactional Outbox — Reliable message delivery with MassTransit's EF Core Outbox pattern
  • Flexible Delivery — Database only, event bus only, or both — configure per environment
  • Polly Resilience — Built-in retry (exponential backoff + jitter) and circuit breaker policies
  • Idempotency — Each audit entry has a unique IdempotencyKey preventing duplicates across retries and redelivery

📦 Packages

Package Description NuGet
Appouse.Observer Core library (abstractions, interceptor, models, Polly) NuGet
Appouse.Observer.PostgreSql PostgreSQL provider NuGet
Appouse.Observer.SqlServer SQL Server provider NuGet
Appouse.Observer.Oracle Oracle provider NuGet
Appouse.Observer.MySql MySQL provider NuGet
Appouse.Observer.MassTransit MassTransit messaging integration NuGet
Appouse.Observer.MassTransit.Outbox Transactional Outbox for reliable delivery NuGet

🚀 Quick Start

1. Install NuGet Packages

# Core + your database provider
dotnet add package Appouse.Observer
dotnet add package Appouse.Observer.PostgreSql    # or SqlServer, Oracle, MySql

# Optional: message queue publishing
dotnet add package Appouse.Observer.MassTransit

# Optional: transactional outbox for reliable delivery
dotnet add package Appouse.Observer.MassTransit.Outbox

2. Define Your Entities (No Interface Required!)

using Appouse.Observer.Abstractions;

// In OptOut mode (default), ALL entities are audited automatically.
// No marker interface needed!
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }

    [AuditIgnore]  // ← This property will NOT appear in audit logs
    public decimal InternalCost { get; set; }

    [AuditMask(ShowLast = 4)]  // ← Stored as "****5678" in audit logs
    public string? SupplierCode { get; set; }

    [AuditOverride(DisplayName = "Birim Fiyat", OutputFormat = "N2")]
    public decimal UnitPrice { get; set; }
}

// Exclude an entire entity from audit
[AuditIgnore]
public class AppSetting
{
    public int Id { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }
}

3. Configure Services

Observer supports two integration approaches. Choose the one that fits your target framework:

Option A: Interceptor Approach (.NET 5+)

Uses SaveChangesInterceptor (EF Core 5.0+). Register the interceptor on your DbContextOptionsBuilder:

// Program.cs
using Appouse.Observer.Extensions;
using Appouse.Observer.PostgreSql.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Register Observer core
builder.Services.AddObserver(options =>
{
    options.AuditSchema = "observer";
    options.AutoCreateTables = true;
    options.AutoCreateTriggers = true;
    options.EnableRetention = true;
    options.RetentionPeriod = TimeSpan.FromDays(730); // 2 years
    options.ProtectAuditTables = true;
});

// Register database provider
builder.Services.AddObserverPostgreSql(
    builder.Configuration.GetConnectionString("DefaultConnection")!);

// Register your DbContext WITH Observer interceptors
builder.Services.AddDbContext<MyDbContext>((sp, options) =>
{
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")!);
    options.AddObserverInterceptors(sp); // ← THIS enables EF Core interception
});
Option B: Base Class Approach (All Frameworks — including .NET Standard 2.0)

Uses ObserverDbContextBase<T> which overrides SaveChanges/SaveChangesAsync. Works on all EF Core versions, including 3.x:

using Appouse.Observer.Extensions;

// 1. Inherit your DbContext from ObserverDbContextBase
public class MyDbContext : ObserverDbContextBase<MyDbContext>
{
    public MyDbContext(
        DbContextOptions<MyDbContext> options,
        IServiceProvider serviceProvider)
        : base(options, serviceProvider) { }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Order> Orders => Set<Order>();
}

// 2. Register as usual — no AddObserverInterceptors needed
builder.Services.AddObserver(options =>
{
    options.AuditSchema = "observer";
    options.AutoCreateTables = true;
    options.AutoCreateTriggers = true;
});

builder.Services.AddObserverPostgreSql(connectionString);

builder.Services.AddDbContext<MyDbContext>((sp, options) =>
{
    options.UseNpgsql(connectionString);
    // No AddObserverInterceptors call — base class handles it
});

Note: Both approaches use the same AuditChangeCollector engine internally and produce identical audit entries. On .NET 5+ you can use either; on .NET Standard 2.0 only the base class approach is available.

Integration Approach Comparison
Interceptor (AddObserverInterceptors) Base Class (ObserverDbContextBase<T>)
Minimum Framework .NET 5 (EF Core 5.0+) .NET Standard 2.0 (EF Core 3.1+)
DbContext Changes None — interceptor hooks externally Inherit from ObserverDbContextBase<T>
DI Registration options.AddObserverInterceptors(sp) Pass IServiceProvider to constructor
Multiple DbContexts Each needs AddObserverInterceptors Each inherits from base class
Audit Logic Identical Identical

4. That's It! 🎉

Observer will automatically:

  • Create audit tables (observer.audit_log and observer.audit_detail)
  • Create database triggers on all auditable entity tables
  • Track every INSERT, UPDATE, DELETE with full metadata
  • Assign a unique IdempotencyKey (Guid) to each audit entry
  • Protect audit records from tampering

🏷️ Audit Attributes

Attribute Summary

Attribute Class Property Description
[AuditInclude] Explicitly include entity/property in audit
[AuditIgnore] Exclude entity/property from audit
[AuditOverride] Override column name, display name, or output format
[AuditMask] Mask sensitive values (e.g., ****1234)

Audit Mode: OptOut vs OptIn

Mode Default Behavior Attribute Usage
OptOut (default) All entities audited Use [AuditIgnore] to exclude
OptIn No entities audited Use [AuditInclude] to include
builder.Services.AddObserver(options =>
{
    options.AuditMode = AuditMode.OptIn;   // Only [AuditInclude] entities
    // options.AuditMode = AuditMode.OptOut;  // Everything (default)
});

[AuditIgnore] — Exclude from Audit

using Appouse.Observer.Abstractions;

// Class-level: entire entity excluded
[AuditIgnore]
public class TempCache
{
    public int Id { get; set; }
    public string Data { get; set; }
}

// Property-level: specific properties excluded
public class User
{
    public int Id { get; set; }
    public string Email { get; set; }       // ✅ Audited
    public string FullName { get; set; }    // ✅ Audited
    public string Role { get; set; }        // ✅ Audited

    [AuditIgnore]                            // ❌ NOT audited
    public string PasswordHash { get; set; }

    [AuditIgnore]                            // ❌ NOT audited
    public string? SecurityStamp { get; set; }
}

You can also exclude columns globally via ObserverOptions.ExcludedColumns (useful for cross-cutting exclusions like CreditCardNumber that apply to all entities):

options.ExcludedColumns.Add("CreditCardNumber");
options.ExcludedColumns.Add("SSN");

Note: Both methods work together. A property is excluded if either [AuditIgnore] is present or the column name is in ExcludedColumns.

[AuditInclude] — Explicitly Include in Audit

// OptIn mode: only this entity is audited
[AuditInclude]
public class Payment
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

// Mixed: entity excluded but specific property re-included
[AuditIgnore]
public class UserProfile
{
    public int Id { get; set; }

    [AuditInclude]  // ← This property IS audited despite class [AuditIgnore]
    public string Email { get; set; }

    public string InternalData { get; set; }  // ← NOT audited
}

[AuditOverride] — Customize Audit Output

Override column names, display names, or value formatting in audit logs:

public class Order
{
    public int Id { get; set; }

    [AuditOverride(DisplayName = "Sipariş Tutarı", OutputFormat = "N2")]
    public decimal TotalAmount { get; set; }  // Stored as "1,234.56"

    [AuditOverride(ColumnName = "order_date", OutputFormat = "yyyy-MM-dd")]
    public DateTime OrderDate { get; set; }   // Stored as "2026-04-10"
}
Property Description
ColumnName Overrides the column name in audit detail records
DisplayName Human-readable label for UI/reports
OutputFormat Format string applied via ToString(format)

[AuditMask] — Mask Sensitive Data

Automatically masks property values before storing in audit logs:

public class Payment
{
    public int Id { get; set; }

    [AuditMask(ShowLast = 4)]
    public string CreditCardNumber { get; set; }  // "****5678"

    [AuditMask(ShowFirst = 2, ShowLast = 2, MaskCharacter = '#')]
    public string PhoneNumber { get; set; }         // "05########90"

    [AuditMask(PreserveLength = false)]
    public string SecretKey { get; set; }            // "****"
}
Property Default Description
MaskCharacter '*' Character used for masking
ShowFirst 0 Visible characters at start
ShowLast 4 Visible characters at end
PreserveLength true Keep original length or use fixed 4-char mask

Note: Masking is applied to both OldValue and NewValue in audit details.

🎛️ Custom Fields (AuditContext)

Inject IAuditContextAccessor anywhere in your code to attach custom metadata to audit entries:

using Appouse.Observer.Abstractions;

public class OrderService
{
    private readonly IAuditContextAccessor _auditCtx;
    private readonly AppDbContext _db;

    public OrderService(IAuditContextAccessor auditCtx, AppDbContext db)
    {
        _auditCtx = auditCtx;
        _db = db;
    }

    public async Task PlaceOrder(OrderDto dto)
    {
        // Set custom fields — fluent API, set once per scope
        _auditCtx.Context
            .SetField("Department", "Finance")
            .SetField("Region", "EU-West")
            .SetComment("Quarterly price adjustment per management approval")
            .SetTag("batch-import")
            .SetTag("high-priority");

        _db.Orders.Add(new Order { /* ... */ });
        await _db.SaveChangesAsync();
        // → Audit entry will include:
        // CustomFields = {"Department":"Finance","Region":"EU-West",
        //                 "_comment":"Quarterly price adjustment...",
        //                 "_tags":"batch-import;high-priority"}
    }
}

AuditContext API

Method Description
SetField(key, value) Set a custom key-value field
GetField(key) Get a field value by key
RemoveField(key) Remove a field
SetTag(tag) Add a categorization tag
SetComment(text) Set a human-readable reason (stored as _comment field)
GetAllFields() Get a snapshot of all fields + tags
Clear() Clear all fields and tags

Note: AuditContext is scoped — one instance per DI scope (one per HTTP request in web apps). All SaveChanges calls within the same scope will include the custom fields.

📊 What Gets Tracked

Audit Log Entry (observer.audit_log)

Field Description
id Auto-increment primary key
idempotency_key Unique Guid for deduplication (unique index)
timestamp_utc When the change occurred (UTC)
local_timestamp When the change occurred (server time)
operation INSERT / UPDATE / DELETE
source Application (EF Core) or Database (trigger)
schema_name Database schema
table_name Table name
row_id Primary key of affected row
entity_type .NET CLR type name
transaction_id DB transaction ID (for grouping)
application_user Who made the change (from auth context)
db_user SQL user that executed the query
method_name API endpoint / method name
activity_id Distributed tracing Activity ID
trace_identifier HTTP request trace ID
correlation_id Correlation ID for request chaining
client_ip Client IP address
user_agent Client User-Agent
additional_data Custom metadata from IAuditMetadataProvider (JSON)
custom_fields Custom fields from AuditContext (JSON)

Audit Detail (observer.audit_detail)

Field Description
column_name Name of the changed column (or [AuditOverride] override)
display_name Human-readable name from [AuditOverride]
old_value Previous value (null for INSERT, masked if [AuditMask])
new_value New value (null for DELETE, masked if [AuditMask])
data_type Column data type

🔑 Idempotency

Every audit log entry is assigned a unique IdempotencyKey (Guid) when it is created by the interceptor. This key is used to prevent duplicate entries across:

  • Polly retries — If a DB write fails and is retried, the key ensures no duplicate rows are inserted.
  • Message redelivery — If a message broker redelivers an event, consumers can check the key for deduplication.
  • Outbox reprocessing — If the outbox delivery service reprocesses a message, the key prevents duplicates.

The key is enforced with a unique index on the observer.audit_log table.

// The IdempotencyKey is automatically generated — no configuration needed.
// It flows through the entire pipeline: Interceptor → IAuditStore → IAuditEventPublisher

// On the consumer side, use it for deduplication:
public class AuditEventConsumer : IConsumer<AuditLogCreatedEvent>
{
    public async Task Consume(ConsumeContext<AuditLogCreatedEvent> context)
    {
        var key = context.Message.IdempotencyKey;
        
        // Check if already processed
        if (await _repository.ExistsAsync(key))
            return; // Skip duplicate
        
        // Process the audit event...
        await _repository.SaveAsync(context.Message);
    }
}

🔒 Immutability & Protection

Audit records are append-only. Protection is enforced at two levels:

  1. EF Core Level: AuditProtectionInterceptor blocks DELETE/UPDATE on audit entities
  2. Database Level: Protection triggers reject DELETE/UPDATE on audit tables

Only the retention service (with a special session variable) can clean up old records.

⚙️ Configuration Options

builder.Services.AddObserver(options =>
{
    // Audit mode
    options.AuditMode = AuditMode.OptOut;       // Default: OptOut (all entities audited)
    // options.AuditMode = AuditMode.OptIn;      // Only [AuditInclude] entities

    // Schema & table names
    options.AuditSchema = "observer";           // Default: "observer"
    options.AuditLogTableName = "audit_log";    // Default: "audit_log"
    options.AuditDetailTableName = "audit_detail"; // Default: "audit_detail"

    // Delivery mode
    options.DeliveryMode = AuditDeliveryMode.DatabaseOnly; // Default
    // options.DeliveryMode = AuditDeliveryMode.EventBusOnly;
    // options.DeliveryMode = AuditDeliveryMode.Both;

    // Auto-setup
    options.AutoCreateTables = true;            // Default: true
    options.AutoCreateTriggers = true;          // Default: true

    // Retention (global default)
    options.EnableRetention = false;            // Default: false
    options.RetentionPeriod = TimeSpan.FromDays(365);
    options.RetentionCheckInterval = TimeSpan.FromHours(24);

    // Retention per table (overrides global)
    options.TableRetentionOverrides["public.orders"] = TimeSpan.FromDays(1825); // 5 years
    options.TableRetentionOverrides["public.logs"] = TimeSpan.FromDays(90);      // 90 days

    // Protection
    options.ProtectAuditTables = true;          // Default: true

    // Metadata
    options.IncludeClientIp = true;             // Default: true
    options.IncludeUserAgent = false;           // Default: false

    // Resilience (Polly)
    options.Resilience.Enabled = true;                // Default: false
    options.Resilience.MaxRetryAttempts = 3;           // Default: 3
    options.Resilience.BaseDelay = TimeSpan.FromMilliseconds(200); // Default: 200ms
    options.Resilience.EnableCircuitBreaker = true;    // Default: false

    // Exclude sensitive columns globally
    options.ExcludedColumns.Add("CreditCardNumber");
    options.ExcludedColumns.Add("SSN");
});

🗄️ Database Providers

PostgreSQL

builder.Services.AddObserverPostgreSql("Host=localhost;Database=mydb;...");

Uses SET LOCAL for session context and PL/pgSQL functions for triggers.

SQL Server

builder.Services.AddObserverSqlServer("Server=localhost;Database=mydb;...");

Uses sp_set_session_context / SESSION_CONTEXT() and T-SQL triggers.

Oracle

builder.Services.AddObserverOracle("Data Source=...;User Id=...;Password=...;");

Uses DBMS_SESSION.SET_CONTEXT / SYS_CONTEXT() and PL/SQL triggers.

MySQL

builder.Services.AddObserverMySql("Server=localhost;Database=mydb;...");

Uses @user_defined_variables and separate INSERT/UPDATE/DELETE triggers.

🛡️ Resilience (Polly)

Observer includes built-in Polly v8 resilience support for audit operations. When enabled, all audit store operations (DB writes and/or event publishing) are automatically wrapped with:

  • Retry with exponential backoff + jitter
  • Circuit Breaker (optional) to prevent cascading failures
builder.Services.AddObserver(options =>
{
    options.Resilience.Enabled = true;

    // Retry configuration
    options.Resilience.MaxRetryAttempts = 3;                        // Default: 3
    options.Resilience.BaseDelay = TimeSpan.FromMilliseconds(200);  // Default: 200ms
    options.Resilience.MaxDelay = TimeSpan.FromSeconds(5);          // Default: 5s

    // Circuit breaker (optional)
    options.Resilience.EnableCircuitBreaker = true;                 // Default: false
    options.Resilience.CircuitBreakerFailureThreshold = 0.5;        // 50% failure rate
    options.Resilience.CircuitBreakerSamplingDuration = TimeSpan.FromSeconds(30);
    options.Resilience.CircuitBreakerBreakDuration = TimeSpan.FromSeconds(15);
    options.Resilience.CircuitBreakerMinimumThroughput = 10;
});

The resilience pipeline is applied as a decorator around whatever IAuditStore is active (EfAuditStore, CompositeAuditStore), so it works with all delivery modes. Combined with IdempotencyKey, retries are fully safe — duplicate entries are automatically filtered out.

📡 Message Queue Integration (MassTransit)

Observer can publish audit events to message brokers like RabbitMQ, Kafka, or Azure Service Bus using MassTransit. You can choose to write to the database only, publish events only, or do both.

Install Packages

# Core + DB provider + MassTransit
dotnet add package Appouse.Observer
dotnet add package Appouse.Observer.PostgreSql       # or SqlServer, Oracle, MySql
dotnet add package Appouse.Observer.MassTransit

# Optional: Transactional Outbox for reliable delivery
dotnet add package Appouse.Observer.MassTransit.Outbox

Delivery Modes

Mode Database Event Bus Use Case
DatabaseOnly Default. Local audit trail only
EventBusOnly Centralized audit via message broker
Both Local trail + real-time event streaming

Example: DB + RabbitMQ

using Appouse.Observer.Extensions;
using Appouse.Observer.PostgreSql.Extensions;
using Appouse.Observer.MassTransit.Extensions;
using Appouse.Observer.Models;

var builder = WebApplication.CreateBuilder(args);

// 1. Observer core — write to both DB and event bus
builder.Services.AddObserver(options =>
{
    options.DeliveryMode = AuditDeliveryMode.Both;
    options.AutoCreateTables = true;
    options.AutoCreateTriggers = true;
    options.Resilience.Enabled = true;  // Retry on failures
});

builder.Services.AddObserverPostgreSql(
    builder.Configuration.GetConnectionString("DefaultConnection")!);

// 2. MassTransit with RabbitMQ
builder.Services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.ConfigureEndpoints(context);
    });
});

// 3. Connect Observer to MassTransit
builder.Services.AddObserverMassTransit();

Example: RabbitMQ with Transactional Outbox

For guaranteed delivery, use the Outbox pattern. Messages are stored in the database first and delivered by a background service:

using Appouse.Observer.MassTransit.Outbox.Extensions;

// 1. Observer core
builder.Services.AddObserver(options =>
{
    options.DeliveryMode = AuditDeliveryMode.Both;
    options.Resilience.Enabled = true;
});
builder.Services.AddObserverPostgreSql(connectionString);

// 2. MassTransit with RabbitMQ + EF Core Outbox
builder.Services.AddMassTransit(x =>
{
    x.AddEntityFrameworkOutbox<AuditDbContext>(o =>
    {
        o.UsePostgres();   // or UseSqlServer(), UseMySql()
        o.UseBusOutbox();  // Enable Bus Outbox for publish scope
    });

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.ConfigureEndpoints(context);
    });
});

// 3. Observer MassTransit + Outbox
builder.Services.AddObserverMassTransit();
builder.Services.AddObserverMassTransitOutbox();

Example: Event Bus Only (No DB)

builder.Services.AddObserver(options =>
{
    options.DeliveryMode = AuditDeliveryMode.EventBusOnly;
});

builder.Services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) => cfg.Host("rabbitmq://localhost"));
});

builder.Services.AddObserverMassTransit();

Example: Kafka

builder.Services.AddMassTransit(x =>
{
    x.UsingInMemory();
    x.AddRider(rider =>
    {
        rider.AddProducer<AuditLogCreatedEvent>("audit-events-topic");
        rider.UsingKafka((ctx, k) => k.Host("localhost:9092"));
    });
});

Consuming Audit Events

Any service can consume the published events:

public class AuditEventConsumer : IConsumer<AuditLogCreatedEvent>
{
    public async Task Consume(ConsumeContext<AuditLogCreatedEvent> context)
    {
        var audit = context.Message;

        // Use IdempotencyKey for deduplication
        if (await _repo.ExistsAsync(audit.IdempotencyKey))
            return;

        Console.WriteLine($"[{audit.Operation}] {audit.SchemaName}.{audit.TableName} " +
                          $"by {audit.ApplicationUser} at {audit.TimestampUtc}");

        foreach (var detail in audit.Details)
        {
            Console.WriteLine($"  {detail.ColumnName}: {detail.OldValue} → {detail.NewValue}");
        }

        await _repo.SaveAsync(audit);
    }
}

🔧 Custom Metadata Provider

Override the default metadata provider for custom scenarios:

public class CustomMetadataProvider : IAuditMetadataProvider
{
    public string? GetApplicationUser() => "custom-user";
    public string? GetMethodName() => "custom-method";
    public string? GetActivityId() => Activity.Current?.Id;
    public string? GetTraceIdentifier() => null;
    public string? GetCorrelationId() => "custom-correlation";
    public string? GetClientIp() => null;
    public string? GetUserAgent() => null;
    public IDictionary<string, string>? GetAdditionalData() => 
        new Dictionary<string, string> { ["TenantId"] = "tenant-1" };
}

// Register before AddObserver
builder.Services.AddSingleton<IAuditMetadataProvider, CustomMetadataProvider>();

🏗️ Architecture

                    ┌──────────────────────────────────────┐
                    │         AuditChangeCollector          │  ← shared audit engine
                    │  CollectAuditEntries() / FlushAsync() │
                    └──────────┬───────────────┬────────────┘
                               │               │
              ┌────────────────▼───┐   ┌───────▼──────────────────────┐
              │ SaveChanges        │   │ ObserverDbContextBase<T>     │
              │ Interceptor        │   │ (all EF Core versions)       │
              │ (.NET 5+ only)     │   │ SaveChanges() override       │
              └────────────────────┘   └──────────────────────────────┘
                               │               │
                               ▼               ▼
                      ResilientAuditStore (Polly)
                               │
                        CompositeAuditStore
                               │
                    ┌──────────┴──────────┐
                    ▼                     ▼
              EfAuditStore         IAuditEventPublisher
              (DB + Idempotency)   (MassTransit Publish)
                                         │
                          ┌──────────────┴──────────────┐
                          ▼                             ▼
                    Direct Publish              Outbox Pattern
                    (fire & forget)            (transactional)
                          │                             │
                          ▼                             ▼
                    ┌─────────┐                ┌──────────────┐
                    │RabbitMQ  │                │  DB Outbox   │
                    │Kafka     │                │    Table     │
                    │Azure SB  │                │      │       │
                    │InMemory  │                │      ▼       │
                    └─────────┘                │  Delivery    │
                                               │  Service → MQ │
                                               └──────────────┘

    Direct SQL / Other Apps  ──→  Database  ──→  DB Triggers  ──→  Audit Tables
  • AuditChangeCollector: Shared engine that scans ChangeTracker, builds audit entries, flushes to store. Used by both integration approaches.
  • SaveChangesInterceptor (.NET 5+): Hooks into EF Core's interceptor pipeline — no code changes to your DbContext.
  • ObserverDbContextBase (all frameworks): Base class that overrides SaveChanges/SaveChangesAsync — the only option on .NET Standard 2.0 / EF Core 3.x.
  • DB Triggers: Captures ALL changes (including raw SQL), reads session context set by the application layer.
  • AuditContext: Scoped service for attaching custom fields, tags, and comments per request.
  • CompositeAuditStore: Routes audit entries to DB, event bus, or both based on DeliveryMode.
  • ResilientAuditStore: Polly decorator providing retry + circuit breaker around any store.
  • IdempotencyKey: Guid assigned once, enforced with unique DB index, carried through events.

⬆️ Migrating from IAuditableEntity

If you previously used IAuditableEntity, it still works — the interface is treated as an implicit [AuditInclude]. However, it is now [Obsolete] and will be removed in a future major version.

// ❌ Old approach (still works but deprecated)
public class Product : IAuditableEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// ✅ New approach — OptOut mode (default): nothing needed
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// ✅ New approach — OptIn mode: use [AuditInclude]
[AuditInclude]
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

📦 Build & Pack

# Build
dotnet build Appouse.Observer.sln -c Release

# Run tests
dotnet test Appouse.Observer.sln -c Release

# Pack all packages (or use pack.bat)
dotnet pack Appouse.Observer.sln -c Release -o ./artifacts/packages

# Or simply:
.\pack.bat

# Push to local feed
dotnet nuget push ./artifacts/packages/*.nupkg -s C:\LocalNuGetFeed

# Push to NuGet.org
dotnet nuget push ./artifacts/packages/*.nupkg -s https://api.nuget.org/v3/index.json -k YOUR_API_KEY

🎯 Framework Compatibility Matrix

Feature .NET Standard 2.0 .NET 5 .NET 6 .NET 8 .NET 9
Core audit engine
ObserverDbContextBase<T>
SaveChangesInterceptor
AuditProtectionInterceptor
SqlQueryRaw<T> (table check) ❌¹ ❌¹ ❌¹
DB Triggers
MassTransit integration
MassTransit Outbox
Polly resilience
Retention service

¹ Falls back to ADO.NET DbCommand.ExecuteScalarAsync automatically.

EF Core & Provider Versions

TFM EF Core Npgsql SqlServer Pomelo (MySQL) Oracle
netstandard2.0 3.1.32 3.1.18 3.1.32 3.2.7 3.19.180
net5.0 5.0.17 5.0.10 5.0.17 5.0.4 5.21.90
net6.0 6.0.36 6.0.29 6.0.36 6.0.3 6.21.90
net8.0 / net9.0 8.0.11 8.0.11 8.0.11 8.0.2 8.23.60

📁 Project Structure

Appouse.Observer/
├── src/
│   ├── Appouse.Observer/                    # Core library (attributes, interceptor, models, Polly)
│   │   ├── Abstractions/                    # Interfaces, attributes, contracts
│   │   ├── Compatibility/                   # Polyfills for older frameworks
│   │   ├── Configuration/                   # ObserverOptions, resilience options
│   │   ├── Extensions/                      # DI extensions, ObserverDbContextBase
│   │   ├── Interceptors/                    # AuditChangeCollector, SaveChangesInterceptor
│   │   ├── Models/                          # AuditLogEntry, AuditLogDetail, enums
│   │   ├── Protection/                      # AuditProtectionInterceptor
│   │   ├── Providers/                       # DefaultAuditMetadataProvider
│   │   ├── Retention/                       # RetentionHostedService
│   │   └── Store/                           # AuditDbContext, EfAuditStore, ResilientAuditStore
│   ├── Appouse.Observer.PostgreSql/         # PostgreSQL provider
│   ├── Appouse.Observer.SqlServer/          # SQL Server provider
│   ├── Appouse.Observer.Oracle/             # Oracle provider
│   ├── Appouse.Observer.MySql/              # MySQL provider
│   ├── Appouse.Observer.MassTransit/        # MassTransit messaging
│   └── Appouse.Observer.MassTransit.Outbox/ # Transactional Outbox (net8.0+ only)
├── tests/
│   ├── Appouse.Observer.UnitTests/
│   └── Appouse.Observer.IntegrationTests/
├── samples/
│   └── Appouse.Observer.Sample/
├── docs/
│   └── ARCHITECTURE.md
└── pack.bat                                 # NuGet package build script

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🤝 Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Appouse.Observer:

Package Downloads
Appouse.Observer.MassTransit

MassTransit integration for Appouse.Observer audit trail library. Publishes audit events to message brokers (RabbitMQ, Kafka, Azure Service Bus) with optional Transactional Outbox support.

Appouse.Observer.MySql

MySQL provider for Appouse.Observer audit trail library. Provides automatic trigger creation and management for MySQL databases.

Appouse.Observer.Oracle

Oracle provider for Appouse.Observer audit trail library. Provides automatic trigger creation and management for Oracle databases.

Appouse.Observer.SqlServer

SQL Server provider for Appouse.Observer audit trail library. Provides automatic trigger creation and management for SQL Server databases.

Appouse.Observer.PostgreSql

PostgreSQL provider for Appouse.Observer audit trail library. Provides automatic trigger creation and management for PostgreSQL databases.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.2 367 4/13/2026
1.2.0 346 4/13/2026
1.1.0 244 4/10/2026
1.0.0 206 4/4/2026