Aleks Vujic
Aleks Vujic

Reputation: 2251

Cannot access a disposed object in Task.Run

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?

Upvotes: 2

Views: 5822

Answers (2)

Johnathan Barclay
Johnathan Barclay

Reputation: 20363

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

Upvotes: 8

olivier houssin
olivier houssin

Reputation: 862

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

Upvotes: -2

Related Questions