How to Implement Audit Trail in ASP.NET Core with EF Core (2024)

In modern web applications, tracking changes to data can be needed for monitoring, compliance, and debugging reasons.This process, known as creating audit trails, allows developers to see who made changes, when they were made, and what the changes were.Audit trails provide a historical record of changes made to data.

In this blog post, I will show how to implement an audit trail in an ASP.NET Core application using Entity Framework Core (EF Core).

Application We Will Be Auditing

Today we will implement audit trails for the "Books" application that has the following entities:

  • Books
  • Authors
  • Users

I find it useful to include the following properties in all entities that need to be audited:

csharp

public interface IAuditableEntity{ DateTime CreatedAtUtc { get; set; } DateTime? UpdatedAtUtc { get; set; } string CreatedBy { get; set; } string? UpdatedBy { get; set; }}

We need to inherit all our auditable entities from this interface, for example, User and Book:

csharp

public class User : IAuditableEntity{ public Guid Id { get; set; } public required string Email { get; set; } public DateTime CreatedAtUtc { get; set; } public DateTime? UpdatedAtUtc { get; set; } public string CreatedBy { get; set; } = null!; public string? UpdatedBy { get; set; }}public class Book : IAuditableEntity{ public required Guid Id { get; set; } public required string Title { get; set; } public required int Year { get; set; } public Guid AuthorId { get; set; } public Author Author { get; set; } = null!; public DateTime CreatedAtUtc { get; set; } public DateTime? UpdatedAtUtc { get; set; } public string CreatedBy { get; set; } = null!; public string? UpdatedBy { get; set; }}

Now we have a few options, we can implement audit trails manually for each entity or have one implementation that automatically applies to all the entities.In this blog post, I will show you the second option, as it is more robust and easier to maintain.

Configuring Audit Trails Entity in EF Core

The first step in implementing an audit trail is to create an entity that will store the audit logs in a separate database table.This entity should capture details such as the entity type, primary key, a list of changed properties, old values, new values, and the timestamp of the change.

csharp

public class AuditTrail{ public required Guid Id { get; set; } public Guid? UserId { get; set; } public User? User { get; set; } public TrailType TrailType { get; set; } public DateTime DateUtc { get; set; } public required string EntityName { get; set; } public string? PrimaryKey { get; set; } public Dictionary<string, object?> OldValues { get; set; } = []; public Dictionary<string, object?> NewValues { get; set; } = []; public List<string> ChangedColumns { get; set; } = [];}

Here we have a reference to a User entity.Depending on your application needs, you may have this reference or not.

Every audit trail can be of the following types:

  • Entity was created
  • Entity was updated
  • Entity was deleted

csharp

public enum TrailType : byte{ None = 0, Create = 1, Update = 2, Delete = 3}

Let's have a look at how to configure an audit trail entity in EF Core:

csharp

public class AuditTrailConfiguration : IEntityTypeConfiguration<AuditTrail>{ public void Configure(EntityTypeBuilder<AuditTrail> builder) { builder.ToTable("audit_trails"); builder.HasKey(e => e.Id); builder.HasIndex(e => e.EntityName); builder.Property(e => e.Id); builder.Property(e => e.UserId); builder.Property(e => e.EntityName).HasMaxLength(100).IsRequired(); builder.Property(e => e.DateUtc).IsRequired(); builder.Property(e => e.PrimaryKey).HasMaxLength(100); builder.Property(e => e.TrailType).HasConversion<string>(); builder.Property(e => e.ChangedColumns).HasColumnType("jsonb"); builder.Property(e => e.OldValues).HasColumnType("jsonb"); builder.Property(e => e.NewValues).HasColumnType("jsonb"); builder.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .IsRequired(false) .OnDelete(DeleteBehavior.SetNull); }}

I like using json columns to express ChangedColumns, OldValues, and NewValues.In this blog post, in my code example, I use a Postgres database.

If you're using SQLite or another database that doesn't support json columns - you can use string types in your entity and create a EF Core Conversion that serializes an object to a string to save it in a database.When retrieving data from the database, this Conversion will deserialize a JSON string into a corresponding .NET type.

In Postgres database, when using NET 8 and EF 8 you need to EnableDynamicJson in order to be able to have a dynamic json in "jsonb" columns:

csharp

