Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get the current logged in user ID in the ApplicationDbContext using Identity?

I have created a .net core 2.1 MVC application using the template in Visual Studio with the Identity preset (user accounts stored in the application) and I am trying to automate some auditing fields.

Basically what I'm trying to do is overriding the SaveChangesAsync() method so that whenever changes are made to an entity the current logged in user ID is set to the auditing property of CreatedBy or ModifiedBy properties that are created as shadow properties on the entity.

I have looked at what seems to be tons of answers and surprisingly none of them work for me. I have tried injecting IHttpContext, HttpContext, UserManager, and I either can't seem to access a method that returns the user ID or I get a circular dependency error which I don't quite understand why it is happening.

I'm really running desperate with this one. I think something like this should be really straightforward to do, but I'm having a real hard time figuring out how to do it. There seem to be well documented solutions for web api controllers or for MVC controllers but not for use inside the ApplicationDbContext.

If someone can help me or at least point me into the right direction I'd be really grateful, thanks.

like image 430
João Paiva Avatar asked Oct 19 '18 22:10

João Paiva


Video Answer


2 Answers

Let's call it DbContextWithUserAuditing

public class DBContextWithUserAuditing : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
    public string UserId { get; set; }
    public int? TenantId { get; set; }

    public DBContextWithUserAuditing(DbContextOptions<DBContextWithUserAuditing> options) : base(options) { }

    // here we declare our db sets

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.NamesToSnakeCase(); // PostgreSQL
        modelBuilder.EnableSoftDelete();
    }

    public override int SaveChanges()
    {
        ChangeTracker.DetectChanges();
        ChangeTracker.ProcessModification(UserId);
        ChangeTracker.ProcessDeletion(UserId);
        ChangeTracker.ProcessCreation(UserId, TenantId);

        return base.SaveChanges();
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        ChangeTracker.DetectChanges();
        ChangeTracker.ProcessModification(UserId);
        ChangeTracker.ProcessDeletion(UserId);
        ChangeTracker.ProcessCreation(UserId, TenantId);

        return (await base.SaveChangesAsync(true, cancellationToken));
    }
}

Then you have request pipeline and what you need - is a filter hook where you set your UserID

public class AppInitializerFilter : IAsyncActionFilter
{
    private DBContextWithUserAuditing _dbContext;

    public AppInitializerFilter(
        DBContextWithUserAuditing dbContext
        )
    {
        _dbContext = dbContext;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next
        )
    {
        string userId = null;
        int? tenantId = null;

        var claimsIdentity = (ClaimsIdentity)context.HttpContext.User.Identity;

        var userIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
        if (userIdClaim != null)
        {
            userId = userIdClaim.Value;
        }

        var tenantIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == CustomClaims.TenantId);
        if (tenantIdClaim != null)
        {
            tenantId = !string.IsNullOrEmpty(tenantIdClaim.Value) ? int.Parse(tenantIdClaim.Value) : (int?)null;
        }

        _dbContext.UserId = userId;
        _dbContext.TenantId = tenantId;

        var resultContext = await next();
    }
}

You activate this filter in the following way (Startup.cs file)

services
    .AddMvc(options =>
    {
        options.Filters.Add(typeof(OnRequestInit));
    })

Your app is then able to automatically set UserID & TenantID to newly created records

public static class ChangeTrackerExtensions
{
    public static void ProcessCreation(this ChangeTracker changeTracker, string userId, int? tenantId)
    {
        foreach (var item in changeTracker.Entries<IHasCreationTime>().Where(e => e.State == EntityState.Added))
        {
            item.Entity.CreationTime = DateTime.Now;
        }

        foreach (var item in changeTracker.Entries<IHasCreatorUserId>().Where(e => e.State == EntityState.Added))
        {
            item.Entity.CreatorUserId = userId;
        }

        foreach (var item in changeTracker.Entries<IMustHaveTenant>().Where(e => e.State == EntityState.Added))
        {
            if (tenantId.HasValue)
            {
                item.Entity.TenantId = tenantId.Value;
            }
        }
    }

I wouldn't recommend injecting HttpContext, UserManager or anything into your DbContext class because this way you violate Single Responsibility Principle.

like image 188
Alex Herman Avatar answered Oct 09 '22 12:10

Alex Herman


Thanks to all the answers. In the end I decided to create a UserResolveService that receives through DI the HttpContextAccessor and can then get the current user's name. With the name I can then query the database to get whatever information I may need. I then inject this service on the ApplicationDbContext.

IUserResolveService.cs

public interface IUserResolveService
{
    Task<string> GetCurrentSessionUserId(IdentityDbContext dbContext);
}

UserResolveService.cs

public class UserResolveService : IUserResolveService
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public UserResolveService(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public async Task<string> GetCurrentSessionUserId(IdentityDbContext dbContext)
    {
        var currentSessionUserEmail = httpContextAccessor.HttpContext.User.Identity.Name;

        var user = await dbContext.Users                
            .SingleAsync(u => u.Email.Equals(currentSessionUserEmail));
        return user.Id;
    }
}

You have to register the service on startup and inject it on the ApplicationDbContext and you can use it like this:

ApplicationDbContext.cs

var dbContext = this;

var currentSessionUserId = await userResolveService.GetCurrentSessionUserId(dbContext);
like image 32
João Paiva Avatar answered Oct 09 '22 10:10

João Paiva