How to get user information in DbContext using Net Core

22,763

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;
    }
Share:
22,763

Related videos on Youtube

adem caglin
Author by

adem caglin

Software Engineer [email protected]

Updated on March 27, 2021

Comments

  • adem caglin
    adem caglin about 3 years

    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:

    • Using HttpContext.Items to keep user information, injecting IHttpContextAccessor and getting information from the HttpContext.Items(In this case DbContext depends HttpContext, 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
      Joe Audette about 8 years
      instead 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
      adem caglin about 8 years
      that seems really good way, i will try to implement it. thanks.
  • Andriy Zymenko
    Andriy Zymenko about 7 years
    Remove await keyword after return, like this public string GetUser() { return _context.HttpContext.User?.Identity?.Name; }
  • Andriy Zymenko
    Andriy Zymenko about 7 years
    Now IHttpContextAccessor is not registered in services by default. We have to wire it manualy in Startup.cs services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
  • heringer
    heringer about 5 years
    Very 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
    heringer about 5 years
    Usually DbContext is scoped. Should UserResolverService be scoped to? I mean: services.AddScoped<UserResolverService>();
  • dwp4ge
    dwp4ge about 5 years
    If using .NET core 2.1 they added services.AddHttpContextAccessor() which if you take a look under the hood at the source code it uses services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>()
  • Mandeep Janjua
    Mandeep Janjua almost 5 years
    Services 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
    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
    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
    Mayer Spitz almost 2 years
    Thanks! Love the creativity and structure!