2

I am using .NET Core 3.1. I want to run some background processing without user having to wait for it to finish (it takes about 1 minute). Therefore, I used Task.Run like this:

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

I recieve the following exception in marked line:

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

Why does this happen? I used custom constructor (and not scope.ServiceProvider.GetRequiredService<MyDbContext>()) for MyDbContext because I need to save one additional propery (username) for later use in overriden methods.

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
}

What am I doing wrong?

Sam Carlson
  • 1,891
  • 1
  • 17
  • 44
  • what is `MyService` usage ? Is it created in the controller? Asp.Net creates new container scope for each incoming request. It means that container from the scope is disposed when controller's method returns the result. This explains the `IServiceProvider disposed` error you have. This may cause ctor object instances to be disposed by the container while certain task continues to use those instances. – oleksa Apr 09 '20 at 07:25
  • Yes, `MyService` is injected in the controller constructor. Controller then calls `await _myService.CreatePostAsync(...)`. What should I do to avoid this error? – Sam Carlson Apr 09 '20 at 07:29
  • I updated the question so that it includes controller part as well. – Sam Carlson Apr 09 '20 at 07:32
  • You should pretty much never use `Task.Run` in a web app. There is no point. It allows the current thread to be returned to the pool, but simply pulls out another in its place from that same pool. Since all requests are serviced from that same threadpool, all you're doing is cutting your total available request throughput. – Chris Pratt Apr 09 '20 at 14:49

2 Answers2

8

First of all, don't hide a thread-pool operation away in your service; let the calling coded decide whether to run the operation on the thread-pool or not:

As you are using dependency injection, the framework is disposing your DbContext at the end of the HTTP request.

You need to inject your service scope factory into your controller, and request the service from there:

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 works in a similar way to Task.Run, except it ensures that the app doesn't shut down until all background work items have completed.

Your service would need to be something like this:

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

UPDATE

To pass additional parameters to SaveInBackground:

private async Task SaveInBackground(YourParam param)

Then call like:

HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => SaveInBackground(yourParam));
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
-2

You shoud create a Service with a Singleton lifecycle and inject a DBContext inside and queue all tasks inside

olivier houssin
  • 842
  • 1
  • 6
  • 11
  • Injecting a scoped dependency (such as a `DbContext`) into a singleton is in general a bad idea. If you think that it is a good approach in *this particular* case, you should explain why that is, and why you won't run into [concurrency (and other) issues](https://stackoverflow.com/questions/3266295/net-entity-framework-and-transactions/3266481#3266481). – Steven Apr 09 '20 at 08:22
  • I'm okay with the fact that a global DBContext is a terrible idea, that why i suggest to queue tasks to avoid the concurency issue – olivier houssin Apr 09 '20 at 08:57
  • There are other issues with global DbContext instances besides concurrency, such as that their data becomes stale (as noted in the referenced answer). And you might be fine with all this, but in the very least you should warn the inattentive or less experienced reader of your answer about this. – Steven Apr 09 '20 at 09:27