How to create Auditable Entities using Entity Framework Core

In this detailed article, let's look at how we can setup and work with Auditable Fields in a .NET 6 project using Entity Framework Core.

Introduction

Imagine you are working on a multi-user application where data is added and modified by more than one user over the time. To comply with regulations you wish to maintain details about who has created the record and who has updated it.

This is the basic concept behind creating Auditable Entities, where the records also contain generic information about the creation and modification by users.

A simple Auditable record in its simplest implementation contains four additional fields – CreatedBy, CreatedDate, LastModifiedBy, LastModifiedDate.

Why do you need Audit Fields?

The idea is to have these fields filled with information whenever the record is created and later modified. This gives us information about the users who have created and updated it at a record level.

But setting these fields for each and every entity in a very large database – for every operation manually is a tedious and inefficient process. To solve this, we can set these fields automatically just before the record is committed in the database.

In this article, let’s look at how we can use Entity Framework Core to automatically store Auditable Fields information for every operation in a most generic way possible.

The code snippets used in this article are a part of my fully solved .NET 6 boilerplate – ContainerNinja.CleanArchitecture. Please do take a look and leave a star if you like my work.

How to create and set Auditable Entities in .NET 6 with Entity Framework Core

To get started, let’s assume we have an Entity Item, which has the following fields –

namespace ContainerNinja.Contracts.Data.Entities
{
    public class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Categories { get; set; }
        public string ColorCode { get; set; }
    }
}

To make every record maintained for this Entity in the database, let’s start by adding a base class AuditableEntity. Every entity in the database will extend this base class and therefore will have the auditable fields created in the database. The class looks like below –

namespace ContainerNinja.Contracts.Data.Entities
{
    public abstract class BaseEntity
    {
        public virtual int Id { get; set; }
        public virtual DateTime Created { get; set; }
    }

    public abstract class AuditableEntity : BaseEntity
    {
        public virtual string CreatedBy { get; set; }
        public virtual string? ModifiedBy { get; set; }
        public virtual DateTime? LastModified { get; set; }
    }
}

Observe that the class AuditableEntity is abstract and extends another base class BaseEntity which has the primary key and a field “Created” that stores the CreatedDate.

The entity Item now extends AuditableEntity and therefore has all these properties (from BaseEntity and AuditableEntity) added by default.

namespace ContainerNinja.Contracts.Data.Entities
{
    public class Item : AuditableEntity
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public string Categories { get; set; }
        public string ColorCode { get; set; }
    }
}

The solution also stores the User information associated with the system in another table Users. The entity class looks like below –

using ContainerNinja.Contracts.Enum;

namespace ContainerNinja.Contracts.Data.Entities
{
    public class User : BaseEntity
    {
        public string EmailAddress { get; set; }
        public UserRole Role { get; set; }
        public string Password { get; set; }
    }
}

How to create Auditable Entities using Entity Framework Core

Since User entity extends only the BaseEntity, it only has Id and CreatedDate while Modified information is unnecessary for this case.

So now we have two entities Item and User which are of base types AuditableEntity and BaseEntity respectively. The requirement is to fill these auditable fields whenever an operation happens on these entities – particularly on the entity Item.

To achieve this, we will modify the DbContext class and override the implementation of SaveChanges() method. For starters, to commit changes to the database we call SaveChanges() method on the DbContext instance.

Since DbContext already has an implementation of the SaveChanges() we don’t override it. In this case, we override the SaveChanges() method and there we loop through all the records set to commit to the database. We can then set these auditable fields to the entities before writing them to the database!

The DbContext class looks like below –

using Microsoft.EntityFrameworkCore;
using ContainerNinja.Contracts.Data.Entities;
using ContainerNinja.Contracts.Services;

namespace ContainerNinja.Migrations
{
    public class DatabaseContext : DbContext
    {
        private readonly IUserService _user;

        public DatabaseContext(
            DbContextOptions<DatabaseContext> options, 
            IUserService user) : base(options)
        {
            ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            _user = user;
        }

