#SharePointProblems | Koskila.net

Solutions are worthless unless shared! Antti K. Koskela's Personal Professional Blog

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

koskila
Reading Time 6 min
Word Count 942 words
Comments 13 comments
Rating 4.7 (3 votes)
View

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 are 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

Time needed: 5 minutes.

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().

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

    <pre lang="csharp">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; }       } } </pre>

    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 to NOT create it as abstract.

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

    <pre lang="csharp">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; }     } } </pre>

  3. 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 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;   }  } }

  4. 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:

    <!--<pre lang="csharp"> 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> -> Microsoft.AspNetCore.Identity.UserManager<Applicationuser>(Microsoft.AspNetCore.Identity.AspNetUserManager<Applicationuser>) -> Microsoft.AspNetCore.Identity.IUserStore<Applicationuser>(Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore<Applicationuser> ,Microsoft.AspNetCore.Identity.IdentityRole,ApplicationDbContext,string,Microsoft.AspNetCore.Identity.IdentityRoleClaim<String>, Microsoft.AspNetCore.Identity.IdentityUserRole<String>, Microsoft.AspNetCore.Identity.IdentityUserLogin<String>, Microsoft.AspNetCore.Identity.IdentityUserToken<String>, Microsoft.AspNetCore.Identity.IdentityRoleClaim<String>) -> ApplicationDbContext -> Microsoft.AspNetCore.Identity.UserManager<Applicationuser> Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain.CheckCircularDependency(Type serviceType) </pre>-->

    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?

  5. 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 the below example:

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

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

    <pre lang="csharp">services.AddSingleton<ihttpcontextaccessor,httpcontextaccessor>(); </ihttpcontextaccessor,httpcontextaccessor></pre>

  7. So you'll end up with SaveChanges() being overridden roughly like so:

    <pre lang="csharp">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;             }         }     } }   </applicationdbcontext></applicationuser></pre>

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


(If there are 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. Keep believing, it's only been a couple of years!)

Comments

Interactive comments not implemented yet. Showing legacy comments migrated from WordPress.
2019-08-28 13:26:28)
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!
2019-10-25 06:07:53
Hi David, Man, I wish I already had this blog back when I ran into issues with the base class being abstract - I remember it messing up generating new migrations, and have avoided it ever since. Entirely possible, that I've just run into a real edge case, though! If I encounter anything about it, I'll be sure to update this post, too :)
2019-08-28 19:17:45)
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
2019-10-01 10:18:16)
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
2019-10-02 13:00:33)
Thanks
Divya
2020-01-20 14:33:52)
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value; "UserId" getting null value and getting "Object reference not set to an instance of object" error. How to resolve this issue? Please help me on this.
2020-01-20 21:53:43
Hi Divya, Here's another blog post describing how to handle identity in .NET Core: https://www.koskila.net/how-to-get-current-user-in-asp-net-core/ And this one describes the configuration to populate the Claims: https://www.koskila.net/iterating-group-memberships-using-claims-in-net-core/ Let me know if they help you out!
2020-01-21 14:21:42)
Thanks.
2020-01-21 22:24:32
My pleasure! :)
Ario
2021-05-09 15:33:16)
THANK YOU SO MUCH! You have no Idea I've been looking for this explanation everywhere You helped me a lot
Antti K. Koskela
2021-05-12 09:39:06
Thanks for your comment, Ario. Happy to hear it was helpful!
Rusty
2021-10-17 17:18:55)
Thank you very much for taking the time to share this, I found it very helpful and useful! I had to make a slight modification because I wasn't including the Created Date on my forms and so it would be overwritten on update.I was able to resolve this by adding an IsModified = false. I am deriving from a BaseEntity for my models where the ID and Created/Modified values are set. (I haven't added in user tracking yet, so removed that part of the code for now) .NET Core 5 EF Core 5
public override Task<int> SaveChangesAsync(
                                    bool acceptAllChangesOnSuccess,
                                    CancellationToken token = default)
        {
            AddTimestamps();
            return base.SaveChangesAsync(acceptAllChangesOnSuccess, token);
        }

        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));

            foreach (var entity in entities)
            {
                if (entity.State == EntityState.Added)
                {
                    ((BaseEntity)entity.Entity).CreatedDate = DateTime.UtcNow;
                } else
                {
                    entity.Property("CreatedDate").IsModified = false;
                }
             ((BaseEntity)entity.Entity).ModifiedDate = DateTime.UtcNow;
            }
        }
Antti K. Koskela
2021-10-22 13:17:57
Thanks for your comment, and thanks for sharing, Rusty!
Whitewater Magpie Ltd.
© 2025
Static Site Generation timestamp: 2025-08-21T07:25:12Z