2

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

Schoof
  • 2,715
  • 5
  • 29
  • 41

2 Answers2

3

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!

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thank you, that makes sense! Any suggestions on how to solve this? I need the UserId on each call to SaveChanges and I would like to avoid duplicate code (eg. always getting the UserId before calling SaveChanges and than passing it along) – Schoof Oct 07 '22 at 06:57
  • 1
    Yes, will post an answer later if no one beats me to it. – MrC aka Shaun Curtis Oct 07 '22 at 09:25
  • Thank you! Yeah that's how I'm doing it right now, I'm not having the DbContext taking care of getting the user, but doing it in a service before hand. :) – Schoof Oct 11 '22 at 07:47
1

IHttpContextAccessor returns no result in deployed website (might be because IHttpContextAccessor is not thread safe?)

Nothing to do with whether IHttpContextAccessor is not thread safe... It's simply because the HttpContext object is not available in Blazor Server App, as communication between the client side (browser) and server side is done through the SignalR protocol, not HTTP. But there is a way how to access the HttpContext object before the Blazor App is rendered, as the initial call to the app is always made through HTTP request; that is, when you enter a url into the address bar of your browser and hit the enter button. See here how to do that...

The following code snippet describes how to inject an AuthenticationStateProvider into the ApplicationDbContext object created by default when you select Individual Accounts in Blazor Server App. Copy and test. It should work...

Data/ApplicationDbContext.cs

public class ApplicationDbContext : IdentityDbContext
{
    public DbSet<Employee> Employees { get; set; }
    private AuthenticationStateProvider _authenticationStateProvider;
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> 
        options, AuthenticationStateProvider stateProvider)
            : base(options)
        {
            _authenticationStateProvider = stateProvider;
        }
           
        public override async Task<int> 
               SaveChangesAsync(CancellationToken cancellationToken)
        {
             var stateProvider = await 
           _authenticationStateProvider.GetAuthenticationStateAsync();
    
            if (stateProvider.User.Identity.IsAuthenticated)
            {
                Console.WriteLine("Authenticated User name: " + 
                              stateProvider.User.Identity.Name);
            }
            // Delegate the saving action to the base class
            return await base.SaveChangesAsync(cancellationToken);
        }
    }

Create an Employee Repository class service:

EmployeeRepository.cs

using <put here the namespace of your app>.Data;
using <put here the namespace of your app>.Models;
using Microsoft.EntityFrameworkCore;

public class EmployeeRepository
{
    private readonly ApplicationDbContext ApplicationDbContext;
    public EmployeeRepository(ApplicationDbContext 
                                         applicationDbContext)
    {
        ApplicationDbContext = applicationDbContext;
    }
    public async Task<Employee> CreateEmployee(Employee employee)
    {
       
        CancellationTokenSource cancellationTokenSource = new 
                                    CancellationTokenSource();
        CancellationToken token = cancellationTokenSource.Token;

        await ApplicationDbContext.Employees.AddAsync(employee);
        await ApplicationDbContext.SaveChangesAsync(token);

        return employee;
    }
}

Index.razor

@inject EmployeeRepository EmployeeRepository
@using  <Put here....>.Models


<button type="button" @onclick="SaveEmployee">Save Employee</button>

@if (emp != null)
{
    <div>@emp.ID.ToString()</div>
    <div>@emp.FirstName</div>
    <div>@emp.LastName</div>
    <div>@emp.City</div>
}
@code
{
    private Employee emp;

    private async Task SaveEmployee()
    {
        Employee employee = new Employee { FirstName = "Joana", LastName = "Brown", City = "London" };

        emp = await EmployeeRepository.CreateEmployee(employee);
    }
}

Create model class Employee:

Models/Employee.cs

public class Employee
  {
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
  }

Note: To test this code, you'll have to create A Blazor Server App with Individual Accounts, create the database, including the Employees table

Last but not least: Startup

// Created by the default template
  //services.AddDbContext<ApplicationDbContext>(options =>
  // options.UseSqlServer(
  //        Configuration.GetConnectionString("DefaultConnection")));

  services.AddDefaultIdentity<IdentityUser>(options => 
      options.SignIn.RequireConfirmedAccount = true)
      .AddEntityFrameworkStores<ApplicationDbContext>();

  services.AddRazorPages();
  services.AddServerSideBlazor();
  services.AddDbContextFactory<ApplicationDbContext>(options =>
     options.UseSqlServer(
      Configuration.GetConnectionString("DefaultConnection")), 
                                      ServiceLifetime.Scoped);
  // This is your code... 
  services.AddScoped<ApplicationDbContext>(p => 
  p.GetRequiredService<IDbContextFactory<ApplicationDbContext>> 
       ().CreateDbContext()); 

  services.AddScoped<EmployeeRepository>();

 services.AddScoped<AuthenticationStateProvider, 
 RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
 services.AddDatabaseDeveloperPageExceptionFilter();
 services.AddSingleton<WeatherForecastService>();

