.NET Core fundamentals in one picture.

How to add creator/modified info to all of your EF models at once in .NET Core

Reading Time: 5 minutes.

This is a tip that should often be the first thing you do in your projects with database backend, no matter which technology you use: Add some basic info about modified and created times, and the user information – so that if something happens, everyone will know who to blame 😉

There’s a lot of great blog articles describing how to do this in .NET Framework, but not that many for .NET Core. It’s very similar, but not the same. I learned that by copy-pasting code from the former to the latter…

So what do you need to do, to make it work?

Solution

You’ll need to add a new base class for all of the models, add the properties there, and then make sure to populate the properties with values on SaveChanges().

First, the easy part. This is exactly the same in both .NET Framework and Core. Add a following (or similar) class:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
 
namespace Contoso.Data
{
    public class BaseEntity
    {
        public DateTime? CreatedDate { get; set; }
 
        public DateTime? ModifiedDate { get; set; }
 
        [Column("CreatedBy")]
        [Display(Name = "Creator")]
        public string CreatedBy { get; set; }
 
        [Column("ModifiedBy")]
        [Display(Name = "Modifier")]
        public string ModifiedBy { get; set; }   
    }
}

I’ve seen examples of people creating this as an abstract class. As good an idea that is (as it prevents you from using the class directly in error), it doesn’t seem to work too well with Entity Framework. Hence, it’s probably a good idea NOT to create it as abstract.

So, now we’ve got the class to inherit these properties from. Then modify your existing models to inherit this class:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
 
namespace Contoso.Data
{
    public class YourModel : BaseEntity
    {
        [Key]
        public int Id { get; set; }
 
        [Required]
        [MaxLength(500)]
        public string Title { get; set; }
    }
}

Next, open your db context class. In a lot of cases, this is called ApplicationDbContext (no other reason than common practice). You’ll need to override the SaveChanges method, and this happens in a pretty different way than in . NET Framework.

See below for how you used to do this in .NET Framework (don’t use this for .NET Core):

/// 
/// How to override SaveChanges method in .NET Framework - this doesn't work in .NET Core! 
/// 
public class ApplicationDbContext: DbContext 
{
 
 public override int SaveChanges() 
 {
  AddTimestamps();
  return base.SaveChanges();
 }
 
 public override async Task<int> SaveChangesAsync() 
 {
  AddTimestamps();
  return await base.SaveChangesAsync();
 }
 
 private void AddTimestamps() 
 {
  var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
 
  var currentUsername = !string.IsNullOrEmpty(System.Web.HttpContext.Current?.User?.Identity?.Name) ? HttpContext.Current.User.Identity.Name : "Anonymous";
 
  foreach (var entity in entities) {
   if (entity.State == EntityState.Added) {
    ((BaseEntity) entity.Entity).DateCreated = DateTime.UtcNow;
    ((BaseEntity) entity.Entity).UserCreated = currentUsername;
   }
   ((BaseEntity) entity.Entity).DateModified = DateTime.UtcNow;
   ((BaseEntity) entity.Entity).UserModified = currentUsername;
  }
 }
}

You’ll need a different way to figure out the current user, as you don’t have System.Web.HttpContext.Current.User available anymore.

My first idea was to use the injected userManager to get user information to then add to the database – but I overlooked a pretty big issue there, for I ran into this error:

An unhandled exception occurred while processing the request.

InvalidOperationException: A circular dependency was detected for the service of type 'Microsoft.AspNetCore.Identity.UserManager<ApplicationUser>'.

Microsoft.AspNetCore.Identity.ISecurityStampValidator(Microsoft.AspNetCore.Identity.SecurityStampValidatorapplicationuser>) ->
Microsoft.AspNetCore.Identity.SignInManager<Applicationuser> -> 

...

Of course – that was kinda stupid. Since the data for user information is stored in the database, injecting functionality that accesses that data in the component that enables access to that data, is bound to cause an issue like a circular dependency. Bah.

“A circular depedency was detected…” Yeah, I guess that was kinda stupid, wasn’t it?

However, there’s an easy workaround! There always is, isn’t there?

