dbContext is Disposed in a Time Hosted Service

Rafael Eberle

I'm trying to get my TimeHostedService with a DBContext to work but I always get the error: Cannot access a disposed context instance. So here is my Code:

First of all, I have my DoWork Method inside my Time Hosted Service. Because the class that I call (IEasyConficService) is scoped and the TimeHosted Service is singleton, I have to implement it with the scope.ServiceProvider. I think, this is so far fine.

private void DoWork(object? state)
{
    using var scope = _serviceScopeFactory.CreateScope();
    var easyConfigService = scope.ServiceProvider.GetRequiredService<IEasyConfigService>();
    easyConfigService.ExecuteUnfinishedEasyConfigs();
}

Now my function ExecuteUnfinishedEasyConfigs is collecting all the unfinished Gateways that I have from the Database. And then give it to the Method 'TryExecuteEasyConfig'.

public async Task ExecuteUnfinishedEasyConfigs()
{
    var gateways = _context.Gateways.Include(g => g.Machine)
        .Where(g => g.EasyConfigStatus == EasyConfigStatus.OnGoing)
        .Include(g => g.Machine)
        .ThenInclude(m => m.MachineType)
        .ToList();
    await Parallel.ForEachAsync(gateways, async (gateway, _) => await TryExecuteEasyConfig(gateway));
}

This works so far, because the _context is set in the constructor of the class with DI.

public class EasyConfigService : IEasyConfigService {
    private readonly SuncarContext _context;
    
    public EasyConfigService(SuncarContext context)
    {
        _context = context;
    }
}

But inside the TryExecuteEasyConfig method, which is also in the class EasyConfigService, I get the disposed Error from above, if I try call _context.SaveChangesAsync()

private async Task TryExecuteEasyConfig(Gateway gateway)
{
    // Do some Stuff
    await _context.SaveChangesAsync();
}

I do not know, why I can first take some Information out of the DBContext and afterwards the SaveChangesAsync() is failing. Why is the _context now disposed?

First I thought, it is because I do a Parallel.FoarEachAsync but it also happend, if I do a normal foreach. Is there any way to use a DBContext safely inside a TimeHostedService?

Edit 15.06.2023 11:12

The normal foreach is looking like:

foreach (var gateway in gateways)
{
    await TryExecuteEasyConfig(gateway);
}

Edit 15.06.2023 11:16

The SuncarContext is added as following:

services.AddDbContext<SuncarContext>(options =>
    options.UseNpgsql(Configuration.GetConnectionString("PostgresConnection"),
        sqlOption =>
        {
            sqlOption.EnableRetryOnFailure(
                5,
                TimeSpan.FromSeconds(5),
                null);
        }
    ));

Edit 15.06.2023 11.21

The Time Hosted Service is Using the StartAsync Method, where I use a Timer, that is calling a DoWork Method.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Backend.BusinessLayer.Services.Interfaces;

