简体   繁体   English

.NET实体框架核心,依赖注入和线程处理的DBContext System.ObjectDisposed异常

[英]DBContext System.ObjectDisposed Exception with .NET Entity Framework Core, Dependency Injection and threading

I am not sure if I am going about this correctly at all. 我不确定我是否正确地解决了这个问题。

Background: I have a controller action GET foo() as an example. 背景:我以一个控制器动作GET foo()为例。 This foo() needs to go and call bar(), and bar() may take a verrrrrry long time. 这个foo()需要去调用bar(),而bar()可能要花很长时间。 So, I need foo() to respond with "OK" before (or regardless of when) bar() completes . 因此, 在bar()完成之前(或无论何时)我需要foo()以“ OK”响应

A slight complexity is that bar() needs to access the DBContext and fetch some data from the DB. 稍微复杂一点是bar()需要访问DBContext并从DB中获取一些数据。 With my current implementation, I get a "DBContext System.ObjectDisposed " exception when I try to access the db via bar. 在当前的实现中,当我尝试通过bar访问数据库时,出现“ DBContext System.ObjectDisposed”异常。 Any ideas why and how I can around this please? 有什么想法为什么以及如何可以解决这个问题? I am really new to threading and tasks so I could be completely going about this wrong! 我真的是线程和任务的新手,所以我可能会完全解决这个错误!

I use dependency injection to provide the DB context on startup 我使用依赖注入在启动时提供数据库上下文

     services.AddEntityFrameworkNpgsql()
        .AddDbContext<MyDBContext>()
        .BuildServiceProvider();

I then make call to foo() which in turn calls bar() using a new thread (maybe I am doing this wrong?): 然后,我对foo()进行调用,而foo()依次使用新线程调用bar()(也许我做错了吗?):

    public async Task<string> foo(string msg)
    {

        Thread x = new  Thread(async () =>
        {
            await bar(msg);
        });

        x.IsBackground = true;
        x.Start();

        return "OK.";
    }

So bar immediately tries to access DBContext to grab some entities and it throws the exception! 因此,bar立即尝试访问DBContext来获取某些实体,并引发异常!

Unhandled Exception: System.ObjectDisposedException: Cannot access a disposed object. 未处理的异常:System.ObjectDisposedException:无法访问已处置的对象。 A common cause of this error is disposing a context 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, or wrapping the context in a using statement. 如果在上下文上调用Dispose()或将上下文包装在using语句中,则可能会发生这种情况。 If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. 如果使用依赖项注入,则应让依赖项注入容器负责处理上下文实例。 Object name: 'MyDBContext'. 对象名称:“ MyDBContext”。

If I take bar() out of the thread, it is fine but of course "OK" not returned until bar has finished with its very long process which is the problem I need to get around. 如果我从线程中取出bar(),那很好,但是当然,直到bar完成非常长的过程后才返回“ OK”,这是我需要解决的问题。

Many thanks for some guidance please. 非常感谢您的指导。

EDIT with running code BUT it is still waiting for the Task.Run to complete before returning "OK." 使用运行代码进行编辑,但是它仍在等待Task.Run完成,然后返回“确定”。 (almost there?) (快好了?)

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);
    }
}

In Asp.net, the lifetime of the injected items are up to the framework. 在Asp.net中,注入项的生存期取决于框架。 Once foo() returns, Asp has no knowledge that you created a thread that still needs the DbContext that it gave you, so Asp disposes the context and you get a problem. 一旦foo()返回,Asp就不知道您创建了一个仍需要它提供给您的DbContext的线程,因此Asp处置了上下文,您遇到了问题。

You can create a new DbContext yourself in your thread and you can decide when to dispose it. 您可以在线程中自己创建一个新的DbContext,然后可以决定何时处置它。 This isn't as nice because if your database configuration changes, you now have two places that may need to be updated. 这不是很好,因为如果数据库配置发生更改,那么您现在有两个地方可能需要更新。 Here's an example: 这是一个例子:

var optionsBuilder = new DbContextOptionsBuilder<MyDBContext>();
optionsBuilder.UseNpgsql(ConnectionString);

using(var dataContext = new MyDBContext(optionsBuilder.Options)){
    //Do stuff here
}

As a second option, Asp.net core also has the ability using IHostedService to create background services and register them in your startup, complete with dependency injection. 第二种选择是,Asp.net核心还具有使用IHostedService创建后台服务并在启动时注册后台服务的能力,并完成了依赖项注入。 You could create a background service which runs background tasks and then foo() could add the task to the service's queue. 您可以创建一个运行后台任务的后台服务,然后foo()可以将该任务添加到服务的队列中。 Here's an example . 这是一个例子

AddDbContext<>() registers the MyDBContext as a service with ServiceLifetime.Scoped which means that your DbContext is created per web request. AddDbContext<>()使用ServiceLifetime.ScopedMyDBContext注册为服务,这意味着将根据Web请求创建DbContext It is disposed when request is completed. 请求完成后将其处置。

The easiest way to avoid that exception is inject IServiceScopeFactory into your controller, then create a new scope using CreateScope() and request the MyDBContext service from that scope (you don't need to worry about DbContextOptions ). 避免该异常的最简单方法是将IServiceScopeFactory注入到控制器中,然后使用CreateScope()创建一个新范围,并从该范围中请求MyDBContext服务(您不必担心DbContextOptions )。 When job is done then dispose that scope that subsequently disposes DbContext . 完成工作后,再处置随后处置DbContext范围。 Instead of Thread it is better to use Task . 最好使用Task而不是Thread Task API is more powerful and usually has better performance. Task API功能更强大,通常具有更好的性能。 It will look like this: 它看起来像这样:

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.";
    }
}

Also you should know that Asp.Net is not the best place for background tasks (eg when app is hosted in IIS it can be shut down because of app pool recycles) 您还应该知道, Asp.Net不是执行后台任务的最佳场所(例如,将应用程序托管在IIS中时,由于应用程序池回收,可以将其关闭)

update 更新

Your ServerLogger.Info("SendBigFile finished."); 您的ServerLogger.Info("SendBigFile finished."); doesn't wait when client.PostAsync is finished. 不等待client.PostAsync完成时。 It logs immediately after Task.Run started new task. 它在Task.Run启动新任务后立即记录。 To log it after client.PostAsync is finished you need to put ServerLogger.Info("SendBigFile finished."); 要在client.PostAsync完成后client.PostAsync日志记录,您需要将ServerLogger.Info("SendBigFile finished."); right after await GoOffAndSend(someProvider, fileProvider, fileBytes); await GoOffAndSend(someProvider, fileProvider, fileBytes); :

...
await GoOffAndSend(someProvider, fileProvider, fileBytes);
ServerLogger.Info("SendBigFile finished.");
...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM