Monday, December 17, 2018

Accessing Entity Framework context on the background on .NET Core


So, you tried to access Entity Framework Core's db context on a background thread in .NET Core and got this error:
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.

What do you do?

Before we review the solution, it's important to understand why most of the time you shouldn't do that. A few good explanations are described here. And, if you're looking for a good introduction on how to do it right in Asp.NET using Azure, Scott Hanselman has more information available here.

However, such an approach sometimes may be necessary. For example, what about proof of concepts or MVPs? Should we care implementing all that boilerplate? Probably not.

So let's that a look at how to do it.

IServiceScopeFactory

The key to doing this is using IServiceScopeFactory. Available on the Microsoft.Extensions.DependencyInjection package, IServiceScopeFactory provides us a singleton from which we can resolve services trough DI the same way the .NET Core framework does for us.

Microsoft describes it as:
A factory for creating instances of IServiceScope, which is used to create services within a scope. Create an IServiceScope which contains an IServiceProvider used to resolve dependencies from a newly created scope.

The Implementation

The implementation is divided in 3 (three) steps:
  1. Inject the IServiceScopeFactory singleton on your controller;
  2. Pass the instance of IServiceScopeFactory to your background task or thread;
  3. Use it;

Step 1 -  Inject IServiceScopeFactory in your controller

First, you need to inject IServiceScopeFactory in your controller.

private readonly IServiceScopeFactory serviceScopeFactory;

public ApiController(ApplicationDbContext context, IServiceScopeFactory serviceScopeFactory)
{
this.context = context;
this.serviceScopeFactory = serviceScopeFactory;
}

Step 2 -  Pass it to your background thread

Then, you have some code that supposedly invokes the bg thread/task. For example:
public async Task<IActionResult> TestAsyncCall(string id)
{
Task.Run(() => BgTask(id, serviceScopeFactory));
return Ok("Thanks, your code will be executed!");
}

Step 3 -  Resolve the service from the background task

And finally, when your background thread is run, access the scope and have the framework initialize the EF context for you with:
private async Task BgTask(string id, IServiceScopeFactory serviceScopeFactory)
{
await Task.Delay(30000);
using (var scope = serviceScopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<ApplicationDbContext>();

dbContext.MyTable.Add(new MyObject { Id = "c6334a9e-4e7a-48bd-9b65-290c92b85f6f", Message = "Test bg thread" });
await dbContext.SaveChangesAsync();
}

}


And because it's a singleton, IServiceScopeFactory won't throw an exception when you try to access it.
at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed()
at Microsoft.EntityFrameworkCore.DbContext.Add[TEntity](TEntity entity)
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Add(TEntity entity)

Conclusion

While you shouldn't use this as a pattern to process background tasks, there are situations where this is necessary. Since the there isn't much documentation around IServiceScopeFactory I thought it was good to document it.

Hope it helps.

See Also

Why you should start using .NET Core
Package Management in .NET Core
Building and Running Asp.NET Core apps on Linux

For more .NET Core posts on this blog, please click here.

References

IServiceScopeFactory Interface
StackOverflow - DbContext for background tasks via Dependency Injection
StackOverflow - .NET Core IServiceScopeFactory.CreateScope() vs IServiceProvider.CreateScope() extension
GitHub - CreateScope from IServiceProvider