var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);dataSourceBuilder.EnableDynamicJson();builder.Services.AddDbContext<ApplicationDbContext>((provider, options) =>{ var interceptor = provider.GetRequiredService<AuditableInterceptor>(); options.EnableSensitiveDataLogging() .UseNpgsql(dataSourceBuilder.Build(), npgsqlOptions => { npgsqlOptions.MigrationsHistoryTable("__MyMigrationsHistory", "devtips_audit_trails"); }) .AddInterceptors(interceptor) .UseSnakeCaseNamingConvention();});

Implementing Audit Trails for all Auditable Entities

We can implement an auditing in EF Core DbContext that will automatically be applied to all entities that inherit from IAuditableEntity.But first we need to get a user that is performing create, update or delete actions on these entities.

Let's define a CurrentSessionProvider that will retrieve current user identifier from the ClaimsPrinciple of a current HttpRequest:

csharp

public interface ICurrentSessionProvider{ Guid? GetUserId();}public class CurrentSessionProvider : ICurrentSessionProvider{ private readonly Guid? _currentUserId; public CurrentSessionProvider(IHttpContextAccessor accessor) { var userId = accessor.HttpContext?.User.FindFirstValue("userid"); if (userId is null) { return; } _currentUserId = Guid.TryParse(userId, out var guid) ? guid : null; } public Guid? GetUserId() => _currentUserId;}

You need to register the provider and IHttpContextAccessor in the DI:

csharp

builder.Services.AddHttpContextAccessor();builder.Services.AddScoped<ICurrentSessionProvider, CurrentSessionProvider>();

To create the audit trails, we can use EF Core Changer Tracker capabilities to get entities that are created, updated or deleted.

We need to inject ICurrentSessionProvider into DbContext and override SaveChangesAsync method to create audit trails.

csharp

public class ApplicationDbContext( DbContextOptions<ApplicationDbContext> options, ICurrentSessionProvider currentSessionProvider) : DbContext(options){ public ICurrentSessionProvider CurrentSessionProvider => currentSessionProvider; public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new()) { var userId = CurrentSessionProvider.GetUserId(); SetAuditableProperties(userId); var auditEntries = HandleAuditingBeforeSaveChanges(userId).ToList(); if (auditEntries.Count > 0) { await AuditTrails.AddRangeAsync(auditEntries, cancellationToken); } return await base.SaveChangesAsync(cancellationToken); }}

Note, that we are creating AuditTrails before calling base.SaveChangesAsync to make sure that we persist all changes to the database in a single transaction.

In the code above we are performing two operations:

  • setting auditable properties to the created, updated or deleted records
  • creating audit trail records

For all entities that inherit from IAuditableEntity we set Created and Updated fields.In some cases changes might not be triggered by a user, but rather a code.In such cases we set that a "system" performed changes.

For example, this can be a background job, database seeding, etc.

csharp

private void SetAuditableProperties(Guid? userId){ const string systemSource = "system"; foreach (var entry in ChangeTracker.Entries<IAuditableEntity>()) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedAtUtc = DateTime.UtcNow; entry.Entity.CreatedBy = userId?.ToString() ?? systemSource; break; case EntityState.Modified: entry.Entity.UpdatedAtUtc = DateTime.UtcNow; entry.Entity.UpdatedBy = userId?.ToString() ?? systemSource; break; } }}

Now let's have a look at how to create audit trail records.Again we're iterating through IAuditableEntity entities and select those that were created, updated or deleted:

csharp

private List<AuditTrail> HandleAuditingBeforeSaveChanges(Guid? userId){ var auditableEntries = ChangeTracker.Entries<IAuditableEntity>() .Where(x => x.State is EntityState.Added or EntityState.Deleted or EntityState.Modified) .Select(x => CreateTrailEntry(userId, x)) .ToList(); return auditableEntries;}private static AuditTrail CreateTrailEntry(Guid? userId, EntityEntry<IAuditableEntity> entry){ var trailEntry = new AuditTrail { Id = Guid.NewGuid(), EntityName = entry.Entity.GetType().Name, UserId = userId, DateUtc = DateTime.UtcNow }; SetAuditTrailPropertyValues(entry, trailEntry); SetAuditTrailNavigationValues(entry, trailEntry); SetAuditTrailReferenceValues(entry, trailEntry); return trailEntry;}

