Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get user information in DbContext using Net Core

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?

like image 666
adem caglin Avatar asked Apr 04 '16 11:04

adem caglin


2 Answers

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.

like image 195
Rion Williams Avatar answered Oct 27 '22 06:10

Rion Williams


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;
    }
like image 26
Ogglas Avatar answered Oct 27 '22 07:10

Ogglas