【问题标题】:Cannot access a disposed object in Task.Run无法访问 Task.Run 中的已处置对象
【发布时间】:2020-07-21 18:07:10
【问题描述】:

我正在使用 .NET Core 3.1。我想运行一些后台处理,而无需用户等待它完成(大约需要 1 分钟)。因此,我这样使用Task.Run:

public class MyController : Controller
{
    private readonly IMyService _myService;

    public MyController(IMyService myService)
    {
        _myService = myService;
    }

    public async Task<IActionResult> Create(...)
    {
        await _myService.CreatePostAsync(...);
        return View();
    }
}

public class MyService : IMyService
{
    private readonly MyDbContext _dbContext;
    private readonly IServiceScopeFactory _scopeFactory;

    public MyService(MyDbContext dbContext, IServiceScopeFactory scopeFactory)
    {
        _dbContext = dbContext;
        _scopeFactory = scopeFactory;
    }

    public async Task CreatePostAsync(Post post)
    {
        ...
        string username = GetUsername();
        DbContextOptions<MyDbContext> dbOptions = GetDbOptions();
        Task.Run(() => SaveFiles(username, dbOptions, _scopeFactory));
    }

    private void SaveFiles(string username, DbContextOptions<MyDbContext> dbOptions, IServiceScopeFactory scopeFactory)
    {
        using (var scope = scopeFactory.CreateScope())
        {
            var otherService = scope.ServiceProvider.GetRequiredService<IOtherService>();
            var cntxt = new MyDbContext(dbOptions, username);

            Post post = new Post("abc", username);

            cntxt.Post.Add(post); <----- EXCEPTION

            cntxt.SaveChanges();
        }
    }
}

我在标记的行中收到以下异常:

System.ObjectDisposedException: 'Cannot access a disposed object. Object name: 'IServiceProvider'.'

为什么会这样?我为 MyDbContext 使用了自定义构造函数(而不是 scope.ServiceProvider.GetRequiredService&lt;MyDbContext&gt;()),因为我需要保存一个额外的属性(用户名)以供以后在重写方法中使用。

public partial class MyDbContext
{
    private string _username;
    private readonly DbContextOptions<MyDbContext> _options;

    public DbContextOptions<MyDbContext> DbOptions { get { return _options; } }

    public MyDbContext(DbContextOptions<MyDbContext> options, string username) : base(options)
    {
        _username = username;
        _options = options;
    }

    ... other overriden methods
}

我做错了什么?

【问题讨论】:

  • MyService 的用法是什么?它是在控制器中创建的吗? Asp.Net 为每个传入请求创建新的容器范围。这意味着当控制器的方法返回结果时,作用域中的容器被释放。这解释了您遇到的 IServiceProvider disposed 错误。这可能会导致 ctor 对象实例被容器释放,而某些任务会继续使用这些实例。
  • 是的,MyService 被注入到控制器构造函数中。然后控制器调用await _myService.CreatePostAsync(...)。我应该怎么做才能避免这个错误?
  • 我更新了问题,使其也包含控制器部分。
  • 你几乎不应该在网络应用程序中使用Task.Run。无关紧要。它允许将当前线程返回到池中,但只是从同一个池中拉出另一个线程。由于所有请求都由同一个线程池提供服务,因此您所做的只是减少可用请求的总吞吐量。

标签: c# entity-framework asp.net-core dependency-injection


【解决方案1】:

你应该创建一个具有单例生命周期的服务,并在其中注入一个 DBContext 并将所有任务排队

【讨论】:

  • 将作用域依赖(例如DbContext)注入单例通常是个坏主意。如果您认为在这个特殊的案例中这是一个好方法,您应该解释为什么会这样,以及为什么您不会遇到concurrency (and other) issues。
  • 我同意全局 DBContext 是一个糟糕的想法,这就是为什么我建议将任务排队以避免并发问题
  • 除了并发性之外,全局 DbContext 实例还有其他问题,例如它们的数据变得陈旧(如参考答案中所述)。你可能对这一切都很好,但至少你应该警告那些注意力不集中或经验不足的读者关于这个问题的回答。
【解决方案2】:

首先,不要在服务中隐藏线程池操作;让调用代码决定是否在线程池上运行操作:

当您使用依赖注入时,框架会在 HTTP 请求结束时释放您的 DbContext。

您需要将您的服务范围工厂注入您的控制器,并从那里请求服务:

public class MyController : Controller
{
    private readonly IMyService _myService;
    private readonly IServiceScopeFactory _scopeFactory;

    public MyController(IMyService myService, IServiceScopeFactory scopeFactory)
    {
        _myService = myService;
        _scopeFactory = scopeFactory;
    }

    public async Task<IActionResult> Create(...)
    {
        HostingEnvironment.QueueBackgroundWorkItem(SaveInBackground);
        return View();
    }

    private async Task SaveInBackground(CancellationToken ct)
    {
        using (var scope = scopeFactory.CreateScope())
        {
            var scopedService = scope.ServiceProvider.GetRequiredService<IMyService>();
            await scopedService.CreatePostAsync(...);
        }
    }
}

HostingEnvironment.QueueBackgroundWorkItem 的工作方式与Task.Run 类似,但它确保应用在所有后台工作项完成之前不会关闭。

您的服务需要是这样的:

public class MyService : IMyService
{
    private readonly MyDbContext _dbContext;

    public MyService(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task CreatePostAsync(Post post)
    {
        _dbContext.Post.Add(post);

        await _dbContext.SaveChangesAsync();
    }
}

更新

将附加参数传递给SaveInBackground:

private async Task SaveInBackground(YourParam param)

然后像这样调用:

HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => SaveInBackground(yourParam));

【讨论】:

  • 谢谢!如何将附加参数传递给SaveInBackground(例如我要保存到数据库的模型)?
  • HostingEnvironment.QueueBackgroundWorkItem 在 .NET Core 3 中不可用。我应该改用什么?
猜你喜欢
  • 2011-10-23
  • 2011-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-23
相关资源
最近更新 更多