如何解决使用后台或托管服务时无法访问已处理的对象
我正在尝试使用 BackgroundService
或 IHostedService
,然后让它调用另一个类来使用 DB Context 执行数据库操作。我遇到的问题是,当我执行数据库操作时,出现以下异常:
System.ObjectdisposedException: '无法访问已处理的上下文 实例。
此异常最初是在此调用堆栈中引发的:Microsoft.EntityFrameworkCore.DbContext.Checkdisposed()
对象名称:DBContext
来源:Microsoft.EntityFrameworkCore
我在 MyBusinessService Update
函数的第一行放置了一个断点并检查了 DBContext
,它显示 ChangeTracker
和 Database
都已经抛出了 {{1} 异常}.因此,在调用 System.ObjectdisposedException
之前,DBContext
在某个地方被处理,我只是不确定在哪里。
Update()
解决方法
FSW 事件处理程序应尽可能短,否则有丢失事件的风险。 ObjectDisposedException
文本丢失,因此无法知道处理了什么。也许是 _run
对象?还有什么?如果是 _run
,很可能没有等待 ExecuteAsync
并且整个班级最终都被处理掉了。
为了避免丢失事件,FSW 事件处理程序应该将实际作业卸载到另一个工作器,通常是通过队列。工作人员应该负责创建他们需要的资源和连接。最后,应用程序实际上应该等待所有这些事情完成。
使用 Channel
使这相对容易:
var channel = Channel.CreateUnbounded<FileSystemEventArgs>();
var writer = channel.Writer;
var reader=channel.Reader;
...
watcher.Created += (o,e)=>writer.TryWrite(e);
这足以发布事件。一个工作方法可以异步地从那个通道读取并做任何需要做的事情:
async ProcessFiles(ChannelReader<FileSystemEventArgs> input)
{
using var run = new MyEtlScript();
await foreach(var evt in input.ReadAllAsync())
{
if(args.ChangeType == WatcherChangeTypes.Created)
{
await _run.ReadCSV(evt);
}
}
}
这个worker可以用于:
var workerTask= ProcessFiles(reader);
要停止处理,除了禁用 FSW,我们还需要调用 ChannelWriter.Complete
。工作人员将继续处理任何剩余的事件:
watcher.EnableRaisingEvents=false;
writer.Complete();
await workerTask;
所有这些都可以捆绑在一个类中:
class FileImporter : IDisposable
{
FileSystemWatcher _watcher;
Channel<FileSystemEventArgs> _channel;
Task _workerTask=null;
CancellationTokenSource _cts;
public FileImporter(string path,string filter)
{
_channel=Channel.CreateUnbounded<FileSystemEventArgs>();
_watcher=new FileSystemWatcher(path);
_watcher.NotifyFilter = NotifyFilters.Attributes...;
_watcher.Filter = filter;
_watcher.Created += (o,e)=>_channel.Writer.TryWrite(e);
_cts=new CancellationTokenSource();
_workerTask=ProcessFiles(_channel,cts.Token);
}
public void Start()
{
_watcher.EnableRaisingEvents=true;
}
public await StopAsync()
{
_watcher.EnableRaisingEvents=false;
await _workerTask;
}
async ProcessFiles(ChannelReader<FileSystemEventArgs> input,CancellationToken token=default)
{
using var run = new MyEtlScript();
await foreach(var evt in input.ReadAllAsync(token))
{
if(args.ChangeType == WatcherChangeTypes.Created)
{
await run.ReadCSV(evt);
}
}
}
public void Dispose()
{
_cts.Cancel();
try
{
StopAsync().Wait();
}
finally
{
_watcher.Dispose();
_cts.Dispose();
}
}
}
在长期运行的作业中使用作用域 DbContext
看起来被处理的对象是一个 DbContext,可能通过 DI 作为 Scoped 服务提供。 Consuming a scoped service in a background task 中描述了对此的解决方案 - 我们需要定义我们自己的范围并使用它来创建和处理其中的新服务。为此,我们需要在类中添加 IServiceProvider
作为依赖项:
public FileImporter(IServiceProvider services,string path,string filter)
{
_services=services;
...
}
在以未知时间间隔处理文件时最有意义的范围是文件操作本身:
async ProcessFiles(ChannelReader<FileSystemEventArgs> input,CancellationToken token=default)
{
await foreach(var evt in input.ReadAllAsync(token))
{
if(args.ChangeType == WatcherChangeTypes.Created)
{
using (var scope = Services.CreateScope());
var db = scope.ServiceProvider
.GetRequiredService<MyDbContext>();
using var run = new MyEtlScript(db);
await run.ReadCSV(evt);
}
}
}
当然,如果 MyEtlScript
本身是注册的,我们只需要请求那个类:
async ProcessFiles(ChannelReader<FileSystemEventArgs> input,CancellationToken token=default)
{
await foreach(var evt in input.ReadAllAsync(token))
{
if(args.ChangeType == WatcherChangeTypes.Created)
{
using (var scope = Services.CreateScope());
var run = scope.ServiceProvider
.GetRequiredService<MyEtlScript>();
await run.ReadCSV(evt);
}
}
}
,
您的异步回调事件处理程序必须具有签名:
private void OnChanged(...)
您不要在这里启动异步任务!
如果每次进行数据库操作时都启动文件系统观察器(观察路径总是相同的!!!),那么这是一个糟糕的设计,绝对是一种矫枉过正。更好的是将文件事件缓存在一个队列中(封装在一个类中)并稍后处理它们。您还可以防止丢失事件的问题,而且性能更好。
当您收到已处理的错误时,这可能是由于在本地声明了“观察者”:
FileSystemWatcher watcher = new FileSystemWatcher("/app/Import");
“观察者”可能会被自动处置。
将“watcher”声明为类字段并在构造函数中开始监视,或者在数据库类范围内为 FileSystemWatcher 提供 Start() 和 Stop() 方法。
问候
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。