1

I had previously asked a question that was answered properly, but the problem is that when my custom AuthenticationStateProvider is registered as a scoped

services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();

I get the following error:

System.InvalidOperationException: GetAuthenticationStateAsync was called before SetAuthenticationState

But, when it is registered as a singleton, it works correctly, However, the single instance creates for the lifetime of the application domain by AddSingelton, and so this is not good.(Why? Because of :))

What should I do to register my custom AuthenticationStateProvider as a scoped, but its value is not null?

Edit:
According to @MrC aka Shaun Curtis Comment:
It's my CustomAuthenticationStateProvider:

 public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
    {
        private readonly IServiceScopeFactory _scopeFactory;

        public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
            : base(loggerFactory) =>
            _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));

        protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);

        protected override async Task<bool> ValidateAuthenticationStateAsync(
            AuthenticationState authenticationState, CancellationToken cancellationToken)
        {
            // Get the user from a new scope to ensure it fetches fresh data
            var scope = _scopeFactory.CreateScope();
            try
            {
                var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
                return await ValidateUserAsync(userManager, authenticationState?.User);
            }
            finally
            {
                if (scope is IAsyncDisposable asyncDisposable)
                {
                    await asyncDisposable.DisposeAsync();
                }
                else
                {
                    scope.Dispose();
                }
            }
        }

        private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
        {
            if (principal is null)
            {
                return false;
            }

            var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
            if (!int.TryParse(userIdString, out var userId))
            {
                return false;
            }

            var user = await userManager.FindUserAsync(userId);
            return user is not null;
        }
    }

And it's a program configuration and service registration:

public void ConfigureServices(IServiceCollection services)
{

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

#region Authentication
//Authentication
services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(
        Configuration.GetConnectionString("LocalDBConnection"),
        serverDbContextOptionsBuilder =>
        {
            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
            serverDbContextOptionsBuilder.CommandTimeout(minutes);
            serverDbContextOptionsBuilder.EnableRetryOnFailure();
        })
        .AddInterceptors(new CorrectCommandInterceptor()); ;
});
//add policy
services.AddAuthorization(options =>
{
    options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
    options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
});
// Needed for cookie auth.
services
    .AddAuthentication(options =>
    {
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(options =>
    {
        options.SlidingExpiration = false;
        options.LoginPath = "/";
        options.LogoutPath = "/login";
        //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
        options.Cookie.Name = ".my.app1.cookie";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = context =>
            {
                var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                return cookieValidatorService.ValidateAsync(context);
            }
        };
    });
#endregion

//AutoMapper
services.AddAutoMapper(typeof(MappingProfile).Assembly);

//CustomAuthenticationStateProvider
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
.
.
}
Arani
  • 891
  • 7
  • 18
  • I've had a quick look at the previous question and the custom state provider suggested in that looks suspect. I'm pretty sure creating a new Service Scope is the source of your problem. Can you add your actual `CustomAuthenticationStateProvider` to this question and your service registration in program. – MrC aka Shaun Curtis Oct 31 '22 at 15:18
  • Sure, I added them to my question. – Arani Nov 01 '22 at 05:21

2 Answers2

2

Don't worry about the AddSingelton in the Blazor apps. Scoped dependencies act the same as Singleton registered dependencies in Blazor apps (^).

  • Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
  • The Blazor Server hosting model supports the Scoped lifetime across HTTP requests (Just for the Razor Pages or MVC portion of the app) but not across SignalR connection/circuit messages among components that are loaded on the client.

That's why there's a scope.ServiceProvider.GetRequiredService here to ensure the retrived user is fetched from a new scope and has a fresh data. Actually this solution is taken from the Microsoft's sample.