        public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            // loop through all the entities under tracking
            // which are of type User
            foreach (var item in ChangeTracker.Entries<User>().AsEnumerable())
            {
                if (item.State == EntityState.Added)
                {
                    item.Entity.Created = DateTime.UtcNow;
                }
            }

            // loop through all the entities under tracking
            // which are of type AuditableEntity (Item is-a AuditableEntity, so it qualifies)
            foreach (var item in ChangeTracker.Entries<AuditableEntity>().AsEnumerable())
            {
                if (item.State == EntityState.Added)
                {
                    item.Entity.Created = DateTime.UtcNow;
                    item.Entity.CreatedBy = _user.UserId;
                }
                else if (item.State == EntityState.Modified)
                {
                    item.Entity.LastModified = DateTime.UtcNow;
                    item.Entity.ModifiedBy = _user.UserId;
                }
            }

            return base.SaveChangesAsync(cancellationToken);
        }

        public DbSet<Item> Items { get; set; }
        public DbSet<User> Users { get; set; }
    }
}

The below section is where the expected behavior happens –

// loop through all the entities under tracking
// which are of type AuditableEntity (Item is-a AuditableEntity, so it qualifies)
foreach (var item in ChangeTracker.Entries<AuditableEntity>().AsEnumerable())
{
    if (item.State == EntityState.Added)
    {
        item.Entity.Created = DateTime.UtcNow;
        item.Entity.CreatedBy = _user.UserId;
    }
    else if (item.State == EntityState.Modified)
    {
        item.Entity.LastModified = DateTime.UtcNow;
        item.Entity.ModifiedBy = _user.UserId;
    }
}

In the above code snippet, we loop through all the records which are being tracked by EntityFrameworkCore. EFCore exposes property ChangeTracker which “Provides access to information and operations for entity instances this context is tracking“.

When we call ChangeTracker.Entities<TClass>() with a type parameter, it returns all the entities currently tracked which are of the type passed.

In our case, we passed AuditableEntity which results in all entities which are of type AuditableEntity.

Remember that Item extends AuditableEntity so it qualifies the “is-a” relationship.

So we get all the Item objects. Since User extends BaseEntity and NOT AuditableEntity, User is not a AuditableEntity and so those entities are not returned. Simple!

Within the loop, we see if the record is being added or updated based on the State – EntityState.Added or EntityState.Modified. If it is INSERT we set values for Created and CreatedBy. If UPDATE we set values of LastModified and ModifiedBy.

The User related information is obtained from the IUserService which is injected from within the constructor (yes, we can inject via constructor – it works).

The IUserService looks like below. It pulls the current User information based on the HttpContext set by the Token.

using ContainerNinja.Contracts.Services;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace ContainerNinja.Core.Services
{
    public class UserService : IUserService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UserService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string UserId
        {
            get
            {
                var context = _httpContextAccessor.HttpContext;
                if (context?.User != null && context?.User.Identity != null && context.User.Identity.IsAuthenticated)
                {
                    var identifier = context.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                    if (identifier != null)
                    {
                        return identifier.Value;
                    }
                }

                return string.Empty;
            }
        }
    }
}

Conclusion

When we run the application and try inserting or updating Items, we can see that the fields are automatically set each time and our implementation is decoupled from the business logic, that happens on the entity.

It is generally a best practice to have audit fields for any table in the database along with respective fields. It helps in keeping a track of who has modified what.

A simple approach of maintaining auditable fields in Entity Framework Core is by using the Change Tracking feature and setting up the fields before writing to the database.

This way the implementation is separated and is kept generic for any entity that extends the AuditableEntity.

We can take it a level further by keeping an Audit Trail of all the changes that happens on the entity in a separate table. We will look into further in an upcoming follow-up article.

The code snippets used in this article are a part of my fully solved .NET 6 boilerplate – ContainerNinja.CleanArchitecture.

Please do take a look and leave a star if you like my work ❤


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *