调用事件处理器并使用DBContext时抛出‘已释放上下文’异常求助
我有一个处理文件上传的Service,上传完成后会触发UploadCompleted事件。在事件处理器中,需要设置“上传完成”状态并写入“上传完成时间”,但从数据库获取实体时触发了“已释放上下文”异常。
异常信息
"Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances."
环境说明
SpotUploadDbContext通过构造函数依赖注入实例化,服务配置为Scoped生命周期FileService本身的生命周期为Transient
相关代码
public FileService( SpotUploadDbContext dbContext, IHttpContextAccessor httpContextAccessor, IOptions<AppSettings> appSettings, IOptions<FolderSettings> folderSettings, IStringLocalizer<SharedResource> stringLocalizer, IWebHostEnvironment hostEnvironment) { this._dbContext = dbContext; this._httpContextAccessor = httpContextAccessor; this._appSettings = appSettings.Value; this._folderSettings = folderSettings.Value; this._defaultFormOptions = new FormOptions { MultipartBoundaryLengthLimit = int.MaxValue }; this._stringLocalizer = stringLocalizer; this.hostEnvironment = hostEnvironment; // Bind event here this.UploadCompleted += this.FileService_UploadCompleted; } public async Task UploadFileAsync(Guid spotId) { // ... var formAccumulator = new KeyValueAccumulator(); var boundary = RequestHelpers.GetBoundary( MediaTypeHeaderValue.Parse(request.ContentType), this._defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, request.Body); var section = await reader.ReadNextSectionAsync(); var fileId = spotId.ToString(); UploadCompletedEventArgs eventArgs = null; var extension = string.Empty; var trustedFileName = string.Empty; var fileName = "testfile.mp3"; while (section is not null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { // ... if (RequestHelpers.HasFileContentDisposition(contentDisposition)) { trustedFileName = fileId + extension; var targetStream = this.CreateFileStreamFromUploadAsync(fileId, extension); eventArgs ??= new UploadCompletedEventArgs { FileId = fileId, OriginalFileName = fileName, TrustedFileName = trustedFileName, FileSize = contentDisposition.Size, FilePath = targetStream.Name, SpotId = spotEntity.Id }; await section.Body.CopyToAsync(targetStream); } else if (RequestHelpers.HasFormDataContentDisposition(contentDisposition)) { var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name); var encoding = GetEncoding(section); using var streamReader = new StreamReader(section.Body, encoding, true, 1024, true); var value = await streamReader.ReadToEndAsync(); formAccumulator.Append(key.Value, value); // ... } } // Drain any remaining section body that hasn't been consumed and // read the headers for the next section. section = await reader.ReadNextSectionAsync(); } // Multipartreader should be finished here (while loops stopped) // Raise upload completed event this.UploadCompleted?.Invoke(this, eventArgs); // ... await this._dbContext.SaveChangesAsync(); } private async void FileService_UploadCompleted(object sender, UploadCompletedEventArgs e) { var uploadState = await this._dbContext.LovSpotUploadState.FirstOrDefaultAsync(x => x.Id == SpotUploadStates.UploadFinished); // Exception is thrown here! var spot = await this._dbContext.Spots.FirstOrDefaultAsync(x => x.Id == e.SpotId); spot.UploadEndDate = DateTime.Now; spot.SpotUploadState = uploadState; await this._dbContext.SaveChangesAsync(); }
问题原因
async void的异步特性:事件处理器使用async void,这种方法的异步执行不会被调用方等待。当UploadFileAsync执行完毕并退出当前请求的Scoped生命周期后,DI容器会释放SpotUploadDbContext,但此时事件处理器可能仍在后台运行,导致访问已释放的上下文。- 生命周期不匹配:Transient的
FileService持有Scoped的SpotUploadDbContext,Transient实例可能在Scoped上下文被释放后仍然存活,此时访问上下文必然报错。
解决方案
方案1:移除事件模式,直接异步调用处理逻辑
取消事件触发,在上传完成后直接调用异步处理方法,确保上下文在存活期间完成操作:
// 移除构造函数中的事件绑定 // this.UploadCompleted += this.FileService_UploadCompleted; public async Task UploadFileAsync(Guid spotId) { // ... 上传逻辑 ... // 直接调用处理方法 if (eventArgs != null) { await FileService_UploadCompletedAsync(eventArgs); } await this._dbContext.SaveChangesAsync(); } // 将事件处理器改为async Task方法 private async Task FileService_UploadCompletedAsync(UploadCompletedEventArgs e) { var uploadState = await this._dbContext.LovSpotUploadState.FirstOrDefaultAsync(x => x.Id == SpotUploadStates.UploadFinished); var spot = await this._dbContext.Spots.FirstOrDefaultAsync(x => x.Id == e.SpotId); spot.UploadEndDate = DateTime.Now; spot.SpotUploadState = uploadState; await this._dbContext.SaveChangesAsync(); }
方案2:使用IServiceScopeFactory创建独立上下文
如果必须保留事件模式,在事件处理器中创建新的Scoped上下文,避免使用已释放的实例:
// 注入IServiceScopeFactory到FileService public FileService( SpotUploadDbContext dbContext, IHttpContextAccessor httpContextAccessor, IOptions<AppSettings> appSettings, IOptions<FolderSettings> folderSettings, IStringLocalizer<SharedResource> stringLocalizer, IWebHostEnvironment hostEnvironment, IServiceScopeFactory scopeFactory) // 新增注入 { this._dbContext = dbContext; this._scopeFactory = scopeFactory; // ... 其他初始化 ... this.UploadCompleted += this.FileService_UploadCompleted; } private async void FileService_UploadCompleted(object sender, UploadCompletedEventArgs e) { // 创建新的Scope,获取独立的DBContext实例 using var scope = _scopeFactory.CreateScope(); var scopedDbContext = scope.ServiceProvider.GetRequiredService<SpotUploadDbContext>(); var uploadState = await scopedDbContext.LovSpotUploadState.FirstOrDefaultAsync(x => x.Id == SpotUploadStates.UploadFinished); var spot = await scopedDbContext.Spots.FirstOrDefaultAsync(x => x.Id == e.SpotId); spot.UploadEndDate = DateTime.Now; spot.SpotUploadState = uploadState; await scopedDbContext.SaveChangesAsync(); }
方案3:调整Service生命周期为Scoped
将FileService的生命周期改为Scoped,使其与SpotUploadDbContext的生命周期保持一致,避免持有已释放的上下文:
// 在Program.cs/Startup.cs中配置服务 services.AddScoped<FileService>();
内容的提问来源于stack exchange,提问作者mimimi

