Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DBContext System.ObjectDisposed Exception with .NET Entity Framework Core, Dependency Injection and threading

I am not sure if I am going about this correctly at all.

Background: I have a controller action GET foo() as an example. This foo() needs to go and call bar(), and bar() may take a verrrrrry long time. So, I need foo() to respond with "OK" before (or regardless of when) bar() completes.

A slight complexity is that bar() needs to access the DBContext and fetch some data from the DB. With my current implementation, I get a "DBContext System.ObjectDisposed " exception when I try to access the db via bar. Any ideas why and how I can around this please? I am really new to threading and tasks so I could be completely going about this wrong!

I use dependency injection to provide the DB context on startup

     services.AddEntityFrameworkNpgsql()
        .AddDbContext<MyDBContext>()
        .BuildServiceProvider();

I then make call to foo() which in turn calls bar() using a new thread (maybe I am doing this wrong?):

    public async Task<string> foo(string msg)
    {

        Thread x = new  Thread(async () =>
        {
            await bar(msg);
        });

        x.IsBackground = true;
        x.Start();

        return "OK.";
    }

So bar immediately tries to access DBContext to grab some entities and it throws the exception!

Unhandled Exception: System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'MyDBContext'.

If I take bar() out of the thread, it is fine but of course "OK" not returned until bar has finished with its very long process which is the problem I need to get around.

Many thanks for some guidance please.

EDIT with running code BUT it is still waiting for the Task.Run to complete before returning "OK." (almost there?)

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);
    }
}
like image 518
PKCS12 Avatar asked Feb 08 '19 17:02

PKCS12


2 Answers

AddDbContext<>() registers the MyDBContext as a service with ServiceLifetime.Scoped which means that your DbContext is created per web request. It is disposed when request is completed.

The easiest way to avoid that exception is inject IServiceScopeFactory into your controller, then create a new scope using CreateScope() and request the MyDBContext service from that scope (you don't need to worry about DbContextOptions). When job is done then dispose that scope that subsequently disposes DbContext. Instead of Thread it is better to use Task. Task API is more powerful and usually has better performance. It will look like this:

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.";
    }
}

Also you should know that Asp.Net is not the best place for background tasks (e.g. when app is hosted in IIS it can be shut down because of app pool recycles)

update

Your ServerLogger.Info("SendBigFile finished."); doesn't wait when client.PostAsync is finished. It logs immediately after Task.Run started new task. To log it after client.PostAsync is finished you need to put ServerLogger.Info("SendBigFile finished."); right after await GoOffAndSend(someProvider, fileProvider, fileBytes);:

...
await GoOffAndSend(someProvider, fileProvider, fileBytes);
ServerLogger.Info("SendBigFile finished.");
...
like image 154
AlbertK Avatar answered Oct 13 '22 03:10

AlbertK


In Asp.net, the lifetime of the injected items are up to the framework. Once foo() returns, Asp has no knowledge that you created a thread that still needs the DbContext that it gave you, so Asp disposes the context and you get a problem.

You can create a new DbContext yourself in your thread and you can decide when to dispose it. This isn't as nice because if your database configuration changes, you now have two places that may need to be updated. Here's an example:

var optionsBuilder = new DbContextOptionsBuilder<MyDBContext>();
optionsBuilder.UseNpgsql(ConnectionString);

using(var dataContext = new MyDBContext(optionsBuilder.Options)){
    //Do stuff here
}

As a second option, Asp.net core also has the ability using IHostedService to create background services and register them in your startup, complete with dependency injection. You could create a background service which runs background tasks and then foo() could add the task to the service's queue. Here's an example.

like image 26
spectacularbob Avatar answered Oct 13 '22 01:10

spectacularbob