I am trying to develop a class library in which i want to implement custom DbContext
. In the SaveChanges
method of the DbContext
, i need to get current user’s information(department, username etc.) for auditing purpose. Some part of the DbContext
code is below:
public override int SaveChanges()
{
// find all changed entities which is ICreateAuditedEntity
var addedAuditedEntities = ChangeTracker.Entries<ICreateAuditedEntity>()
.Where(p => p.State == EntityState.Added)
.Select(p => p.Entity);
var now = DateTime.Now;
foreach (var added in addedAuditedEntities)
{
added.CreatedAt = now;
added.CreatedBy = ?;
added.CreatedByDepartment = ?
}
return base.SaveChanges();
}
Two options coming to mind:
HttpContext.Items
to keep user information, injecting IHttpContextAccessor and getting information from the
HttpContext.Items
(In this case DbContext
depends HttpContext
, is it
correct?)HttpContext.Items
and getting information from the object( I read some posts
that ThreadStatic is not safe)Question : Which is the best fit into my case? Is there another way you suggest?
I implemented an approach similar to this that is covered in this blog post and basically involves creating a service that will use dependency injection to inject the HttpContext
(and underlying user information) into a particular context, or however you would prefer to use it.
A very basic implementation might look something like this:
public class UserResolverService
{
private readonly IHttpContextAccessor _context;
public UserResolverService(IHttpContextAccessor context)
{
_context = context;
}
public string GetUser()
{
return _context.HttpContext.User?.Identity?.Name;
}
}
You would just need to inject this into the pipeline within the ConfigureServices
method in your Startup.cs
file :
services.AddTransient<UserResolverService>();
And then finally, just access it within the constructor of your specified DbContext
:
public partial class ExampleContext : IExampleContext
{
private YourContext _context;
private string _user;
public ExampleContext(YourContext context, UserResolverService userService)
{
_context = context;
_user = userService.GetUser();
}
}
Then you should be able to use _user
to reference the current user within your context. This can easily be extended to store / access any content available within the current request as well.
Thanks to @RionWilliams for the original answer. This is how we solved CreatedBy and UpdatedBy via DbContext
, AD B2C
users and Web Api in .Net Core
3.1. SysStartTime
and SysEndTime
is basically CreatedDate
and UpdatedDate
but with version history (information about data stored in the table at any point in time) via temporal tables.
More about that here:
https://stackoverflow.com/a/64776658/3850405
Generic interface:
public interface IEntity
{
public DateTime SysStartTime { get; set; }
public DateTime SysEndTime { get; set; }
public int CreatedById { get; set; }
public User CreatedBy { get; set; }
public int UpdatedById { get; set; }
public User UpdatedBy { get; set; }
}
DbContext:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions options) : base(options)
{
}
public DbSet<User> User { get; set; }
public string _currentUserExternalId;
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
var user = await User.SingleAsync(x => x.ExternalId == _currentUserExternalId);
AddCreatedByOrUpdatedBy(user);
return (await base.SaveChangesAsync(true, cancellationToken));
}
public override int SaveChanges()
{
var user = User.Single(x => x.ExternalId == _currentUserExternalId);
AddCreatedByOrUpdatedBy(user);
return base.SaveChanges();
}
public void AddCreatedByOrUpdatedBy(User user)
{
foreach (var changedEntity in ChangeTracker.Entries())
{
if (changedEntity.Entity is IEntity entity)
{
switch (changedEntity.State)
{
case EntityState.Added:
entity.CreatedBy = user;
entity.UpdatedBy = user;
break;
case EntityState.Modified:
Entry(entity).Reference(x => x.CreatedBy).IsModified = false;
entity.UpdatedBy = user;
break;
}
}
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var property in modelBuilder.Model.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(string)))
{
if (property.GetMaxLength() == null)
property.SetMaxLength(256);
}
foreach (var property in modelBuilder.Model.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(DateTime)))
{
property.SetColumnType("datetime2(0)");
}
foreach (var et in modelBuilder.Model.GetEntityTypes())
{
foreach (var prop in et.GetProperties())
{
if (prop.Name == "SysStartTime" || prop.Name == "SysEndTime")
{
prop.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
}
}
}
modelBuilder.Entity<Question>()
.HasOne(q => q.UpdatedBy)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
}
ExtendedApplicationDbContext:
public class ExtendedApplicationDbContext
{
public ApplicationDbContext _context;
public UserResolverService _userService;
public ExtendedApplicationDbContext(ApplicationDbContext context, UserResolverService userService)
{
_context = context;
_userService = userService;
_context._currentUserExternalId = _userService.GetNameIdentifier();
}
}
UserResolverService:
public class UserResolverService
{
public readonly IHttpContextAccessor _context;
public UserResolverService(IHttpContextAccessor context)
{
_context = context;
}
public string GetGivenName()
{
return _context.HttpContext.User.FindFirst(ClaimTypes.GivenName).Value;
}
public string GetSurname()
{
return _context.HttpContext.User.FindFirst(ClaimTypes.Surname).Value;
}
public string GetNameIdentifier()
{
return _context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
}
public string GetEmails()
{
return _context.HttpContext.User.FindFirst("emails").Value;
}
}
Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddTransient<UserResolverService>();
services.AddTransient<ExtendedApplicationDbContext>();
...
Can then be used like this in any Controller
:
public class QuestionsController : ControllerBase
{
private readonly ILogger<QuestionsController> _logger;
private readonly ExtendedApplicationDbContext _extendedApplicationDbContext;
public QuestionsController(ILogger<QuestionsController> logger, ExtendedApplicationDbContext extendedApplicationDbContext)
{
_logger = logger;
_extendedApplicationDbContext = extendedApplicationDbContext;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With