As a Laravel user for quite sometime, it's quite easy for me to add User creator, modifier and timestamp for each model.
In the case of .Net 6 (or even earlier) it can be handled at DbContext and creating your base class that has the generic properties for the timestamps (created at, modified at) and user details (created_by / modified_by)
using System.ComponentModel.DataAnnotations;
namespace YourApp.Models{
public class BaseEntity
{
public DateTime DateCreated { get; set; }
[Required]
//id of the user created
public string UserCreated { get; set; }
public DateTime? DateModified { get; set; }
[Required]
//id of the user edited
public string UserModified { get; set; }
}
}
Below is my subclass using the super class BaseEntity
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace YourApp.Models;
/*
* Patient Model
*/
public class MyCustomModel : BaseEntity
{
[StringLength(125)]
public string FirstName { get; set; }
[StringLength(125)]
public string LastName { get; set; }
[StringLength(125)]
public string Middlename { get; set; } = string.Empty;
}
So how to automatically do the timestamps and user details? I implemented the tutorial from the blog that I followed
here. According to the blog, you can implement it at the override of your DbContext'
SaveChangesAsync.
My DbContext below. I highlighted from the code below the important parts where the magic happens.
using Microsoft.EntityFrameworkCore;
using YourApp.Models;
using System.Security.Claims;
namespace YourApp.Contexts
{
public class DataContext : DbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public DataContext(DbContextOptions <DataContext> options, IHttpContextAccessor httpContextAccessor) : base(options)
{
_httpContextAccessor = httpContextAccessor;
}
public DbSet<MyCustomModel > CustomModels{ get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var insertedEntries = this.ChangeTracker.Entries()
.Where(x => x.State == EntityState.Added)
.Select(x => x.Entity);
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
var currentUsername = !string.IsNullOrEmpty(userId)
? userId
: "Anonymous";
foreach (var insertedEntry in insertedEntries)
{
var auditableEntity = insertedEntry as BaseEntity;
//If the inserted object is an Auditable.
if (auditableEntity != null)
{
auditableEntity.DateCreated = DateTime.Now;
auditableEntity.UserCreated = currentUsername;
auditableEntity.UserModified = currentUsername;
}
}
var modifiedEntries = this.ChangeTracker.Entries()
.Where(x => x.State == EntityState.Modified)
.Select(x => x.Entity);
foreach (var modifiedEntry in modifiedEntries)
{
//If the inserted object is an Auditable.
var auditableEntity = modifiedEntry as BaseEntity;
if (auditableEntity != null)
{
auditableEntity.DateModified = DateTime.Now;
auditableEntity.UserModified = currentUsername;
}
}
return base.SaveChangesAsync(cancellationToken);
}
}
}
Aside from that, ensure also from your login that ClaimTypes.NameIdentifier is not empty
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
I set my value during login. See login method below.
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] UserLogin model)
{
var user = await _userManager.FindByNameAsync(model.Username);
if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
{
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = GetToken(authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
That pretty much of it. Hope that helps you too.