![]() |
VOOZH | about |
dotnet add package Pandatech.EFCore.AuditBase --version 5.0.0
NuGet\Install-Package Pandatech.EFCore.AuditBase -Version 5.0.0
<PackageReference Include="Pandatech.EFCore.AuditBase" Version="5.0.0" />
<PackageVersion Include="Pandatech.EFCore.AuditBase" Version="5.0.0" />Directory.Packages.props
<PackageReference Include="Pandatech.EFCore.AuditBase" />Project file
paket add Pandatech.EFCore.AuditBase --version 5.0.0
#r "nuget: Pandatech.EFCore.AuditBase, 5.0.0"
#:package Pandatech.EFCore.AuditBase@5.0.0
#addin nuget:?package=Pandatech.EFCore.AuditBase&version=5.0.0Install as a Cake Addin
#tool nuget:?package=Pandatech.EFCore.AuditBase&version=5.0.0Install as a Cake Tool
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.
CreatedAt, CreatedByUserId, UpdatedAt, UpdatedByUserId, Deleted, Version
maintained on every entity that inherits AuditEntityBaseSaveChanges interceptor throws at runtime if a modified entity's Version was not
incremented, meaning someone bypassed MarkAsUpdated/MarkAsDeletedDeleted flag with MarkAsDeleted and a global query filter that hides deleted rows transparentlyVersion is decorated with [ConcurrencyCheck]; EF Core raises a concurrency
exception on conflict automaticallyExecuteSoftDeleteAsync and ExecuteUpdateAndMarkUpdatedAsync translate directly to
ExecuteUpdateAsync database calls while still maintaining correct audit fieldsMarkAsDeleted overload on IEnumerable<T> for cases where entities are already
trackeddotnet add package Pandatech.EFCore.AuditBase
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.
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.
product.MarkAsUpdated(userId);
// or with an explicit timestamp:
product.MarkAsUpdated(userId, updatedAt: syncedTime);
await dbContext.SaveChangesAsync(ct);
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.
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseNpgsql(connectionString)
.UseAuditBaseValidatorInterceptor());
UseAuditBaseValidatorInterceptor adds AuditPropertyValidationInterceptor to the context. It hooks into both
SavingChanges and SavingChangesAsync.
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);
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
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);
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:
ExecuteSoftDeleteAsyncandExecuteUpdateAndMarkUpdatedAsyncbypass EF Core's change tracker and do not raise concurrency exceptions. They incrementVersionunconditionally. Use them when bulk throughput matters more than per-row conflict detection.
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 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.
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
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. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.0.0 | 343 | 2/28/2026 |
| 4.0.1 | 151 | 1/26/2026 |
| 4.0.0 | 133 | 12/28/2025 |
| 3.0.10 | 483 | 8/7/2025 |
| 3.0.9 | 307 | 6/1/2025 |
| 3.0.8 | 287 | 4/7/2025 |
| 3.0.7 | 273 | 4/4/2025 |
| 3.0.6 | 251 | 4/4/2025 |
| 3.0.5 | 220 | 4/4/2025 |
| 3.0.4 | 237 | 4/4/2025 |
| 3.0.3 | 311 | 3/12/2025 |
| 3.0.2 | 247 | 2/28/2025 |
| 3.0.1 | 230 | 2/17/2025 |
| 3.0.0 | 346 | 12/1/2024 |
| 2.0.0 | 306 | 11/21/2024 |
| 1.2.1 | 253 | 11/11/2024 |
| 1.2.0 | 215 | 10/17/2024 |
| 1.1.0 | 236 | 7/12/2024 |
| 1.0.3 | 253 | 6/7/2024 |
| 1.0.2 | 263 | 5/31/2024 |
Multi-target net9.0/net10.0, removed Npgsql dependency, framework-pinned EF Core references