This post was most recently updated on March 28th, 2023.
4 min read.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().
- 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. - 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> - 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;
}
}
} - 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? - 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; - 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> - 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!)
- How to close the Sidebar in Microsoft Edge - December 17, 2024
- How to stop Surface Headphones from autoplaying audio when you place them on the desk? - December 10, 2024
- “Phone Link” permanently disabled and Windows claiming “Some of these settings are managed by your organization”? (Probably) an easy fix! - December 3, 2024