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:
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
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With