"Cannot access a disposed context instance" exception while running reccuring call without async

93 views Asked by At

I faced some issue with Entity Framework.

I need to reload some cache object once per 5 minutes, so used Timer object for this, when I populate it first time it works as expected, but on the second run I got this error:

'Cannot access a disposed context instance. A common cause of this error is disposing a context instance 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 instance, or wrapping it 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: 'PostgreSqlContext'.'

Here the Startup config:

services.AddDbContext<PostgreSqlContext>(options =>
{
    options.UseNpgsql(postgresConnectionString);
    options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

PostgreSqlContext object:

public class PostgreSqlContext : DbContext
{
    public PostgreSqlContext(DbContextOptions<PostgreSqlContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }

    public async Task<int> SaveChangesAsync()
    {
        ChangeTracker.DetectChanges();
        return await base.SaveChangesAsync();
    }
    
    public DbSet<Product> Product { get; set; }    
}

Repository logic:

public class ProductRepository : IProductRepository
{
    private readonly PostgreSqlContext _context;

    public ProductRepository(PostgreSqlContext context)
    {
        _context = context;
    }

    public List<Product> GetAllProducts()
    {
        var result = _context.Product.ToList();

        return result;
    }
}

Usage:

public class SomeLogic
{
    private readonly IProductRepository _productRepository;
    private Timer _timer;

    public SomeLogic(IProductRepository productRepository)
    {
        _productRepository = productRepository;

        BuildCache();
        _timer = new Timer(ReloadHandler, null, 60000, Timeout.Infinite);
    }

    private void ReloadHandler(object state = null)
    {
        BuildCache();
        _timer.Change(60000, Timeout.Infinite);
    }

    public void BuildCache()
    {
        var products = _productRepository.GetAllProducts();
        // doing something with it
    }
}


I saw many answers about async/await calls, but in my case it is not relevant, as I don't use something with Task

I got the point, that it happens because the object was disposed, but I didn't find out how to bring it back to life, or reinitialize somehow.

Please, let me know about the mistake.

Thank you!

UPD. Added timer usage as well, but the failure is related to PostgreSqlContext.

Also, if there are other ways to do that, I will be glad to know

1

There are 1 answers

1
Steve Py On BEST ANSWER

As mentioned, The Timer event is going to be executed long after the initial request ends so scoped DbContext instances will get disposed and won't be available. An in- memory cache likely won't be much use as that would only contain the products at the point in time that they were read, where it's likely that you would want to reflect current data state without having to restart the service.

When dealing with multi-threaded operations, including things like timer events, you need to scope the DbContext instance within the thread/event scope. The easiest way to do this cleanly is to use a DbContext Factory that is lifetime scoped as a Singleton. This factory can then construct and provide a DbContext instance on demand that the consumer is responsible for disposing. This is best done without trying to abstract EF with an extra repository layer. EF already provides a Repository in the form of DbSet.

private readonly IDbContextFactory _dbContextFactory;

private Timer _timer;

public SomeLogic(IDbContextFactory dbContextFactory)
{
    _dbContextFactory = dbContextFactory;

    BuildCache();
    _timer = new Timer(ReloadHandler, null, 60000, Timeout.Infinite);
}

private void ReloadHandler(object state = null)
{
    BuildCache();
    _timer.Change(60000, Timeout.Infinite);
}

public void BuildCache()
{
    using var dbContext = _dbContextFactory.Create();
    var products = dbContext.Products.ToList();
    // doing something with it
}

Technically the code would work by registering the DbContext as a Singleton, however you don't want to do that as a solution as long-lived DbContexts will lead to performance issues as well as will end up serving stale data based on their tracking cache behavior. Leaving the DbContext scoped to a lifetime scope / request means all regular calls to the DbContext / repositories can be made as normal. If you do want to pursue a repository-based abstraction I would recommend looking into unit of work patterns to wrap them where the unit of work has a factory that can be registered as a Singleton and manages the lifetime scope of the DbContext so that the factory and repositories can be registered as Singleton to avoid disposal errors. These repositories don't get initialized with a DbContext, but rather a Context scope locator to resolve a DbContext when needed. You can have a look at the DbContextScope from Medhi el Gueddari. (https://mehdi.me/ambient-dbcontext-in-ef6/) There are several forks for EF Core, I use the one managed by Zejji which kept the consistent conventions & naming (https://www.nuget.org/packages/Zejji.DbContextScope.EFCore)