Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get current logged in user in DbContext

For audit purposes I'm trying to get the current logged in user in my DbContext. However I'm having some issues with this. A few things to take into account:

  • In Blazor Server we have to use AddDbContextFactory
  • IHttpContextAccessor returns no result in deployed website (might be because IHttpContextAccessor is not thread safe?)

I created a custom DbContext that injects AuthenticationStateProvider.

public partial class CustomDbContext : DbContext
    {
        private AuthenticationStateProvider _authenticationStateProvider;

        #region construction

        public CustomDbContext ()
        {
        }

        public CustomDbContext (AuthenticationStateProvider stateProvider)
        {
            _authenticationStateProvider = stateProvider;
        }

        [ActivatorUtilitiesConstructor]
        public CustomDbContext (DbContextOptions<CustomDbContext> options, AuthenticationStateProvider stateProvider) : base(options)
        {
            _authenticationStateProvider = stateProvider;
        }

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

        #endregion
        ...

In this DbContext, when overwriting the SaveChanges I get the User and their claims:

  var state = await _authenticationStateProvider.GetAuthenticationStateAsync();
  var userIdClaim = state.User.Claims.FirstOrDefault(c => c.Type == "userId")?.Value;
  userId = userIdClaim != null && !string.IsNullOrEmpty(userIdClaim ) ? userIdClaim : string.Empty;
  ...

However when I call .CreateDbContext(); on the injected DbContextFactory, I get the following exception:

'Cannot resolve scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider' from root provider.'

I've found some topics about this, but the suggested solution there is to create a custom DbContextFactory that is scoped. But then you lose the reason why you are using the DbContextFactory, no?

Any ideas on how to solve this?

Thank you

like image 535
Schoof Avatar asked Jan 29 '26 17:01

Schoof


1 Answers

The DBContextFactory is a singleton registered in the root application DI container, while the AuthenticationStateProvider is a scoped service that is registered in the Hub session DI container. You can't access a lower order service from a higher order service.

You need to rethink your design and provide the user information from whatever scoped service is making whatever call to need a DbConbtext.

Additional Information

I'm not sure what your data pipeline looks like so this example uses the Blazor template weather forecast.

First a View Service that components inject and use.

This injects the AuthenticationStateProvider. It gets the current user for each request and passes it to the data pipeline in a request object.

public class WeatherForecastViewService
{
    private AuthenticationStateProvider _authenticationStateProvider; // scoped service
    private WeatherForecastService _weatherForecastService; //Singleton Service

    public WeatherForecastViewService(AuthenticationStateProvider authenticationStateProvider, WeatherForecastService weatherForecastService)
    {
        _authenticationStateProvider = authenticationStateProvider;
        _weatherForecastService = weatherForecastService;
    }
    
    public async ValueTask SaveWeatherForecast(WeatherForecast record)
    {
        var user = await GetCurrentUser();
        var request = new RecordRequest<WeatherForecast>(record, user );
        await _weatherForecastService.SaveRecord(request);   
    }

    private async ValueTask<ClaimsPrincipal> GetCurrentUser()
    {
        var state = await _authenticationStateProvider.GetAuthenticationStateAsync();
        return state.User ?? new ClaimsPrincipal();
    }
}

Here are the request and result objects:

public readonly struct RecordRequest<TRecord>
{
    public TRecord Record { get; init; }
    public ClaimsPrincipal Identity { get; init; }

    public RecordRequest(TRecord record, ClaimsPrincipal identity)
    {
        this.Record = record;
        this.Identity = identity;
    }
}
public record RecordResult
{
    public bool SuccessState { get; init; }
    public string Message { get; init; }

    private RecordResult(bool successState, string? message)
    {
        this.SuccessState = successState;
        this.Message = message ?? string.Empty;
    }

    public static RecordResult Success(string? message = null)
        => new RecordResult(true, message);

    public static RecordResult Failure(string message)
        => new RecordResult(false, message);
}

And here's the singleton data service

public class WeatherForecastDataService
{
    //  This is a singleton
    private readonly IDbContextFactory<DbContext> _factory;

    public WeatherForecastDataService(IDbContextFactory<DbContext> factory)
        => _factory = factory;

    public async ValueTask<RecordResult> SaveRecord(RecordRequest<WeatherForecast> request)
    {
        if (!request.Identity.IsInRole("SomeRole"))
            return RecordResult.Failure("User does not have authority");

        // simulates some async DB activity
        await Task.Delay(100);
        // Get your DbContext from the injected Factory
        // using var dbContext = this.factory.CreateDbContext();
        // do your db stuff
        return RecordResult.Success();
    }
}

PS I haven'y actually run this code so there may be some typos!

like image 89
MrC aka Shaun Curtis Avatar answered Feb 02 '26 03:02

MrC aka Shaun Curtis



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!