How to get user information in DbContext using Net Core
Solution 1
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.
Solution 2
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;
}
Related videos on Youtube
Comments
-
adem caglin about 3 years
I am trying to develop a class library in which i want to implement custom
DbContext
. In theSaveChanges
method of theDbContext
, i need to get current user’s information(department, username etc.) for auditing purpose. Some part of theDbContext
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:
- Using
HttpContext.Items
to keep user information, injecting IHttpContextAccessor and getting information from theHttpContext.Items
(In this caseDbContext
dependsHttpContext
, is it correct?) - Using ThreadStatic object instead of
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?
-
Joe Audette about 8 yearsinstead of taking a dependency on IHttpContextAccessor directly in your DbContext why not make a service class like AuditLogger and let your DbContext depend on it, AuditLogger can depend on IHttpContextAccessor as its own internal implementation detail
-
adem caglin about 8 yearsthat seems really good way, i will try to implement it. thanks.
- Using
-
Andriy Zymenko about 7 yearsRemove await keyword after return, like this
public string GetUser() { return _context.HttpContext.User?.Identity?.Name; }
-
Andriy Zymenko about 7 yearsNow IHttpContextAccessor is not registered in services by default. We have to wire it manualy in Startup.cs
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
-
heringer about 5 yearsVery elegant solution, thanks. I am thinking about adding an interface to be implemented by UserResolverService and make the DbContext to use it instead of the UserResloverService itself. Thus, it would decouple de DbContext from IHttpContextAccessor to help unit tests and other usages.
-
heringer about 5 yearsUsually DbContext is scoped. Should UserResolverService be scoped to? I mean: services.AddScoped<UserResolverService>();
-
dwp4ge about 5 yearsIf using .NET core 2.1 they added
services.AddHttpContextAccessor()
which if you take a look under the hood at the source code it usesservices.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>()
-
Mandeep Janjua almost 5 yearsServices refer data\dbcontext project. To inject a service into a data project, you have to refer it. I'm afraid it will cause a cyclic reference
-
Ruslan_K about 3 years@MandeepJanjua Hi, what is a workaround? Is it ok to add a package Microsoft.AspNetCore.Http to data project?
-
Mandeep Janjua about 3 years@Ruslan_K No. Data project should not know about http. Right approach would be to build-up this info in the services and pass it along to the db project
-
Mayer Spitz almost 2 yearsThanks! Love the creativity and structure!