public class EasyConfigExecutor : IHostedService, IDisposable {
    private readonly ILogger<EasyConfigExecutor> _logger;
    private Timer? _timer;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public EasyConfigExecutor(ILogger<EasyConfigExecutor> logger, IServiceScopeFactory serviceScopeFactory)
    {
        _logger = logger;
        _serviceScopeFactory = serviceScopeFactory;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var easyConfigService = scope.ServiceProvider.GetService<IEasyConfigService>() ?? throw new ArgumentNullException();
        easyConfigService.ExecuteUnfinishedEasyConfigs();
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Edit 15.06.2023 14.22 - Conclusion

The Answer of Dai definitely works. Thanks for your support.

After looking through the Internet, I found also a good article from Microsoft, how to use timed asynchronous background task in c#. That's the way I'm using it now. Probably this will help some other people: Micosoft Timed asynchronous background Task

Dai

You should read this document fully first: https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#asynchronous-programming


The problem is in your DoWork method: which you've defined as a normal void method when it needs to be an async Task method so that you can await the TryExecuteEasyConfig method.

...instead, what happens is the DoWork method itself will return as soon as the first await is encountered inside/via easyConfigService.ExecuteUnfinishedEasyConfigs();.

You haven't shown-us your TimeHosted service - nor really told us much about it, but I assume you're using IHostedService (which supports async methods as-is) with a non-async-safe Timer callback, such as System.Timers.Timer or System.Threading.Timer?

In any event, you don't need to use any Timer types, just change your code to this:

public sealed class MyHostedService : IHostedService
{
    private readonly IDbContextFactory<SuncarContext> dbFactory;

    private readonly Object lockObj = new Object();
    private CancellationTokenSource cts = new CancellationTokenSource();
    private Task? loopTask;

    public MyHostedService( IDbContextFactory<SuncarContext> dbFactory )
    {
        this.dbFactory = dbFactory;
    }

    public Task StartAsync( CancellationToken cancellationToken )
    {
        lock( this.lockObj )
        {
            if( this.loopTask != null ) throw new InvalidOperationException( "Already running..." );

            this.loopTask = Task.Run( this.RunLoopAsync );
        }

        return Task.CompletedTask;
    }

    public Task StopAsync( CancellationToken cancellationToken )
    {
        // TODO: add better reentrancy protection

        Task? rundownLoopTask; // <-- Make a local copy of `this.loopTask` for thread-safety reasons.
        lock( this.lockObj )
        {
            rundownLoopTask = this.loopTask;
        }

        this.cts.Cancel();

        try
        {
            await rundownLoopTask;
        }
        catch( OperationCanceledException ) // <-- This is an expected-exception.
        {
            // NOOP
        }
        finally
        {
            lock( this.lockObj )
            {
                this.lockObj = null;
                this.cts.Dispose();
                this.cts = new CancellationTokenSource();
            }
        }
    }
    
    private async Task RunLoopAsync()
    {
        while( true )
        {
            await Task.Delay( TimeSpan.FromSeconds( 60 ) );

            try
            {
                await this.RunLoopBodyOnceAsync( this.cts.Token );
            }
            catch( OperationCanceledException ) when ( this.cts.IsCancellationRequested )
            {
                throw; // Always rethrow this so the StopAsync method can observe it.
            }
            catch( Exception ex )
            {
                // TODO: Log the exception and decide if the loop should be aborted or not.
            }
        }
        
    }

    private async Task RunLoopBodyOnceAsync( CancellationToken cancellationToken )
    {
        using( SuncarContext db = this.dbFactory.Create() )
        {
            IQueryable<Gateway> q = db.Gateways
                .Include( g => g.Machine )
                .ThenInclude( m => m.MachineType )
                .Where( g => g.EasyConfigStatus == EasyConfigStatus.OnGoing );
                
            List<Gateway> gateways = await q.ToListAsync( cancellationToken );

            DoStuffWithGateways( gateways );

            await db.SaveChangesAsync( cancellationToken );
        }
    }
}

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

DbContext Lifecycle In WCF Service Hosted In IIS

Issues with DbContext getting disposed after multiple calls to service

DbContext not Disposed using dependencyinjection

Context Disposed into a service

Usermanager.DbContext already disposed in Middleware

DbContext get's disposed within using region

'DbContext has been disposed error' on Multiple Calls

c# - DbContext gets disposed in BackgroundService

Pass Dbcontext to inner method being disposed

ASP.Net Core DbContext disposed in HostedService

DbContext has been disposed when using IQueryable<>

Windsor and DbContext per request - DbContext has been disposed

EFCore 5.0: Change tracker when DbContext is disposed (DbContext lifetime)

Prevent DbContext from being disposed until async task completes

DbContext is Disposed When Using Unity Dependency Injection on WebApi project

How to fix DbContext has been disposed for an async scenario?

EF Core DbContext being disposed of before Async methods are complete

ToListAsync() in a DbContext using statement : "The ObjectContext disposed", How to deal?

"The operation cannot be completed because the DbContext has been disposed"

EF Core - DbContext disposed when query from multiple tables sequentially

DBContext as a pararameter to a long running task? Access to disposed closure

Await is disposing DbContext - The ObjectContext instance has been disposed

Autofac - Retrieving new instance of UnitOfWork - "DBcontext has been disposed error"

DbContext has been disposed, does not make any sense

The operation cannot be completed because the DbContext has been disposed exception

DbContext Gets Disposed Upon Using async Task method

DbContext has been disposed (ASP.NET MVC)

asp.net core dbcontext is disposed when timer elapsed

Cannot access a disposed object (on DbContext) error on Hangfire recurrent job in ABP