An audit trail record can contain the following types of properties:

  • plain properties (like Book's Title or Year of Publication)
  • reference property (like Book's Author)
  • navigation property (like Author's Books)

Let's have a look at how to add plain properties to audit trails:

csharp

private static void SetAuditTrailPropertyValues(EntityEntry entry, AuditTrail trailEntry){ // Skip temp fields (that will be assigned automatically by ef core engine, for example: when inserting an entity foreach (var property in entry.Properties.Where(x => !x.IsTemporary)) { if (property.Metadata.IsPrimaryKey()) { trailEntry.PrimaryKey = property.CurrentValue?.ToString(); continue; } // Filter properties that should not appear in the audit list if (property.Metadata.Name.Equals("PasswordHash")) { continue; } SetAuditTrailPropertyValue(entry, trailEntry, property); }}private static void SetAuditTrailPropertyValue(EntityEntry entry, AuditTrail trailEntry, PropertyEntry property){ var propertyName = property.Metadata.Name; switch (entry.State) { case EntityState.Added: trailEntry.TrailType = TrailType.Create; trailEntry.NewValues[propertyName] = property.CurrentValue; break; case EntityState.Deleted: trailEntry.TrailType = TrailType.Delete; trailEntry.OldValues[propertyName] = property.OriginalValue; break; case EntityState.Modified: if (property.IsModified && (property.OriginalValue is null || !property.OriginalValue.Equals(property.CurrentValue))) { trailEntry.ChangedColumns.Add(propertyName); trailEntry.TrailType = TrailType.Update; trailEntry.OldValues[propertyName] = property.OriginalValue; trailEntry.NewValues[propertyName] = property.CurrentValue; } break; } if (trailEntry.ChangedColumns.Count > 0) { trailEntry.TrailType = TrailType.Update; }}

If you need to exclude any sensitive fields, you can do it here.For example, we are excluding PasswordHash property from audit trails.

Now let's explore how to add reference and navigation properties into audit trails:

csharp

private static void SetAuditTrailReferenceValues(EntityEntry entry, AuditTrail trailEntry){ foreach (var reference in entry.References.Where(x => x.IsModified)) { var referenceName = reference.EntityEntry.Entity.GetType().Name; trailEntry.ChangedColumns.Add(referenceName); }}private static void SetAuditTrailNavigationValues(EntityEntry entry, AuditTrail trailEntry){ foreach (var navigation in entry.Navigations.Where(x => x.Metadata.IsCollection && x.IsModified)) { if (navigation.CurrentValue is not IEnumerable<object> enumerable) { continue; } var collection = enumerable.ToList(); if (collection.Count == 0) { continue; } var navigationName = collection.First().GetType().Name; trailEntry.ChangedColumns.Add(navigationName); }}

Finally, we can run our application to see auditing in action.

Here is an example of auditing properties set by a system and by a user in the authors table:

How to Implement Audit Trail in ASP.NET Core with EF Core (1)

Here is how the audit_trails table looks like:

How to Implement Audit Trail in ASP.NET Core with EF Core (2)

How to Implement Audit Trail in ASP.NET Core with EF Core (3)

Hope you find this blog post useful. Happy coding!

How to Implement Audit Trail in ASP.NET Core with EF Core (2024)

FAQs

How to Implement Audit Trail in ASP.NET Core with EF Core? ›

The first step in implementing an audit trail is to create an entity that will store the audit logs in a separate database table. This entity should capture details such as the entity type, primary key, a list of changed properties, old values, new values, and the timestamp of the change.

How to implement audit trail in ASP.NET Core? ›

  1. Step 1: Create Database and related tables in MS SQL Server. ...
  2. Step 2: Create a asp.net core web api project name AuditLog.API.
  3. Step 3: Install the following nuget packages in the project. ...
  4. Step 4: Create a Model class name Product in Models folder.
Jan 15, 2024

How do you install an audit trail? ›

Navigate to C:\Program Files\Microsoft System Center\Orchestrator\Management Server .
  1. To activate the Audit Trail, enter atlc /enable.
  2. To deactivate the Audit Trail, enter atlc /disable.
Jul 10, 2024

How to implement an audit trail? ›

A: Implementing an audit trail involves defining your audit trail policy, identifying the tools you need, integrating the audit trail into your system, testing and validating, and monitoring and analyzing the audit trail data.

How to create an audit log in C#? ›

In the "CreateAuditTrail" method, we send in the following parameters.
  1. AuditActionType: Create/Delete/Update...
  2. KeyFieldID: Link to the table record this audit belongs to.
  3. OldObject / NewObject: the existing (database) and new (ViewModel) states of the data before saving the update to the database.
Jun 26, 2024

How to do an audit trail in Sage? ›

Run the Audit Trail Report
  1. Go to Reports.
  2. Select Audit Trail. ...
  3. To search for a transaction, enter the amount of the transaction or reference in the search box.
  4. To view the audit trail for a particular date range, from the Period drop-down, choose one of the following options:
Jan 23, 2024

What is an audit trail integration? ›

An audit trail is a step-by-step record by which accounting, trade details, or other financial data can be traced to their source. Audit trails are used to verify and track many types of transactions, including accounting transactions and trades in brokerage accounts.

What is the difference between audit trails and log files? ›

Granularity: Audit trails are incredibly detailed, capturing every action taken within a system, often down to the keystroke or mouse click level. In contrast, log files are typically less detailed and focus on recording system events and errors.

What is audit trail plugin? ›

Article Details. Content. The Audit Trail plugin is a free add-on that can be installed from the Plugin module. After installing the plugin, the Audit button is added to the toolbar of the Sales Order module, Purchase Order module, Part module, Product module, and. Work Order module.

Is audit log and audit trail the same? ›

A series of audit logs is called an audit trail because it shows a sequential record of all the activity on a specific system.

What is audit trail examples? ›

Audit trails (or audit logs) act as record-keepers that document evidence of certain events, procedures or operations, so their purpose is to reduce fraud, material errors, and unauthorized use. Even your grocery store receipt is an example of a logged audit trail.

What are the requirements for audit trail? ›

Companies must keep accurate transaction records, failing which can lead to penalties. Audit trails must be maintained for at least eight years, following best practices like data backups, user authentication, regular audits, and automated logging.

What are the disadvantages of using an audit trail? ›

It requires a considerable amount of time and money to institute practices and train employees to maintain audit trails. It does become easier when the audit trail is fully automated, but that also costs more money.

How to create a log file in asp net c#? ›

Write a Log file in . Net Application
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. namespace BackToBasicProj.
  8. {
Aug 16, 2016

How to perform logging in C#? ›

Get started
  1. Creates an ILoggerFactory. The ILoggerFactory stores all the configuration that determines where log messages are sent. ...
  2. Creates an ILogger with a category named "Program". ...
  3. Calls LogInformation to log a message at the Information level.
Jul 17, 2024

How to create an event log in C#? ›

Use the WriteEvent and WriteEntry methods to write events to an event log. You must specify an event source to write events; you must create and configure the event source before writing the first entry with the source. Create the new event source during the installation of your application.

How do you write an audit trail? ›

An audit trail should include the information needed to establish what events occurred and what person or system caused them. That event record would then have a time-stamp for the event, the user ID associated with it, the program or command that initiated the event, and the result.

How do you ensure a proper audit trail? ›

Verify accuracy: Ensure the recorded changes are accurate by comparing them with source documents and other supporting evidence. Retain records: Log and protect the audit trail records securely and confidentially, following relevant record retention policies and data protection regulations.

How to implement audit trail in SQL? ›

Enable the audit trail by selecting the Keep a record of all user activity in an audit trail database check box. Then select the activities you want to store in the audit trail database. Select the Comment required check box, if you want to force the user to enter a comment for this type of operation.

What is a setup audit trail? ›

The Setup Audit Trail feature in Salesforce enables you to closely track changes made to your organization. It records all modifications concerning the administration, customization, security, sharing, data management, development, and more of your Salesforce organization.

Top Articles
Latest Posts
Article information

Author: Roderick King

Last Updated:

Views: 5972

Rating: 4 / 5 (51 voted)

Reviews: 90% of readers found this page helpful

Author information

Name: Roderick King

Birthday: 1997-10-09

Address: 3782 Madge Knoll, East Dudley, MA 63913

Phone: +2521695290067

Job: Customer Sales Coordinator

Hobby: Gunsmithing, Embroidery, Parkour, Kitesurfing, Rock climbing, Sand art, Beekeeping

Introduction: My name is Roderick King, I am a cute, splendid, excited, perfect, gentle, funny, vivacious person who loves writing and wants to share my knowledge and understanding with you.