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);
}
}
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)
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.");
...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With