You can get the userId or email address associated with their account from the claims in their identity. That should be enough for your creator & editor information! See below for an example.

var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

To get this to work, you’ll need to add this into the ConfigureServices -method of your Startup.cs file:

services.AddSingleton<IHttpContextAccessor,HttpContextAccessor>();

So you’ll end up with SaveChanges() being overridden roughly like so:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
 
namespace Contoso.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
 
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor httpContextAccessor)
            : base(options)
        {
            _httpContextAccessor = httpContextAccessor;
        }
 
        // DbSet implementation omitted.
        public DbSet ...
 
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
 
        public override int SaveChanges()
        {
            AddTimestamps();
            return base.SaveChanges();
        }
 
        private void AddTimestamps()
        {
            var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
 
            var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
 
            var currentUsername = !string.IsNullOrEmpty(userId)
                ? userId
                : "Anonymous";
 
            foreach (var entity in entities)
            {
                if (entity.State == EntityState.Added)
                {
                    ((BaseEntity)entity.Entity).CreatedDate = DateTime.UtcNow;
                    ((BaseEntity)entity.Entity).CreatedBy = currentUsername;
                }
 
                ((BaseEntity)entity.Entity).ModifiedDate = DateTime.UtcNow;
                ((BaseEntity)entity.Entity).ModifiedBy = currentUsername;
            }
        }
    }
}

Et voilà! This override will enable you to save info on modified date, modifier, created date and creator. Very useful baseline info for your basic metadata needs! And not too complicated to implement.


(If there’s some inconsistencies with type definitions being “closed” as HTML tags in the examples above, I apologize – it’s an issue in WordPress. I’ve filed a bug for it on the backlog, but it’s probably not going to be fixed as the development team is bringing out a new “Code”-block at some point)

Antti K. Koskela

Antti Koskela is a proud digital native nomadic millennial full stack developer (is that enough funny buzzwords? That's definitely enough funny buzzwords!), who works as a Solutions Architect for Valo Intranet, the product that will make you fall in love with your intranet. Working with the global partner network, he's responsible for the success of Valo deployments happening all around the world.

He's been a developer from 2004 (starting with PHP and Java), and he's been bending and twisting SharePoint into different shapes since MOSS. Nowadays he's not only working on SharePoint, but also on .NET projects, Azure, Office 365 and a lot of other stuff.

This is his personal professional (e.g. professional, but definitely personal) blog.
mm

5
Leave a Reply

avatar
5000
4 Comment threads
1 Thread replies
4 Followers
 
Most reacted comment
Hottest comment thread
4 Comment authors
Antti K. KoskelaAbhimeziantouDavid Recent comment authors
  Subscribe  
newest oldest most voted
Notify of
David
Guest

You mention that it’s problematic using an abstract base class for your entities, can you elaborate? I’ve done that for years and never had a problem with it. Thanks!

meziantou
Guest

Very interesting post! I’ve written something similar a few times ago. With my solution you can choose to use read/write properties (similar to your post), read only properties, or no properties at all. You can check my post at https://www.meziantou.net/entity-framework-core-generate-tracking-columns.htm

Abhi
Guest
Abhi

I have entity class with different modifier How can it handle ?
My class as following:
public abstract class BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public T Id { get; set; }

[Display(Order = 1, Name = “Tag No”)]
[MaxLength(100)]
public string TagNo { get; set; }

private DateTime? createdDate;
[DataType(DataType.DateTime)]
public DateTime CreatedDate
{
get { return createdDate ?? DateTime.UtcNow; }
set { createdDate = value; }
}

[DataType(DataType.DateTime)]
public DateTime? ModifiedDate { get; set; }

public string CreatedBy { get; set; }

public string ModifiedBy { get; set; }

[Timestamp]
public byte[] Version { get; set; }

[DataType(DataType.DateTime)]
public DateTime? DeletedOn { get; set; }
public bool? IsDeleted { get; set; } = false;

}

How to write query to handle your code.?

var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));

BaseEntity how it will modify your above query to make it work.
please suggest it.

Thank you in advance.

Abhi
Guest
Abhi

Thanks