Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling Multiple Hangfire Jobs with DBContext Errors

I am using Hangfire to schedule powershell tasks. Specifically a job is created to schedule a forward and then remove the forward (email). On the create forward task the job ID's are stored into a SQL database and on the remove forward task the database entry is removed (so I can display a table that shows the scheduled forwards). On the remove forward tasks hangfire is failing the first time around, and then succeeding the second time around. It is failing with the following errors:

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.

AND

System.InvalidOperationException: An attempt was made to use the context instance while it is being configured. A DbContext instance cannot be used inside 'OnConfiguring' since it is still being configured at this point. This can happen if a second operation is started on this context instance before a previous operation completed. Any instance members are not guaranteed to be thread safe.

I have tried making the service that it is calling scoped and transient and I get the same error.

The method that is scheduled to run in Hangfire is pretty simple.

public async Task RemoveForwardByScheduleAsync(ScheduledForward scheduledForward)
    {
        var forward = await _context.ScheduledForwards.FirstOrDefaultAsync(x => x.StartJobId == scheduledForward.StartJobId);

        if (forward != null)
        {
            await RemoveForwardAsync(forward.FromId);

            _context.ScheduledForwards.Remove(forward);
            await _context.SaveChangesAsync();
        }
    }

The reason why it succeeds the second time appears to be because it actually worked the first time and removed the item from the database, so when it is run a second time forward == null because it is no longer in the database and it doesn't hit the savechangesasync method (that's what is triggering the error's for dbcontext).

UPDATE: I was wrong about this. It is not actually working the first time unless it was the first job in the list of jobs to run. I have learned that this is only happening for jobs that are scheduled at the same time as other jobs, and the first job that runs succeeds. Then on the retry the next job that runs succeeds. It's almost like Hangfire is not calling a new instance for each job when multiple jobs are scheduled to run that use the same service.

public class PowerBlazorDbContext : DbContext
{

    public PowerBlazorDbContext(DbContextOptions<PowerBlazorDbContext> options) : base(options)
    {

    }

    public DbSet<ScheduledForward> ScheduledForwards => Set<ScheduledForward>();

}


public class PowerService : IPowerService
{

    private static PowerBlazorDbContext _context;
    public PowerRun(PowerBlazorDbContext context)
    {
        _context = context;
    }

I have tried with both scoped and transient and get the same result no matter which service I use (iPowerRun and IPowerService are copies of each other).

builder.Services.AddScoped<iPowerRun, PowerRun>();
builder.Services.AddTransient<IPowerService, PowerService>();

Here is where the hangfire jobs are created.

public async Task<ScheduledForward> ScheduleForward(MailboxUser forwardFrom, MailboxUser forwardTo,bool sendToBoth, DateTime startTime, DateTime stopTime)
    {
        DateTimeOffset start = new DateTimeOffset(startTime);
        DateTimeOffset stop = new DateTimeOffset(stopTime);
        var scheduled = new ScheduledForward()
        {
            FromDisplay = forwardFrom.DisplayName,
            FromId = forwardFrom.Identity,
            ToId = forwardTo.Identity,
            ToDisplay = forwardTo.DisplayName,
            SendToBoth = sendToBoth,
            StartTime = startTime,
            EndTime = stopTime,
            StartJobId = "",
            StopJobId = "",
        };
        
        scheduled.StartJobId = BackgroundJob.Schedule<PowerService>(x => x.ForwardMailboxAsync(forwardFrom.Identity, forwardTo.Identity, sendToBoth), start);
        scheduled.StopJobId = BackgroundJob.Schedule<PowerService>(x => x.RemoveForwardByScheduleAsync(scheduled), stop);

        await _context.ScheduledForwards.AddAsync(scheduled);
        await _context.SaveChangesAsync();
        return scheduled;
    }

I really think this is an issue with Hangfire where it is not calling a new instance of the service for each job, however I could be wrong. When this issue occurs it actually breaks Blazor's access to the database as well and a refresh of the page yields the same error that I am seeing in Hangfire.

like image 258
Tom Gordon Avatar asked Nov 28 '25 19:11

Tom Gordon


1 Answers

Ok so it appears to be working using a DBContext Factory. I am not sure if I implemented this properly though. Please comment if there is a better way. Will this dispose of the dbcontext properly the way I implemented it?

I added Factory to the AddDBContext entry in program.cs

builder.Services.AddDbContextFactory<PowerBlazorDbContext>(
opt => opt.UseSqlServer(
    builder.Configuration.GetConnectionString("PowerBlazorDb")));

I then changed the injection to a context factory

public class PowerService : IPowerService
{

    private readonly IDbContextFactory<PowerBlazorDbContext> _contextFactory;

    public PowerService(IDbContextFactory<PowerBlazorDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

I then added await using var _context = await _contextFactory.CreateDbContextAsnyc() in front of every instance of _context that was being used from the old method of dbcontext.

public async Task<ScheduledForward> ScheduleForward(MailboxUser forwardFrom, MailboxUser forwardTo,bool sendToBoth, DateTime startTime, DateTime stopTime)
    {
        DateTimeOffset start = new DateTimeOffset(startTime);
        DateTimeOffset stop = new DateTimeOffset(stopTime);
        var scheduled = new ScheduledForward()
        {
            FromDisplay = forwardFrom.DisplayName,
            FromId = forwardFrom.Identity,
            ToId = forwardTo.Identity,
            ToDisplay = forwardTo.DisplayName,
            SendToBoth = sendToBoth,
            StartTime = startTime,
            EndTime = stopTime,
            StartJobId = "",
            StopJobId = "",
        };
        
        scheduled.StartJobId = BackgroundJob.Schedule<PowerRun>(x => x.ForwardMailboxAsync(forwardFrom.Identity, forwardTo.Identity, sendToBoth), start);
        scheduled.StopJobId = BackgroundJob.Schedule<PowerRun>(x => x.RemoveForwardByScheduleAsync(scheduled), stop);
        await using var _context = await _contextFactory.CreateDbContextAsync();
        await _context.ScheduledForwards.AddAsync(scheduled);
        await _context.SaveChangesAsync();
        return scheduled;
    }
like image 101
Tom Gordon Avatar answered Nov 30 '25 09:11

Tom Gordon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!