Pandatech.EFCore.AuditBase 5.0.0

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

Pandatech.EFCore.AuditBase

Auditing base for EF Core entities. Inherit one class and get automatic CreatedAt/UpdatedAt/UserId tracking, soft delete, optimistic concurrency via row versioning, bulk update/delete helpers, and a SaveChanges interceptor that enforces correct audit method usage at runtime.

Targets net8.0, net9.0, and net10.0.


Table of Contents

  1. Features
  2. Installation
  3. Getting Started
  4. AuditEntityBase
  5. Registering the Interceptor
  6. Soft Delete Query Filter
  7. Bulk Operations
  8. Concurrency Handling
  9. SyncAuditBase

Features

  • Automatic audit fieldsCreatedAt, CreatedByUserId, UpdatedAt, UpdatedByUserId, Deleted, Version maintained on every entity that inherits AuditEntityBase
  • Enforced audit methods — a SaveChanges interceptor throws at runtime if a modified entity's Version was not incremented, meaning someone bypassed MarkAsUpdated/MarkAsDeleted
  • Soft deleteDeleted flag with MarkAsDeleted and a global query filter that hides deleted rows transparently
  • Optimistic concurrencyVersion is decorated with [ConcurrencyCheck]; EF Core raises a concurrency exception on conflict automatically
  • Bulk helpersExecuteSoftDeleteAsync and ExecuteUpdateAndMarkUpdatedAsync translate directly to ExecuteUpdateAsync database calls while still maintaining correct audit fields
  • In-memory batch deleteMarkAsDeleted overload on IEnumerable<T> for cases where entities are already tracked

Installation

dotnet add package Pandatech.EFCore.AuditBase

Getting Started

Inherit AuditEntityBase in your entity:

public class Product : AuditEntityBase
{
    public long Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

That's enough to gain all audit properties. Wire up the interceptor and query filter as shown below.


AuditEntityBase

CreatedAt          DateTime       Set to UtcNow on construction. Never modified after that.
CreatedByUserId    long?          Required on construction (required init). Never modified after that.
UpdatedAt          DateTime?      Set by MarkAsUpdated / MarkAsDeleted.
UpdatedByUserId    long?          Set by MarkAsUpdated / MarkAsDeleted.
Deleted            bool           Set to true by MarkAsDeleted.
Version            int            Starts at 1. Incremented by every MarkAsUpdated / MarkAsDeleted call.

MarkAsUpdated

product.MarkAsUpdated(userId);
// or with an explicit timestamp:
product.MarkAsUpdated(userId, updatedAt: syncedTime);

await dbContext.SaveChangesAsync(ct);

MarkAsDeleted

product.MarkAsDeleted(userId);
await dbContext.SaveChangesAsync(ct);

Both methods increment Version. The interceptor validates this increment on every SaveChanges call — if you modify an audited entity's properties directly without calling MarkAsUpdated, the interceptor throws:

InvalidOperationException: Entity 'Product' was modified without calling MarkAsUpdated or MarkAsDeleted.

Registering the Interceptor

builder.Services.AddDbContextPool<AppDbContext>(options =>
    options.UseNpgsql(connectionString)
           .UseAuditBaseValidatorInterceptor());

UseAuditBaseValidatorInterceptor adds AuditPropertyValidationInterceptor to the context. It hooks into both SavingChanges and SavingChangesAsync.


Soft Delete Query Filter

Apply a global query filter in OnModelCreating to exclude soft-deleted rows from all queries automatically:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.FilterOutDeletedMarkedObjects();
}

FilterOutDeletedMarkedObjects iterates every entity type that inherits AuditEntityBase and applies .HasQueryFilter(e => !e.Deleted) to each one via expression trees.

To include deleted rows in a specific query:

var all = await dbContext.Products.IgnoreQueryFilters().ToListAsync(ct);

Bulk Operations

ExecuteSoftDeleteAsync

Soft-deletes all rows matching a query in a single UPDATE statement. Does not load entities into memory.

await dbContext.Products
    .Where(p => p.Price > 100)
    .ExecuteSoftDeleteAsync(userId, ct: ct);

Translates to:

UPDATE products
SET deleted = true, updated_at = NOW(), updated_by_user_id = @userId, version = version + 1
WHERE price > 100

ExecuteUpdateAndMarkUpdatedAsync

Updates arbitrary properties while automatically maintaining UpdatedAt, UpdatedByUserId, and Version:

await dbContext.Products
    .Where(p => p.Price > 100)
    .ExecuteUpdateAndMarkUpdatedAsync(
        userId,
        x => x.SetProperty(p => p.Price, p => p.Price * 0.9m),
        ct);

MarkAsDeleted (in-memory batch)

For already-tracked entities where you want to call SaveChanges once after marking several:

var products = await dbContext.Products.Where(p => p.Price > 100).ToListAsync(ct);
products.MarkAsDeleted(userId);
await dbContext.SaveChangesAsync(ct);

Optimistic locking note: ExecuteSoftDeleteAsync and ExecuteUpdateAndMarkUpdatedAsync bypass EF Core's change tracker and do not raise concurrency exceptions. They increment Version unconditionally. Use them when bulk throughput matters more than per-row conflict detection.


Concurrency Handling

Version is decorated with [ConcurrencyCheck]. When two requests load the same entity and both call SaveChanges, the second one gets a DbUpdateConcurrencyException because the Version in the database no longer matches what was read.

try
{
    product.MarkAsUpdated(userId);
    await dbContext.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
    // Reload and retry, or return a 409 to the caller.
}

SyncAuditBase

SyncAuditBase copies audit fields from one entity instance to another while bypassing the interceptor. It is intended for internal synchronization scenarios (e.g., merging detached entity state) and should not be used in normal update flows.

target.SyncAuditBase(source);
await dbContext.SaveChangesAsync(ct);

Setting IgnoreInterceptor = true via SyncAuditBase suppresses validation for all modified entities in that SaveChanges call, so use it only when you are certain the audit state being applied is already correct.


License

MIT

Product Compatible and additional computed target framework versions.
.NET 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 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 (1)

Showing the top 1 NuGet packages that depend on Pandatech.EFCore.AuditBase:

Package Downloads
Pandatech.SharedKernel.Postgres

PostgreSQL integration helpers for ASP.NET Core 10: DbContext registration with or without pooling and audit trail, migrations, health checks, snake_case naming, query locks, exception mapping, and bulk extensions.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.0 107 2/28/2026
4.0.1 127 1/26/2026
4.0.0 111 12/28/2025
3.0.10 459 8/7/2025
3.0.9 285 6/1/2025
3.0.8 269 4/7/2025
3.0.7 254 4/4/2025
3.0.6 230 4/4/2025
3.0.5 200 4/4/2025
3.0.4 219 4/4/2025
3.0.3 295 3/12/2025
3.0.2 230 2/28/2025
3.0.1 214 2/17/2025
3.0.0 324 12/1/2024
2.0.0 287 11/21/2024
1.2.1 234 11/11/2024
1.2.0 201 10/17/2024
1.1.0 221 7/12/2024
1.0.3 236 6/7/2024
1.0.2 242 5/31/2024
Loading failed

Multi-target net9.0/net10.0, removed Npgsql dependency, framework-pinned EF Core references