Blazor Server中DbContext获取当前登录用户的异常问题求助
你遇到的错误
Cannot resolve scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider' from root provider,本质是生命周期不匹配:DbContextFactory默认是Singleton服务,它创建DbContext时使用的是根服务容器,而AuthenticationStateProvider是Scoped服务(每个用户会话一个实例),根容器无法解析Scoped服务,否则会导致Scoped服务被Singleton持有,引发线程安全和数据混淆问题。
以下是三个保留DbContextFactory核心优势的可行解决方案:
方案1:调用DbContext时手动传入用户ID(推荐,简单直接)
不在DbContext中依赖AuthenticationStateProvider,而是在业务层先获取用户信息,再传递给DbContext用于审计逻辑。
步骤1:修改CustomDbContext,添加UserId属性
public partial class CustomDbContext : DbContext { // 存储当前操作的用户ID public string? UserId { get; set; } // 保留无AuthenticationStateProvider依赖的构造函数 public CustomDbContext(DbContextOptions<CustomDbContext> options) : base(options) { } // 重写SaveChanges处理审计逻辑 public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { if (!string.IsNullOrEmpty(UserId)) { // 示例:给审计实体设置创建/修改人信息 foreach (var entry in ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified)) { if (entry.Entity is IAuditableEntity auditable) { if (entry.State == EntityState.Added) { auditable.CreatedBy = UserId; auditable.CreatedAt = DateTime.UtcNow; } auditable.UpdatedBy = UserId; auditable.UpdatedAt = DateTime.UtcNow; } } } return await base.SaveChangesAsync(cancellationToken); } }
步骤2:在组件/服务中获取用户ID并传入DbContext
@inject IDbContextFactory<CustomDbContext> DbContextFactory @inject AuthenticationStateProvider AuthStateProvider async Task SaveEntity() { // 获取当前用户认证状态 var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var userId = authState.User.Claims.FirstOrDefault(c => c.Type == "userId")?.Value ?? string.Empty; // 创建DbContext并设置用户ID using var context = DbContextFactory.CreateDbContext(); context.UserId = userId; // 执行数据操作 context.YourEntities.Add(new YourEntity { /* 属性赋值 */ }); await context.SaveChangesAsync(); }
方案2:封装Scoped服务统一获取带用户信息的DbContext
如果不想每次手动传UserId,可以封装一个Scoped服务,自动处理用户信息注入,同时保留DbContextFactory的Singleton特性。
步骤1:创建Scoped上下文提供者
public interface IScopedDbContextProvider { Task<CustomDbContext> GetDbContextAsync(); } public class ScopedDbContextProvider : IScopedDbContextProvider { private readonly IDbContextFactory<CustomDbContext> _dbContextFactory; private readonly AuthenticationStateProvider _authStateProvider; public ScopedDbContextProvider(IDbContextFactory<CustomDbContext> dbContextFactory, AuthenticationStateProvider authStateProvider) { _dbContextFactory = dbContextFactory; _authStateProvider = authStateProvider; } public async Task<CustomDbContext> GetDbContextAsync() { var context = _dbContextFactory.CreateDbContext(); var authState = await _authStateProvider.GetAuthenticationStateAsync(); context.UserId = authState.User.Claims.FirstOrDefault(c => c.Type == "userId")?.Value ?? string.Empty; return context; } }
步骤2:注册Scoped服务
在Program.cs中添加:
builder.Services.AddScoped<IScopedDbContextProvider, ScopedDbContextProvider>();
步骤3:在组件/服务中使用
@inject IScopedDbContextProvider DbContextProvider async Task SaveData() { using var context = await DbContextProvider.GetDbContextAsync(); // 直接使用上下文,已自动设置UserId context.YourEntities.Add(new YourEntity()); await context.SaveChangesAsync(); }
方案3:使用EF Core拦截器实现审计(解耦审计逻辑)
将审计逻辑从DbContext中分离,用EF Core拦截器处理,通过拦截器传递用户ID,避免DbContext依赖认证服务。
步骤1:创建审计拦截器
public class AuditInterceptor : SaveChangesInterceptor { private string? _userId; // 设置当前操作的用户ID public void SetUserId(string userId) { _userId = userId; } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { if (!string.IsNullOrEmpty(_userId) && eventData.Context is not null) { // 遍历实体处理审计逻辑 foreach (var entry in eventData.Context.ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified)) { if (entry.Entity is IAuditableEntity auditable) { if (entry.State == EntityState.Added) { auditable.CreatedBy = _userId; auditable.CreatedAt = DateTime.UtcNow; } auditable.UpdatedBy = _userId; auditable.UpdatedAt = DateTime.UtcNow; } } } return base.SavingChangesAsync(eventData, result, cancellationToken); } }
步骤2:注册拦截器到DbContextFactory
builder.Services.AddDbContextFactory<CustomDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); // 添加审计拦截器 options.AddInterceptors(new AuditInterceptor()); });
步骤3:在调用时设置用户ID
@inject IDbContextFactory<CustomDbContext> DbContextFactory @inject AuthenticationStateProvider AuthStateProvider async Task SaveEntity() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var userId = authState.User.Claims.FirstOrDefault(c => c.Type == "userId")?.Value ?? string.Empty; using var context = DbContextFactory.CreateDbContext(); // 获取拦截器并设置用户ID var auditInterceptor = context.GetService<AuditInterceptor>(); auditInterceptor?.SetUserId(userId); context.YourEntities.Add(new YourEntity()); await context.SaveChangesAsync(); }
以上方案均保留了DbContextFactory的核心优势(控制上下文生命周期、支持并发场景),同时规避了Scoped服务注入Singleton的风险。其中方案1最直接简单,适合大多数场景;方案2适合需要复用上下文获取逻辑的场景;方案3适合审计逻辑复杂、需要与DbContext解耦的场景。
内容的提问来源于stack exchange,提问作者Schoof

