Home > OS >  Is there a way to inject a scoped service into a custom Controller Activator (or Page Model Activato
Is there a way to inject a scoped service into a custom Controller Activator (or Page Model Activato

Time:04-06

I am working on an ASP.NET Core app and am using a custom PageModelActivatorProvider to create my razor pages. This app has user's and implements Identity using AddIdentityCore. I have added a SignInManager and authentication as well:

builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
    options.SignIn.RequireConfirmedEmail = true;
    options.User.RequireUniqueEmail = true;
})
.AddSignInManager<CustomSignInManager>();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignOutScheme = IdentityConstants.ApplicationScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
    options.LoginPath = new PathString("/Account/Login");
    options.Events = new CookieAuthenticationEvents
    {
        OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
    };
});

I have created a custom UserStore and added it as a scoped service within my Program.cs file:

string connectionString = builder.Configuration.GetConnectionString("connectionString");

builder.Services.AddSingleton<IPageModelActivatorProvider, WebUIPageModelActivatorProvider>();
builder.Services.AddScoped<IUserRepository>(_ => new UserRepository(connectionString));
builder.Services.AddScoped<IUserStore<ApplicationUser>, UserStore>();

The UserManager and SignInManager classes are added as scoped services within AddIdentityCore and AddSignInManager methods, respectively.

My PageModelActivatorProvider class creates a LoginModel page model, which takes a UserManager and a SignInManager. Both of these are scoped services, however my PageModelActivatorProvider is registered as a singleton service, so I cannot inject the UserManager and SignInManager via constructor injection. When I try using the IServiceProvider to GetRequiredService, I get the following exception:

'Cannot resolve scoped service 'Microsoft.AspNetCore.Identity.IUserStore`1[DataAccess.ApplicationUser]' from root provider.'

I get the same exception for UserManager and SignInManager because they are all registered as scoped services and I am trying to access them within a singleton service.

public sealed class WebUIPageModelActivatorProvider : IPageModelActivatorProvider, IDisposable
{
    private readonly IServiceProvider _services;
    
    public WebUIPageModelActivatorProvider(IServiceProvider services)
    {
        // Create Singleton components
        _services = services;
    }

    public Func<PageContext, object> CreateActivator(CompiledPageActionDescriptor descriptor)
    {
        return (context) => _CreatePageModelType(context, descriptor.ModelTypeInfo!.AsType());
    }

    public Action<PageContext, object>? CreateReleaser(CompiledPageActionDescriptor descriptor)
    {
        return (context, pageModel) => (pageModel as IDisposable)?.Dispose();
    }

    private object _CreatePageModelType(PageContext context, Type pageModelType)
    {
        // Scoped components for Identity Core
        IUserStore<ApplicationUser> userStore = _services.GetRequiredService<IUserStore<ApplicationUser>>();
        UserManager<ApplicationUser> userManager = _services.GetRequiredService<UserManager<ApplicationUser>>()!;
        SignInManager<ApplicationUser> signInManager = _services.GetRequiredService<SignInManager<ApplicationUser>>()!;

        // Create Transient components
        switch (pageModelType.Name)
        {
            case nameof(IndexModel):
                return new IndexModel();

            case nameof(LoginModel):
                return new LoginModel(userManager, signInManager, Logger<LoginModel>());

            default: throw new NotImplementedException(pageModelType.FullName);
        }
    }

    public void Dispose()
    {
        // Release singleton components here, if needed
    }

    private ILogger<T> Logger<T>() => this._loggerFactory.CreateLogger<T>();

Next I tried creating an IServiceScope to resolve my scoped services:


...
private object _CreatePageModelType(PageContext context, Type pageModelType)
    {
        using (var scope = _services.CreateScope())
        {
            // Scoped components for Identity Core
            IUserStore<ApplicationUser> userStore = scope.ServiceProvider.GetRequiredService<IUserStore<ApplicationUser>>();
            UserManager<ApplicationUser> userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>()!;
            SignInManager<ApplicationUser> signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<ApplicationUser>>()!;

            // Create Transient components
            switch (pageModelType.Name)
            {
                case nameof(IndexModel):
                    return new IndexModel();

                case nameof(LoginModel):
                    return new LoginModel(userManager, signInManager, Logger<LoginModel>());

                default: throw new NotImplementedException(pageModelType.FullName);
            }
        }
    }
...

This method successfully created the scoped services and injected them into my LoginModel, however, after they were injected, they were disposed of and I received the following exception:

ObjectDisposedException: Cannot access a disposed object. Object name: 'UserManager`1'.

Is there a way to inject a scoped service into a custom PageModelActivatorProvider?

CodePudding user response:

As discussed in the comments, inject the IHttpContextAccessor into the singleton service, and use httpContextAccessor.HttpContext.RequestServices to get the IServiceProvider for the current request which you can use to resolve your scoped services.

public sealed class WebUIPageModelActivatorProvider : IPageModelActivatorProvider, IDisposable
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public WebUIPageModelActivatorProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    private object _CreatePageModelType(PageContext context, Type pageModelType)
    {
        IServiceProvider services = _httpContextAccessor.HttpContext.RequestServices;
        
        // Scoped components for Identity Core
        IUserStore<ApplicationUser> userStore = services.GetRequiredService<IUserStore<ApplicationUser>>();
        UserManager<ApplicationUser> userManager = services.GetRequiredService<UserManager<ApplicationUser>>()!;
        SignInManager<ApplicationUser> signInManager = services.GetRequiredService<SignInManager<ApplicationUser>>()!;

        ...
  • Related