Search code examples
c#asp.net.netasp.net-corehotchocolate

Using AddPooledDbContextFactory with AspNetCore.Identity


I've got asp.net core webapi app which currently uses Microsoft.AspNetCore.Identity and services.AddDbContext in ConfigureServices methods in Startup class and everything works so far.

I tried to use services.AddPooledDbContextFactory instead of services.AddDbContext to improve performance. However when I try to use AddPooledDbContextFactory I've got an error:

System.InvalidOperationException: Unable to resolve service for type 'hostapp.Data.DataContext' while attempting to activate 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.RoleStore5[hostapp.Models.AppRole,hostapp.Data.DataContext,System.Int32,hostapp.Models.AppUserRole,Microsoft.AspNetCore.Identity.IdentityRoleClaim1[System.Int32]]'

After seeing this error I created new webapi project which uses AddPooledDbContextFactory without Microsoft.AspNetCore.Identity which works fine. So my question is:

What is the proper way of using services.AddPooledDbContextFactory with Microsoft.AspNetCore.Identity to avoid error above?

in ProjectRoot/Startup.cs

using System.Threading.Tasks;
using hostapp.Data;
using hostapp.GraphQL;
using hostapp.Interfaces;
using hostapp.Models;
using hostapp.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace hostapp
{
    public class Startup
    {
        
        // ...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ICookieService, CookieService>();
            
            // EARLIER IMPLEMENTATION WHICH WORKS FINE =>
            // services.AddDbContext<DataContext>(options =>
            // {
            //     options.UseSqlServer(_config.GetConnectionString("DefaultConnection"));
            // });

            // CURRENT IMPLEMENTATION WHICH CAUSES AN ERROR
            services.AddPooledDbContextFactory<DataContext>(options =>
            {
                options.UseSqlServer(_config.GetConnectionString("DefaultConnection"));
            });

            services.AddIdentityCore<AppUser>(opt =>
            {
                opt.Password.RequireNonAlphanumeric = false;
            })
            .AddRoles<AppRole>()
            .AddRoleManager<RoleManager<AppRole>>()
            .AddSignInManager<SignInManager<AppUser>>()
            .AddRoleValidator<RoleValidator<AppRole>>()
            .AddEntityFrameworkStores<DataContext>();

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.Cookie.Name = "app-cookie";
                // options.Cookie.Expiration = TimeSpan.FromMinutes(30);
                options.Events.OnRedirectToLogin = context =>
                {
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return Task.CompletedTask;
                };
            });

            services.AddAuthorization(opt =>
            {
                opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
            });

            services
                .AddGraphQLServer()
                .AddAuthorization()
                .AddQueryType<Query>();

            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseCors(options => options
                .AllowAnyHeader()
                .AllowAnyMethod()
                .WithOrigins("https://localhost:4200")
                .SetIsOriginAllowed(origin => true)
                .AllowCredentials());

            app.UseCookiePolicy(new CookiePolicyOptions
            {
                MinimumSameSitePolicy = SameSiteMode.Strict,
                // HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
                // Secure = CookieSecurePolicy.Always
            });

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapGraphQL();
            });
        }
    }
}

in ProjectRoot/Data/DataContext.cs

using hostapp.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace hostapp.Data
{
    public class DataContext : IdentityDbContext<AppUser, AppRole, int,
                               IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
                               IdentityRoleClaim<int>, IdentityUserToken<int>>
    {
        public DataContext(DbContextOptions<DataContext> options) : base(options)
        {
        }

        public DbSet<AppUserClaim> Claims { get; set; }
        public DbSet<AppUserClaimOffender> Offenders { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<AppUser>()
                .HasMany(ur => ur.AppUserRoles)
                .WithOne(u => u.AppUser)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();

            builder.Entity<AppRole>()
                .HasMany(ur => ur.AppUserRoles)
                .WithOne(u => u.AppRole)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();
        }
    }
}

in ProjectRoot/app.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0"/>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0" NoWarn="NU1605"/>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0" NoWarn="NU1605"/>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3"/>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0"/>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0"/>
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.0"/>
    <PackageReference Include="HotChocolate.AspNetCore" Version="11.1.0"/>
    <PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="11.1.0"/>
    <PackageReference Include="HotChocolate.Data.EntityFramework" Version="11.1.0"/>
    <PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.2.0"/>
  </ItemGroup>
</Project>

Solution

  • What is the proper way of using services.AddPooledDbContextFactory with Microsoft.AspNetCore.Identity to avoid error above?

    In the ConfigureServices, try to use AddScoped() method to register the DBContext and use the provider to get the factory from the services. Then, return the instance to the provider. The code like this:

            services.AddPooledDbContextFactory<DataContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
                options.EnableSensitiveDataLogging();
            }, poolSize:32);
    
            services.AddScoped<DataContext>(p => p.GetRequiredService<IDbContextFactory<DataContext>>().CreateDbContext());