UPDATE:

but does that no against the the recommendations of Microsoft? They ae suggesting to always use using

var context = DbFactory.CreateDbContext(); 

You mean:

using var context = DbFactory.CreateDbContext();

No, it is not against the recommendations of Microsoft. It's another way to instantiate the DbContext. I did it that way in order to stick to this code by you:

services.AddScoped<ApplicationDbContext>(p => p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());

Anyhow, these are the changes you should make in order to reflect "Microsoft's recommendations"

Change:

services.AddScoped<ApplicationDbContext>(p => p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());

To:

services.AddScoped<ApplicationDbContext>();

Change:

private readonly ApplicationDbContext ApplicationDbContext;
public EmployeeRepository(ApplicationDbContext 
                                      applicationDbContext)
{
   ApplicationDbContext = applicationDbContext;
}

To:

    private readonly IDbContextFactory<ApplicationDbContext> 
                                                   DbFactory;
  public EmployeeRepository(IDbContextFactory<ApplicationDbContext> 
                                                        _DbFactory)
  {
       DbFactory = _DbFactory;
  }

And change:

 await ApplicationDbContext.Employees.AddAsync(employee);
 await ApplicationDbContext.SaveChangesAsync(token);

To:

await context.Employees.AddAsync(employee);
await context.SaveChangesAsync(token);

Also add:

using var context = DbFactory.CreateDbContext();

at the beginning of the EmployeeRepository.CreateEmployee method

Run and test.

Hope this work...

New Version

Data/ApplicationDbContext.cs

public class ApplicationDbContext : IdentityDbContext
{
    public DbSet<Employee> Employees { get; set; }
    private AuthenticationStateProvider _authenticationStateProvider;
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> 
        options, AuthenticationStateProvider stateProvider)
            : base(options)
        {
            _authenticationStateProvider = stateProvider;
        }
           
        public override async Task<int> 
               SaveChangesAsync(CancellationToken cancellationToken)
        {
             var stateProvider = await 
           _authenticationStateProvider.GetAuthenticationStateAsync();
    
            if (stateProvider.User.Identity.IsAuthenticated)
            {
                Console.WriteLine("Authenticated User name: " + 
                              stateProvider.User.Identity.Name);
            }
            // Delegate the saving action to the base class
            return await base.SaveChangesAsync(cancellationToken);
        }
    }

Create an Employee Repository class service:

EmployeeRepository.cs

using <put here the namespace of your app>.Data;
using <put here the namespace of your app>.Models;
using Microsoft.EntityFrameworkCore;

 public class EmployeeRepository
{
    private readonly IDbContextFactory<ApplicationDbContext> DbFactory;
    public EmployeeRepository(IDbContextFactory<ApplicationDbContext> _DbFactory)
    {
        DbFactory = _DbFactory;
    }
    public async Task<Employee> CreateEmployee(Employee 
                                                  employee)
    {
        using var context = DbFactory.CreateDbContext();

        // CancellationTokenSource provides the token and have authority to cancel the token
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        CancellationToken token = cancellationTokenSource.Token;

        await context.Employees.AddAsync(employee);
        await context.SaveChangesAsync(token);
       
        return employee;
    }
}

Index.razor

@inject EmployeeRepository EmployeeRepository
@using  <Put here....>.Models


<button type="button" @onclick="SaveEmployee">Save Employee</button>

@if (emp != null)
{
    <div>@emp.ID.ToString()</div>
    <div>@emp.FirstName</div>
    <div>@emp.LastName</div>
    <div>@emp.City</div>
}
@code
{
    private Employee emp;

    private async Task SaveEmployee()
    {
        Employee employee = new Employee { FirstName = "Joana", LastName = "Brown", City = "London" };

        emp = await EmployeeRepository.CreateEmployee(employee);
    }
}

Create model class Employee:

Models/Employee.cs

public class Employee
  {
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
  }

Note: To test this code, you'll have to create A Blazor Server App with Individual Accounts, create the database, including the Employees table

Last but not least: Startup

