5

我不确定我是否正确地处理了这个问题。

背景:我以控制器操作 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() 从线程中取出,那很好,但当然,直到 bar 完成其非常长的过程才返回“OK”,这是我需要解决的问题。

非常感谢您的指导。

使用正在运行的代码进行编辑,但它仍在等待 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);
    }
}
4

2 回答 2

8

AddDbContext<>()将 注册MyDBContext为服务,ServiceLifetime.Scoped这意味着您DbContext是根据 Web 请求创建的。它在请求完成时被释放。

避免该异常的最简单方法是注入IServiceScopeFactory您的控制器,然后使用创建一个新范围CreateScope()并从该范围请求MyDBContext服务(您无需担心DbContextOptions)。工作完成后,处置随后处置的范围DbContext。而不是Thread更好地使用Task. TaskAPI 更强大,通常具有更好的性能。它看起来像这样:

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.");
...
于 2019-02-08T19:10:16.183 回答
3

在 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() 可以将任务添加到服务的队列中。这是一个例子

于 2019-02-08T17:52:44.977 回答