我不确定我是否正确地解决了这个问题。
背景: 我以控制器动作GET foo()为例。这个foo()需要去调用bar(),而bar()可能要花很长时间。因此,我需要foo()才能在bar()完成之前(或与之无关)以“确定”进行响应。
稍微复杂一点是bar()需要访问DBContext并从DB中获取一些数据。在当前的实现中,当我尝试通过bar访问数据库时,出现“ DBContext System.ObjectDisposed”异常。有什么想法为什么以及如何可以解决这个问题?我真的是线程和任务的新手,所以我可能会完全解决这个错误!
我使用依赖注入在启动时提供数据库上下文
services.AddEntityFrameworkNpgsql()
.AddDbContext<MyDBContext>()
.BuildServiceProvider();
然后我调用foo(),然后使用新线程调用bar()(也许我做错了吗?):
public async Task<string> foo(string msg)
{
Thread x = new Thread(async () =>
{
await bar(msg);
});
x.IsBackground = true;
x.Start();
return "OK.";
}
因此,bar立即尝试访问DBContext来获取某些实体,并引发异常!
未处理的异常:System.ObjectDisposedException:无法访问已处置的对象。导致此错误的常见原因是,处理从依赖项注入中解决的上下文,然后稍后尝试在应用程序中的其他位置使用相同的上下文实例。如果在上下文上调用Dispose()或将上下文包装在using语句中,则可能会发生这种情况。如果使用依赖项注入,则应让依赖项注入容器负责处理上下文实例。 对象名称:“ MyDBContext”。
如果我将bar()移出线程,那很好,但当然要等到bar完成非常长的过程后才返回“ OK”,这是我需要解决的问题。
非常感谢您的指导。
使用运行代码进行编辑,但是它仍在等待Task.Run完成,然后返回“确定”。 (快到了吗?)
public async Task<string> SendBigFile(byte[] fileBytes)
{
ServerLogger.Info("SendBigFile called.");
var task = Task.Run(async () =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var someProvider = scope.ServiceProvider.GetService<ISomeProvider>();
var fileProvider = scope.ServiceProvider.GetService<IFileProvider>();
await GoOffAndSend(someProvider, fileProvider, fileBytes);
}
});
ServerLogger.Info("Hello World, this should print and not wait for Task.Run."); //Unfortunately this is waiting even though GoOffAndSend takes at least 1 minute.
return "Ok"; //This is not returned until Task.Run finishes unfortunately... how do I "skip" to this immediately?
}
private async Task GoOffAndSend(ISomeProvider s, IFileProvider f, byte[] bytes)
{
// Some really long task that can take up to 1 minute that involves finding the file, doing weird stuff to it and then
using (var client = new HttpClient())
{
var response = await client.PostAsync("http://abcdef/somewhere", someContent);
}
}
答案 0 :(得分:2)
在Asp.net中,注入项的生存期取决于框架。一旦foo()返回,Asp就不知道您创建了一个仍需要它提供给您的DbContext的线程,因此Asp处置了上下文,您遇到了问题。
您可以在自己的线程中创建一个新的DbContext,并可以决定何时处置它。这不是很好,因为如果数据库配置发生更改,那么您现在有两个地方可能需要更新。这是一个示例:
var optionsBuilder = new DbContextOptionsBuilder<MyDBContext>();
optionsBuilder.UseNpgsql(ConnectionString);
using(var dataContext = new MyDBContext(optionsBuilder.Options)){
//Do stuff here
}
作为第二个选择,Asp.net核心还具有使用IHostedService
创建后台服务并在启动时注册它们的功能,并完成了依赖项注入。您可以创建一个运行后台任务的后台服务,然后foo()可以将该任务添加到服务的队列中。 Here's an example。
答案 1 :(得分:1)
AddDbContext<>()
将MyDBContext
注册为ServiceLifetime.Scoped
的服务,这意味着您的DbContext
是根据Web请求创建的。请求完成后将其丢弃。
避免该异常的最简单方法是将IServiceScopeFactory
注入到控制器中,然后使用CreateScope()
创建一个新的作用域并从该作用域中请求MyDBContext
服务(您不需要担心DbContextOptions
)。完成工作后,再处置随后处置DbContext
的范围。最好使用Thread
而不是Task
。 Task
API更强大,通常具有更好的性能。看起来像这样:
public class ValuesController : ControllerBase
{
IServiceScopeFactory _serviceScopeFactory
public ValuesController(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public async Task<string> foo(string msg)
{
var task = Task.Run(async () =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetService<MyDBContext>();
await bar(db, msg);
}
});
// you may wait or not when task completes
return "OK.";
}
}
另外,您应该知道Asp.Net
并不是执行后台任务的最佳位置(例如,当应用程序托管在IIS中时,由于应用程序池回收,可以将其关闭)
ServerLogger.Info("SendBigFile finished.");
完成后,您的client.PostAsync
不会等待。 Task.Run
开始新任务后立即记录。要在client.PostAsync
完成后对其进行记录,您需要将ServerLogger.Info("SendBigFile finished.");
放在await GoOffAndSend(someProvider, fileProvider, fileBytes);
之后:
...
await GoOffAndSend(someProvider, fileProvider, fileBytes);
ServerLogger.Info("SendBigFile finished.");
...