.NET Core EF+Identity迁移PostgreSQL连接执行异常解决方案
问题根因
你碰到的NpgsqlOperationInProgressException本质是两个问题叠加导致的:
- Npgsql(PostgreSQL的EF Core驱动)不支持SQL Server自带的MARS(多活动结果集)功能,同一个DbContext对应的底层数据库连接,同一时间只能执行一个操作,绝对不支持并发请求,这和SQL Server的默认容错行为完全不同。
- 你的Seeder代码里混用
async/await和.Result同步阻塞异步方法,这种写法本身就会导致异步操作还没执行完就进入下一个数据库调用,在SQL Server环境下靠MARS蒙混过关没报错,切到PostgreSQL就直接触发异常。
第一步:修复服务注册,同时兼容DbContext工厂和Identity
你之前只注册AddDbContextFactory导致Identity找不到DbContext实例的问题很好解决,两个注册方法同时使用即可,它们共享同一套配置,不会产生冲突:
// 同时注册标准DbContext实例和DbContext工厂,共享相同连接配置 var dbConnectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(dbConnectionString)); services.AddDbContextFactory<ApplicationDbContext>(options => options.UseNpgsql(dbConnectionString), ServiceLifetime.Scoped); // 种子服务、HttpContext注册保持原有逻辑 services.AddScoped<Seeder>(); services.AddHttpContextAccessor(); // Identity配置不需要改动,会自动解析上面注册的ApplicationDbContext实例 services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true) .AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); // 原有的Identity密码、锁定策略配置完全保留即可 services.Configure<IdentityOptions>(o => { o.Password.RequireDigit = true; o.Password.RequireLowercase = true; o.Password.RequireNonAlphanumeric = true; o.Password.RequireUppercase = true; o.Password.RequiredLength = 6; o.Password.RequiredUniqueChars = 1; o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); o.Lockout.MaxFailedAccessAttempts = 5; o.Lockout.AllowedForNewUsers = true; o.User.RequireUniqueEmail = true; });
这套注册逻辑下,Web请求管道里的Identity组件(登录鉴权中间件、Controller注入的UserManager/SignInManager)默认用Scoped生命周期的标准DbContext即可,因为正常Web请求流程里数据库操作都是串行执行的,不会触发并发问题。只有种子初始化、后台任务、并行数据库操作的场景,才需要注入IDbContextFactory<ApplicationDbContext>创建独立上下文实例使用。
第二步:修复Seeder代码的异步阻塞问题
你原来的Seeder有两个不规范的点:一是手动实例化RoleStore直接操作DbContext,和UserManager共享上下文容易产生操作冲突;二是用.Result阻塞异步方法,直接导致连接状态异常。修正后的代码如下:
using SomeApp.Data; using SomeApp.Models; using Microsoft.AspNetCore.Identity; using System.Linq; using System.Threading.Tasks; namespace SomeApp { public class Seeder { private readonly RoleManager<IdentityRole> _roleManager; private readonly UserManager<User> _userManager; // 直接注入RoleManager,不需要手动操作DbContext处理角色逻辑 public Seeder(RoleManager<IdentityRole> roleManager, UserManager<User> userManager) { _roleManager = roleManager; _userManager = userManager; } public async Task Seed() { var roles = new[] { "Superadmin", "User" }; foreach (var role in roles) { // 全程使用异步方法,禁止混用同步查询、.Result阻塞 if (await _roleManager.RoleExistsAsync(role)) continue; await _roleManager.CreateAsync(new IdentityRole(role) { NormalizedName = role.ToUpperInvariant() }); // RoleManager内部会自动调用SaveChanges,不需要手动操作DbContext提交 } // 用await等待异步方法执行完成,绝对不要用.Result if (await _userManager.FindByNameAsync("admin1234") == null) await CreateDefaultUser(); } private async Task CreateDefaultUser() { var user = new User { Email = "admin@admin.pl", FirstName = "Admin", LastName = "Admin", UserName = "admin1234", Department = "IT", ImagePath = "/assets/user_icon.png", IsAnonimised = false, IsLoggedIn = true }; // 同样使用await等待创建完成,禁止用.Result var result = await _userManager.CreateAsync(user, "Test1234_"); if (result.Succeeded) { await _userManager.AddToRoleAsync(user, "Superadmin"); } } } }
额外注意事项
- 如果你需要在后台任务、并行逻辑里操作数据库,再注入
IDbContextFactory<ApplicationDbContext>,每次使用时调用CreateDbContext()获取独立实例,用完及时释放即可。如果后台任务需要用到Identity组件,也可以通过工厂创建的上下文手动实例化UserStore/RoleStore,再构造对应的Manager实例,不要直接用请求管道注入的实例。 - 你的
ApplicationDbContext里重复声明了Users、Roles两个DbSet,这两个属性已经在IdentityDbContext基类中内置,重复声明不会报错但属于冗余代码,可以删除。 - 注意你
OnModelCreating里配置的是Role实体,但Identity服务注册时用的是内置IdentityRole,如果你没有自定义Role类继承自IdentityRole,这段配置不会生效,建议统一类型。 - 所有EF Core、Identity的异步方法必须全程用
await关键字等待执行完成,禁止用.Result、.Wait()做同步阻塞,这类写法在Npgsql下几乎百分百会触发连接状态异常。 - Npgsql默认启用连接池,通过工厂创建上下文实例的性能开销极低,不需要担心性能问题。
内容的提问来源于stack exchange,提问作者Adam Kwiatek
相关产品推荐
相关产品推荐

