Pandatech.EFCore.Audit is a powerful and configurable library designed to collect audit trail data from the EF Core
DbContext change tracker. It is built with scalability and professional-grade extensibility in mind.
- Scalable & Configurable: Tailor the behavior to meet your project's needs.
- Composite Key Handling: Returns concatenated composite keys in a single property using
_as the delimiter. - Property Transformation: Customize tracked properties (e.g., rename, transform, or ignore).
- Not atomic: Being event-based, there is a risk of losing audit data in edge cases.
- Does not work with untracked operations like
AsNoTracking,ExecuteUpdate,ExecuteDelete, etc. For such scenarios, use the new manual bulk audit feature described below.
Install the NuGet package:
dotnet add package Pandatech.EFCore.AuditTo integrate Pandatech.EFCore.Audit into your project, follow these steps:
Set up your DbContext to include your entities:
public class PostgresContext(DbContextOptions options) : DbContext(options)
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}Entities can be set up for auditing using custom configurations. Below are examples:
public class Blog
{
public int Id { get; set; }
public required string Title { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public BlogType BlogType { get; set; }
public required byte[] EncryptedKey { get; set; }
}
public class BlogAuditTrailConfiguration : AuditTrailConfigurator<Blog>
{
public BlogAuditTrailConfiguration()
{
SetReadPermission(Permission.UserPermission);
WriteAuditTrailOnEvents(AuditActionType.Create, AuditActionType.Update, AuditActionType.Delete);
RuleFor(s => s.EncryptedKey).Transform(Convert.ToBase64String);
}
}
public enum Permission
{
AdminPermission,
UserPermission
}public class Post
{
public int Id { get; set; }
public int BlogId { get; set; }
public required string Title { get; set; }
public required string Content { get; set; }
public Blog Blog { get; set; } = null!;
}
public class PostAuditConfiguration : AuditTrailConfigurator<Post>
{
public PostAuditConfiguration()
{
SetServiceName("BlogService");
RuleFor(s => s.Content).Ignore();
RuleFor(s => s.Title).Rename("TotallyNewTitle");
}
}- SetServiceName: Specifies a custom service name that will be returned during the audit trail event. This can be useful for identifying the origin of the change.
- SetReadPermission: Assigns a predefined permission level included in the event, enabling better control over who can access the audit information as row-level security.
- WriteAuditTrailOnEvents: Defines the specific events (
Create,Update,Delete) on which an entity should be tracked. If this option is not configured, all events for the entity will be tracked by default. - Exclusion of Configuration: If an entity should not be audited, its configuration should be omitted entirely. Entities without configuration will not be tracked.
- Transform: Allows you to apply a custom function to modify the value of a property before it is recorded in the audit trail. For example, this can be used to encrypt/decrypt or format data.
- Ignore: Skips tracking of the specified property within the entity. Useful for sensitive or irrelevant data.
- Rename: Changes the property name in the audit trail output. This is useful for aligning property names with business-specific terminology or conventions.
Register your DbContext and add the audit trail interceptors:
public static WebApplicationBuilder AddPostgresContext<TContext>(this WebApplicationBuilder builder,
string connectionString)
where TContext : DbContext
{
builder.Services.AddDbContextPool<TContext>((sp, options) =>
{
options
.UseNpgsql(connectionString)
.AddAuditTrailInterceptors(sp);
});
return builder;
}To handle audit trail events, create a consumer class that inherits from IAuditTrailConsumer and implements the
ConsumeAuditTrailAsync method. Implement this method to process audit trail events according to your application's
requirements — for example, logging the events, sending them to an external service, or storing them in a database.
Here is an example implementation that serializes the event data to JSON and writes it to the console:
public class AuditTrailConsumer : IAuditTrailConsumer
{
public Task ConsumeAuditTrailAsync(AuditTrailEventData auditTrailEventData)
{
Console.WriteLine(JsonSerializer.Serialize(auditTrailEventData));
return Task.CompletedTask;
}
}Below is a simplified example of how your Program.cs file might look:
var builder = WebApplication.CreateBuilder(args);
// Register audit trail configurations with the consumer class before adding the DbContext
builder.Services.AddAuditTrail<AuditTrailConsumer>(typeof(Program).Assembly);
// Register DbContext with audit trail interceptors
builder.AddPostgresContext<PostgresContext>(
"Server=localhost;Port=5432;Database=audit_test;User Id=test;Password=test;Pooling=true;");
var app = builder.Build();
app.Run();Note: The
AddAuditTrailmethod registers the audit trail configurations andHttpContextAccessorso it should be always before theAddDbContextmethod. In case of usingAddDbContextbefore registration make sure to register theHttpContextAccessormanually by usingbuilder.Services.AddHttpContextAccessor()method inProgram.cs.
The audit trail event data is represented by the following classes:
public record AuditTrailEventData(List<AuditTrailEventEntity> Entities);
public record AuditTrailEventEntity(
EntityEntry Entry,
string? ServiceName,
AuditActionType ActionType,
string EntityName,
object? ReadPermission,
string PrimaryKeyValue,
Dictionary<string, object?> TrackedProperties);- AuditTrailEventData: Contains a list of
AuditTrailEventEntityobjects. - AuditTrailEventEntity: Represents an audited entity with its associated data.
- Entry: The
EntityEntryobject containing the entity data fromDbContext. - ServiceName: The name of the service where the change originated. Configured manually using
SetServiceName. - ActionType: The type of action performed (
Create,Update,Delete). - EntityName: The name of the entity.
- ReadPermission: The assigned permission level for accessing this audit trail. Configured manually using
SetReadPermission. - PrimaryKeyValue: The primary key value(s) of the entity.
- TrackedProperties: A dictionary containing the tracked properties and their values.
- Entry: The
If you perform operations outside EF Core’s change tracker (e.g. AsNoTracking, ExecuteUpdate(), raw SQL queries,
external APIs, etc.), you can still create audit events by manually specifying:
AuditActionType(Create, Update, Delete)- Primary key value(s)
- The dictionary of changed properties relevant to the operation
This is exposed via a single IAuditTrailPublisher.BulkAuditAsync method (or similar) that accepts a list of manual
audit entries. Each entry contains:
public record ManualAuditEntry(
Type EntityType, // The CLR type you're auditing (e.g. Blog, Post)
AuditActionType Action, // Create, Update, or Delete
List<AuditEntryDetail> ChangedItems // One or more records to track
);
public record AuditEntryDetail(
List<string> PrimaryKeyIds, // e.g. ["10"] or ["10","20"] for composite PK
Dictionary<string, object?> ChangedProperties // developer-supplied property data
);public async Task CreatePublish()
{
var posts = new List<Post>();
var blog = new Blog
{
Id = 2,
Title = "null",
CreatedAt = DateTime.UtcNow,
BlogType = BlogType.Personal,
EncryptedKey = [1, 2, 3]
};
for (var i = 0; i < 10; i++)
{
var post = new Post
{
Title = $"Post {i}",
Content = $"This is post {i}",
Blog = blog
};
posts.Add(post);
}
var auditEntries = new List<ManualAuditEntry>
{
new(
typeof(Blog),
AuditActionType.Create,
[
new AuditEntryDetail(
PrimaryKeyIds: [blog.Id.ToString()],
ChangedProperties: new Dictionary<string, object?>
{
[nameof(blog.Id)] = blog.Id,
[nameof(blog.Title)] = blog.Title,
[nameof(blog.CreatedAt)] = blog.CreatedAt,
[nameof(blog.BlogType)] = blog.BlogType,
[nameof(blog.EncryptedKey)] = blog.EncryptedKey
}
)
]
)
};
var postDetails = new List<AuditEntryDetail>();
foreach (var p in posts)
{
postDetails.Add(
new AuditEntryDetail(
PrimaryKeyIds: [p.Id.ToString()],
ChangedProperties: new Dictionary<string, object?>
{
[nameof(p.Title)] = p.Title,
[nameof(p.Content)] = p.Content,
[nameof(p.BlogId)] = p.BlogId
}
)
);
}
auditEntries.Add(
new ManualAuditEntry(
EntityType: typeof(Post),
Action: AuditActionType.Create,
ChangedItems: postDetails
)
);
await auditPublisher.BulkAuditAsync(auditEntries); // IAuditTrailPublisher.BulkAuditAsync
}When you call BulkAuditAsync, the library will apply any configured transformations (ignore, rename, transform) and
publish the resulting audit trail event.
- Partial Property Tracking: For
Updateactions,TrackedPropertiesonly includes properties that have been modified. - Event Handling: The provided
Console.WriteLinein the demo is a placeholder. You are responsible for implementing your own event handling logic. - Database Compatibility: Compatible with PostgreSQL and other relational databases supported by EF Core.
- **Compatible with
.Net 9 +
This project is licensed under the MIT License.