Search code examples
c#asynchronous.net-coredbcontextobjectdisposedexception

Task.Run causes "Cannot access a disposed context instance" exception


Using Task.Run causes "object disposed" exception in my application if using DbContext.

The code looks like (see the whole chain):

UserController.cs

[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserService userService;

    /// <summary>
    /// </summary>
    /// <param name="userService"></param>
    public UsersController(UserService userService)
    {
        this.userService = userService;
    }


    public async Task<ActionResult> DoSomething()
    {
        await this.userService.MyMethod();
        return this.Ok();
    }
}

UserService.cs

public class UserService
{
    private readonly UserRepository userRepository;

    public UserService(UserRepository userRepository)
    {
        this.userRepository = userRepository;
    }
    
    public async Task MyMethod()
    {
        // some logic
        Task.Run(() => MethodCallAsync());
    }

    void MethodCallAsync()
    {
        // some logic
        // calls UserRepository, which uses the DbContext by DI
    }   
}

UserRepository:

public class UserRepository
{
    private MyDbContext dbContext;

    public UserRepository(MyDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public async Task DoSomethingToo(string username)
    {
        var user = await this.dbContext.Users.SingleOrDefaultAsync(u => u.Username == username);
        // some logic
    }
}

Causes the following exception (message):

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.

How i did configure my db context and UserRepository:

Startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
        // some logic
        if (MyDbContextFactory.GetConnectionString() != null)
        {
            services.AddDbContext<MyDbContext>(options => options.UseMySQL(MyDbContextFactory.GetConnectionString())
            .LogTo(s => System.Diagnostics.Debug.WriteLine(s)));
        }
        
        services.AddScoped(typeof(UserService));
        services.AddScoped(typeof(UserRepository));
        // some logic
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // some logic
        using (var serviceScope = app.ApplicationServices.CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService<MyDbContext>();

            if (dbContext != null)
            {
                dbContext.Database.Migrate();
            }
        }
        // some logic
    }
    
    public class MysqlEntityFrameworkDesignTimeServices : IDesignTimeServices
    {
        public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
        {
            serviceCollection.AddEntityFrameworkMySQL();
            new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
                .TryAddCoreServices();
        }
    }

MyDbContextFactory.cs

    public MyDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
        optionsBuilder.UseMySQL(GetConnectionString());
        return new MyDbContext(optionsBuilder.Options);
    }

If i replace Task.Run with BackgroundJob.Enqueue it works fine. But hangfire creates a lot (> 1k) of entries in redis within some minutes, since this method is called very often. Besides that, if it works with hangfire, it should also work with Task.Run,


Solution

  • Your problem essentially is that Task.Run(() => MethodCallAsync()); starts a separate thread that is still relying on the class field userRepository (and DbContext inside it) - this is inherently not thread-safe and is not supposed to be by design. One way you can fix this is by passing a higher-level instance into your async method - e.g. replacing userRepository not just with DbContext itself, but rather with IDbContextFactory<DbContext>. This is about how it would look like:

    public class UserService
    {
        private readonly IDbContextFactory<MyDbContext> dbContextFactory;
    
        public UserService(IDbContextFactory<MyDbContext> dbContextFactory)
        {
            this.dbContextFactory = dbContextFactory;
        }
        
        public async Task MyMethod()
        {
            // some logic
            Task.Run(() => MethodCallAsync());
        }
    
        void MethodCallAsync()
        {
            // some logic
            var userRepository = new UserRepository(dbContextFactory);
            ...
        }   
    }
    
    public class UserRepository
    {
        private MyDbContext dbContext;
    
        public UserRepository(IDbContextFactory<MyDbContext> dbContextFactory)
        {
            this.dbContext = dbContextFactory.CreateDbContext();
        }
    
        public async Task DoSomethingToo(string username)
        {
            var user = await this.dbContext.Users.SingleOrDefaultAsync(u => u.Username == username);
            // some logic
        }
    }
    

    DbContextFactory is supposed to be thread-safe, so this would probably work. However, if we dive deeper into your code, I would join Panagiotis Kanavos and say that you likely don't need the UserRepository class at all. DbContext is supposed to be your Repository and Unit of Work already, so why not use it directly?

    public class UserService
    {
        private readonly IDbContextFactory<MyDbContext> dbContextFactory;
    
        public UserService(IDbContextFactory<MyDbContext> dbContextFactory)
        {
            this.dbContextFactory = dbContextFactory;
        }
        
        public async Task MyMethod()
        {
            // some logic
            Task.Run(() => MethodCallAsync());
        }
    
        void MethodCallAsync()
        {
            // some logic
            using var dbContext = dbContextFactory.CreateDbContext();
            var user = await dbContext.Users.SingleOrDefaultAsync(u => u.Username == username);
            ...
        }   
    }
    

    You will have two times less code for the same thing and it will be working in a thread-safe manner.