VahidN
  • 18,457
  • 8
  • 73
  • 117
  • So the answer given in https://stackoverflow.com/a/72224965/4444757 is wrong, isn't it? – Arani Nov 02 '22 at 07:34
  • 1
    No. It's about the ASP.NET Core and its `HTTP requests`. A Blazor Server app works inside of a `web-socket` connection/`SignalR` connection. There is no `HTTP request` here and inside of a `web-socket` connection there is no difference between an scoped and a singleton service. – VahidN Nov 02 '22 at 08:09
  • "Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services." Not strictly true. Only in the context of the Blazor SPA session running in the Browser. If you create your own container, such as in `OwningComponentBase` or in the code above then scoped services are created in that container while Singleton services are requested from the SPA session container. – MrC aka Shaun Curtis Nov 02 '22 at 08:31
  • 1
    "The Blazor Server hosting model ... among components that are loaded on the client". No, in Blazor Server everything happens in the Blazor Hub session running on the server. Components are created and managed by the Renderer in the Hub Session. Each SPA session within the Hub has it's own DI container that hosts Scoped and Transient Services. It's created when the Hub Session starts and disposed when the hub session finishes. – MrC aka Shaun Curtis Nov 02 '22 at 09:02
  • This not a "No". It's just a confirmation and extra explanation of the concept. – VahidN Nov 02 '22 at 09:09
  • 1
    Here is sample to make sense of it: https://blazor-university.com/dependency-injection/dependency-lifetimes-and-scopes/comparing-dependency-scopes/ – VahidN Nov 02 '22 at 09:23
  • But now I ran into this problem @VahidN in production phase on Windows server IIS server. https://stackoverflow.com/questions/74626721/blazor-server-cookie-based-authentication-set-the-last-logged-in-user-for-all-c – Arani Dec 03 '22 at 07:19
0

Your problem is probably here:

var scope = _scopeFactory.CreateScope();
/...
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();

You create a new IOC container and request the instance of IUsersService from that container.

If IUsersService is Scoped, it creates a new instance.

IUsersService requires various other services which the new container must provide.

public UsersService(IUnitOfWork uow, ISecurityService securityService, ApplicationDbContext dbContext, IMapper mapper)

Here's the definition of those services in Startup:

services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IRolesService, RolesService>();
services.AddScoped<ISecurityService, SecurityService>();
services.AddScoped<ICookieValidatorService, CookieValidatorService>();
services.AddScoped<IDbInitializerService, DbInitializerService>();

IUnitOfWork and ISecurityService are both Scoped, so it creates new instances of these in the the new Container. You almost certainly don't want that: you want to use the ones from the Hub SPA session container.

You have a bit of a tangled web so without a full view of everything I can't be sure how to restructure things to make it work.

One thing you can try is to just get a standalone instance of IUsersService from the IOC container using ActivatorUtilities. This instance gets instantiated with all the Scoped services from the main container. Make sure you Dispose it if it implements IDisposable.

public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
    {
        private readonly IServiceProvider _serviceProvider;

        public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
            : base(loggerFactory) =>
            _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(scopeFactory));

        protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);

        protected override async Task<bool> ValidateAuthenticationStateAsync(
            AuthenticationState authenticationState, CancellationToken cancellationToken)
        {
            // Get an instance of IUsersService from the IOC Container Service to ensure it fetches fresh data
            IUsersService userManager = null;
            try
            {
                userManager = ActivatorUtilities.CreateInstance<IUsersService>(_serviceProvider);
                return await ValidateUserAsync(userManager, authenticationState?.User);
            }
            finally
            {
                userManager?.Dispose();
            }
        }

        private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
        {
            if (principal is null)
            {
                return false;
            }

            var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
            if (!int.TryParse(userIdString, out var userId))
            {
                return false;
            }

            var user = await userManager.FindUserAsync(userId);
            return user is not null;
        }
    }

For reference this is my test code using the standard ServerAuthenticationStateProvider in a Blazor Server Windows Auth project.

    public class MyAuthenticationProvider : ServerAuthenticationStateProvider
    {
        IServiceProvider _serviceProvider;

        public MyAuthenticationProvider(IServiceProvider serviceProvider, MyService myService)
        {
            _serviceProvider = serviceProvider;
        }

        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            MyService? service = null;
            try
            {
                service = ActivatorUtilities.CreateInstance<MyService>(_serviceProvider);
                // Do something with service
            }
            finally
            {
                service?.Dispose();
            }
            return base.GetAuthenticationStateAsync();
        }
    }
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thanks, but it gives that error again with your method.`System.InvalidOperationException: GetAuthenticationStateAsync was called before SetAuthenticationState` – Arani Nov 02 '22 at 06:25
  • I did say try. The problem is that implementing Authentication is not easy. I looked at the repo you used and decided not to download and run it as I wasn't sure of it's provenence without reviewing all the code first. The whole process of creating a service container to ensure you get a new instance of a service strikes me as a cludge. Without a full view of the code or a minimum reproducible version it's very hard to work out what's going wrong. – MrC aka Shaun Curtis Nov 02 '22 at 08:58