public void ConfigureServices(IServiceCollection services)
{
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();


      
     services.AddRazorPages();
     services.AddServerSideBlazor();

  services.AddDbContextFactory<ApplicationDbContext>(options =>
   options.UseSqlServer(
   Configuration.GetConnectionString("DefaultConnection")), 
                                    ServiceLifetime.Scoped);
        
  services.AddScoped<ApplicationDbContext>();
  services.AddScoped<EmployeeRepository>();

  services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
  services.AddDatabaseDeveloperPageExceptionFilter();
  services.AddSingleton<WeatherForecastService>();
        
}
enet
  • 41,195
  • 5
  • 76
  • 113
  • Thank you Enet, but does that no against the the [recommendations of Microsoft](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-6.0#database-access)? They ae suggesting to always use using `var context = DbFactory.CreateDbContext();` when using your DbContext, which you are not doing there in the Repository? You are injecting the DbContext itself, and not the Factory. – Schoof Oct 11 '22 at 07:58
  • @Schoof, See UPDATE section in my answer. – enet Oct 11 '22 at 09:28
  • Unfortunately using your updated codes comes back to `'Cannot resolve scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider' from root provider.'` when you execute `using var context = DbFactory.CreateDbContext();` – Schoof Oct 12 '22 at 07:36
  • Very strange... The issue is not with my code, but with your copying it, and the fact that you get the same error. I can't yet explain this. I've added complete version of the code. See it under the `New Version` section. Copy the code as is. Don't assume that you've already have it. – enet Oct 12 '22 at 09:53
  • Yeah you're right, I tried again with a new Blazor Server project with Individual User Accounts and now it works. Maybe it's the RevalidatingIdentityAuthenticationStateProvider doing some work there. I can't really use that because in my application I have to use OpenIdConnect. I'll continue searching though, thank you! – Schoof Oct 12 '22 at 10:34
  • So I figured out it's the `ServiceLifetime.Scoped` from `services.AddDbContextFactory(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Scoped);` that makes it work. Does that not go against the [recommendations of Microsoft](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-6.0#database-access)? "...scoped service registrations can be problematic because the instance is shared across components within the user's circuit. DbContext isn't thread safe..." – Schoof Oct 12 '22 at 11:38
  • You'll continue searching for what? The RevalidatingIdentityAuthenticationStateProvider play no role whatsoever here. Your question was how to inject the `AuthenticationStateProvider` into DbContext. I provided the answer, exactly as it was stated in your question. Please accept the answer so that other users will know that it works. `I can't really use that because in my application I have to use OpenIdConnect.` That's your issue, but the question is how to inject the `AuthenticationStateProvider` into DbContext, not if you can use it or not. And please, don't take me for a fool. – enet Oct 12 '22 at 11:44
  • I'm the expert no. 1 of Stackoverflow regarding `OpenIdConnect` and security... Whatever Identity provider you may use has got nothing to do with the `AuthenticationStateProvider` service, which is a simple class that stores claims provided by different Identity providers – enet Oct 12 '22 at 11:44
  • 1
    What are you talking about, I'm not taking you for a fool, I'm trying to have a discussion and come to a solution for my issue. Also see my previous comment, the reason your code works is because you register the DbContextFactory as a Scoped service (instead of the default Singleton). Which I think (emphasise think) is not correct because of the Microsoft recommendation. Of course I could be wrong and I would be happy to be proven wrong. – Schoof Oct 12 '22 at 12:24
  • What is the `Microsoft recommendation?` Please, no link, just state it clearly. – enet Oct 12 '22 at 12:53
  • 1
    This is what I found in their documentation: 'The recommended approach to create a new DbContext with dependencies is to use a factory. EF Core 5.0 or later provides a built-in factory for creating new contexts.' `builder.Services.AddDbContextFactory(opt => opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));` And 'In Blazor Server apps, scoped service registrations can be problematic because the instance is shared across components within the user's circuit. DbContext isn't thread safe and isn't designed for concurrent use. ' – Schoof Oct 12 '22 at 12:58
  • 1
    `'In Blazor Server apps, scoped service registrations can be problematic because the instance is shared across components within the user's circuit.` But they are clearly say that all the three scopes are problematic in Blazor: `The existing lifetimes are inappropriate for these reasons.` Thier recommandation is 1. To use the DbContextFactory (Singleton by default) to cinfigure options. Note that Singleton is much more problematic than the other two scopes. 2. To use `one context per operation: using var context = new MyContext();` – enet Oct 12 '22 at 13:47
  • 1
    And that is exactly what my code does, except that it configures the DbContextFactory with Scoped lifetime instead of Singleton, so that I can inject the AuthenticationStateProvider into the ApplicationDbContext's constructor. They both must have the same scope: Scoped. All the code samples that you see, in the Docs, and elsewhere, including the answer by `MrC aka Shaun Curtis` use the DbContextFactory to create a singleton object, which serves all connected clients. And that is not an issue, as long as you use `one context per operation` – enet Oct 12 '22 at 13:47
  • Awesome thank you, that explains it then. Thank you for taking the time to explain everything, I will use this solution. – Schoof Oct 12 '22 at 15:09