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
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.
Comments