首次访问上下文触发Cannot access a disposed context instance异常求助
Cannot access a disposed context instance异常 问题背景
在Blazor Server项目中,首次访问数据库上下文(DbContext)就触发Cannot access a disposed context instance异常,所有服务(包括DbContext)均注册为Transient生命周期,异常在调用QRCodeService.GetQRCodeAsync时抛出,该方法内部调用仓储的异步查询方法。
关键代码片段
DbContext注册代码
public static IServiceCollection AddSQLContext<TContext>(this IServiceCollection services, string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where TContext : DbContext => services .AddDbContext<TContext>(options => options.UseSqlServer( connectionString, actions => actions.MigrationsAssembly("Discounted.Data") .EnableRetryOnFailure() ), serviceLifetime);
Blazor组件调用代码
protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { using (var source = new CancellationTokenSource()) { version = VersionService.Version; ProtectedBrowserStorageResult<string> customer = await ProtectedSessionStore.GetAsync<string>("customer"); DashboardDto.LocalUser = customer.Value; if (DashboardDto.LocalUser == null) return; // 获取订阅状态(不访问上下文) DashboardDto.LocalStatus = await SubscriptionService.GetSubscriptionStatusAsync(DashboardDto.LocalUser, source.Token); // 首次访问上下文,此处抛出异常 DashboardDto.StoreId = await QRCodeService.GetQRCodeAsync(DashboardDto.LocalUser, source.Token); DashboardDto.Rewards = await RewardService.GetRewardAsync(DashboardDto.StoreId, source.Token); } StateHasChanged(); } }
QRCodeService及仓储调用代码
public class QRCodeService : IQRCodeService { private IAccountRepository _accountRepository; public QRCodeService(IAccountRepository accountRepository) { _accountRepository = accountRepository; } public async Task<string?> GetQRCodeAsync(string customerId, CancellationToken cancellationToken) { // 在此处抛出异常 Account store = await _accountRepository.FirstOrDefaultAsync(x => x.StripeId == customerId, cancellationToken); return store.StoreCode.ToString(); } }
可能的原因及解决方案
1. Blazor组件生命周期与Transient服务的冲突
Blazor Server的组件是长生命周期对象,而Transient服务每次从DI容器获取时都会创建新实例。但如果组件直接注入Transient服务,服务内部的DbContext可能会因为电路(Circuit)的范围变更被提前释放,即使首次调用也可能出现问题。
解决方案:在组件中使用IServiceScopeFactory创建临时服务范围,确保DbContext和相关服务在操作期间处于活跃状态:
// 先在组件构造函数注入IServiceScopeFactory private readonly IServiceScopeFactory _serviceScopeFactory; public YourComponent(IServiceScopeFactory serviceScopeFactory, ...) { _serviceScopeFactory = serviceScopeFactory; // 其他注入项 } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { using var scope = _serviceScopeFactory.CreateScope(); var qrCodeService = scope.ServiceProvider.GetRequiredService<IQRCodeService>(); var subscriptionService = scope.ServiceProvider.GetRequiredService<ISubscriptionService>(); var rewardService = scope.ServiceProvider.GetRequiredService<IRewardService>(); using (var source = new CancellationTokenSource()) { version = VersionService.Version; ProtectedBrowserStorageResult<string> customer = await ProtectedSessionStore.GetAsync<string>("customer"); DashboardDto.LocalUser = customer.Value; if (DashboardDto.LocalUser == null) return; DashboardDto.LocalStatus = await subscriptionService.GetSubscriptionStatusAsync(DashboardDto.LocalUser, source.Token); DashboardDto.StoreId = await qrCodeService.GetQRCodeAsync(DashboardDto.LocalUser, source.Token); DashboardDto.Rewards = await rewardService.GetRewardAsync(DashboardDto.StoreId, source.Token); } StateHasChanged(); } }
2. DbContext注册存在冲突
检查项目中是否有其他地方重复注册了同一个DbContext类型,比如同时使用AddDbContext和AddSQLContext注册同一个TContext,导致生命周期设置被覆盖,出现上下文提前释放的情况。
解决方案:确保DbContext只通过AddSQLContext注册一次,或者统一生命周期配置。
3. 改用Scoped生命周期的DbContext
Blazor Server中,DbContext的推荐生命周期是Scoped(对应电路范围),Transient生命周期在Blazor的长连接场景下容易出现释放问题。
解决方案:修改DbContext的注册为Scoped:
// 调用AddSQLContext时指定ServiceLifetime.Scoped services.AddSQLContext<YourDbContext>(connectionString, ServiceLifetime.Scoped);
同时确保仓储和服务的生命周期与DbContext匹配(比如仓储也注册为Scoped),避免上下文被提前释放。
4. 排查同步阻塞异步代码的情况
检查项目中是否存在同步调用异步方法的场景(比如使用.Result、.Wait()),这种情况会导致线程阻塞,进而触发DbContext被提前释放。
解决方案:确保所有异步方法都通过await调用,避免同步阻塞。
5. 启用DbContext日志排查生命周期
通过启用DbContext的日志,查看上下文的创建和释放时间,确认是否在查询操作前就被释放:
public static IServiceCollection AddSQLContext<TContext>(this IServiceCollection services, string connectionString, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where TContext : DbContext => services .AddDbContext<TContext>(options => options.UseSqlServer( connectionString, actions => actions.MigrationsAssembly("Discounted.Data") .EnableRetryOnFailure() ) .LogTo(Console.WriteLine, LogLevel.Information), // 添加日志 serviceLifetime);
查看日志中DbContext的Created和Disposed事件,判断是否在FirstOrDefaultAsync调用前就已经被释放。
内容的提问来源于stack exchange,提问作者Cornelis

