.NET Entity Framework Core、依赖注入和线程的 DBContext System.ObjectDisposed 异常

Posted

技术标签:

【中文标题】.NET Entity Framework Core、依赖注入和线程的 DBContext System.ObjectDisposed 异常【英文标题】:DBContext System.ObjectDisposed Exception with .NET Entity Framework Core, Dependency Injection and threading 【发布时间】:2019-07-02 23:39:20 【问题描述】:

我不确定我是否完全正确。

背景: 我有一个控制器操作 GET foo() 作为示例。这个 foo() 需要去调用 bar(),而 bar() 可能需要很长时间。因此,我需要 foo() 在 bar() 完成之前(或无论何时)以“OK”响应

有点复杂的是 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() 从线程中取出,那很好,但当然“OK”直到 bar 完成其非常长的过程才返回,这是我需要解决的问题。

非常感谢您的指导。

使用正在运行的代码进行编辑,但它仍在等待 Task.Run 完成,然后返回“OK”。 (快到了?)

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

【问题讨论】:

上下文不是线程安全的。您也不需要原始线程来异步运行任何东西。写await bar(msg); return "OK"; @PanagiotisKanavos 感谢您的回复。等待 bar 会不会导致 bar() 在返回 OK 之前需要完成的问题?如果是这样,那么这不是我想要的。非常感谢您的回复。 您没有发布任何代码,因此无法猜测 bar 做了什么。不知何故,在某处您试图重用已处置的上下文。上下文并不意味着被缓存。它们应该在 using 块内创建并在使用后立即处理。 @Ichirichi: Fire-and-forget on ASP.NET 几乎总是一个坏主意。 【参考方案1】:

AddDbContext&lt;&gt;()MyDBContext 注册为ServiceLifetime.Scoped 的服务,这意味着您的DbContext 是根据Web 请求创建的。请求完成后处理。

避免该异常的最简单方法是将IServiceScopeFactory 注入您的控制器,然后使用CreateScope() 创建一个新范围并从该范围请求MyDBContext 服务(您无需担心DbContextOptions )。工作完成后,处置随后处置DbContext 的范围。而不是Thread,最好使用TaskTask 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 中时,它可能会因为应用程序池回收而关闭)

更新

client.PostAsync 完成时,您的ServerLogger.Info("SendBigFile finished."); 不会等待。它在Task.Run 开始新任务后立即记录。要在client.PostAsync 完成后记录它,您需要将ServerLogger.Info("SendBigFile finished."); 放在await GoOffAndSend(someProvider, fileProvider, fileBytes); 之后:

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

【讨论】:

抱歉我的无知,但是我如何从 startup.cs 注入 IServiceScopeFactory? 您的意思是如何将其放入startup.cs 中?您可以将其注入 ctor:public Startup(IServiceScopeFactory serviceScopeFactory) 或喜欢 this 或喜欢 this 非常感谢您的回复。我能够实施您的建议。但是,它似乎仍在等待 Task.Run 完成,然后返回“OK”。我知道这一点是因为在 Postman 的测试中,直到服务器日志的消息转储出它已经完成时才返回响应。难道我做错了什么?我相信我正在做你发布的事情。 我在我的问题(上面)中添加了更多内容,希望能让事情更清楚!非常感谢。 很清楚。我指出了您的日志记录不准确,因为您正在查看日志以得出结论。只是尝试使用Task.Delay(5000) 而不是client.PostAsync 进行复制。对我来说,SendBigFile 方法会立即返回。【参考方案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。

【讨论】:

谢谢!在启动时注册的后台服务是否是一种可行且可靠的方式,例如处理队列?我真正想知道的是,它是不是一个很好的替代调度服务应用程序的例子。 查看我的示例链接,了解如何创建后台队列工作者。就可靠性而言,Asp.net 不会做任何事情来确保在抛出异常时后台工作程序重新启动,但您可以将自己的异常处理逻辑放入您的实现中。 HostedService 机制只是任务的精美包装。它也不会给你任何持久性。如果你有一个工作队列并且进程意外终止,它会在重新启动后消失,除非你做了一些事情来持久化它。在这种情况下,调度服务应用程序可能具有您需要的更多功能。

以上是关于.NET Entity Framework Core、依赖注入和线程的 DBContext System.ObjectDisposed 异常的主要内容,如果未能解决你的问题,请参考以下文章

ado.net entity framework无法更新数据库

.Net Entity Framework连接MySql

Entity Framework 6 是不是支持 .NET 4.0?

Entity Framework 和 .NET 的一对一关系

.NET Entity Framework 等 PHP 的 ORM [关闭]

带有 ADO.NET Entity Framework 的内部类