How to get user information in DbContext using Net Core

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;
    }

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.