Background Job Management with Hangfire in .NET Core 8

Introduction

Handover of time-consuming tasks to the background is a widespread technique that improves application performance. Without a background job, running time-consuming tasks will affect the system's performance and also cause a bad user experience because the system might respond to the user query after a certain amount of time.

In this article, I will share how we can execute background jobs in ASP.NET Core, and it’s not a silver bullet that you will get all things done. Running background jobs also have some challenges.

Let’s dive in.

I will use a very popular open-source library called Hangfire for creating, scheduling, and executing jobs.

First, create a simple console app in Visual Studio and install the below-required packages for Hangfire. You can use the .NET API project to trigger the background job endpoint.

Prerequisite

  1. Create a dotnet API project, "VideoProcessor"; you can name whatever you want.
  2. Install the required packages.
    dotnet add package Hangfire.Core
    dotnet add package Hangfire.AspNetCore
    dotnet add package Hangfire.PostgreSql

Hangfire Core

Hangfire works with three main components.

  • Storage: Hangfire will keep all the necessary information about the jobs that are running, scheduled, or in the queue related to background job processing.
  • Client: It’s responsible for creating jobs and saving them in storage.
  • Server: The main part, the server, will query the jobs from the storage and execute them. Hangfire gives the ability to place the server in any process. Even if the process terminates, it will be retried automatically after restart.
  • Dashboard: Hangfire provides a user-friendly UI for monitoring and debugging the jobs. I love this!

Configuration

Add the following in the appsettings.json for connecting the database to the hangfire for storage.

"ConnectionStrings": {
  "HangfireConnection": "YourDbServer"
}

Add the following to register the required hangfire services into the dependency injection container.

services.AddHangfire(config =>
    config.UsePostgreSqlStorage(c => 
        c.UseNpgsqlConnection(configuration.GetConnectionString("HangfireConnection"))
    )
);

Add the following code to enable the hangfire dashboard and server.

app.UseHangfireServer();
app.UseHangfireDashboard();

You can access the hangfire dashboard by navigating to “/hangfire“. It’s open to everyone. To protect this endpoint, you can restrict it to only allow for authenticated users.

Use the following code for authorized access only. You can visit this to learn how to authorize the user request.

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new MyAuthorizationFilter() }
});

Fire-and-Forget and Delayed Jobs

The Fire-and-Forget job is one of the most common types of background jobs in Hangfire. It is designed to execute a task immediately in the background once it is enqueued.

BackgroundJob.Enqueue(() => 
    Console.WriteLine("Fire-and-Forget Job executed."));

A Delayed Job allows you to schedule a task to be executed after a certain amount of time has passed. It’s useful when you want to delay the execution of a task for a specific period.

BackgroundJob.Schedule(
    () => Console.WriteLine("Delayed Job executed after 10 seconds."),
    TimeSpan.FromSeconds(10)
);

Advanced Job Scheduling with Recurring and Continuations Jobs

A recurring job is a type of background job that executes on a regular schedule. Unlike fire-and-forget or delayed jobs that run only once, recurring jobs run repeatedly based on a schedule defined using a CRON expression.

RecurringJob.AddOrUpdate(
    "my-recurring-job", 
    () => Console.WriteLine("Recurring Job executed every 15 seconds."), 
    Cron.Minutely()
);

Recurring jobs are useful for tasks like periodic data backups. We can fine-tune the recurrence schedule using CRON expressions, giving flexibility on timing.

A continuation job is a job that only executes after a parent job has been completed. It is useful when you have tasks that depend on the successful execution of another job.

var parentJobId = BackgroundJob.Enqueue(() => 
    Console.WriteLine("Parent Job executed."));

BackgroundJob.ContinueWith(parentJobId, () => 
    Console.WriteLine("Continuation Job executed after the Parent Job."));

Continuation jobs are the perfect fit when we need to chain multiple dependent tasks. For example, we need to first process the data through the job and then send the notifications to the customers.

Managing Multiple Jobs in a Multi-Threaded Environment

In a multi-threaded environment, managing multiple jobs can get complex, especially when it comes to ensuring job concurrency, efficient resource utilization, and preventing race conditions.

Background Jobs are processed by a dedicated pool of worker threads that run inside the hangfire subsystem. When we run the application, it initializes the pool with a fixed number of threads. We can specify our number of threads.

var options = new BackgroundJobServerOptions 
{ 
    WorkerCount = Environment.ProcessorCount * 5 
};

app.UseHangfireServer(options);

Let's say we have these three jobs.

  • Data Import Job
  • Email Notification Job
  • Report Generation Job

We want these three jobs to be run on separate threads, and the benefit of this is efficient resource utilization.

public class BackgroundJobService
{
    public void ImportData()
    {
        Console.WriteLine("Data Import Job started on Thread: " + Thread.CurrentThread.ManagedThreadId);

        // Simulate long-running data import process
        Thread.Sleep(5000); // Simulate workload
        Console.WriteLine("Data Import Job completed.");
    }

    public void SendEmailNotification()
    {
        Console.WriteLine("Email Notification Job started on Thread: " + Thread.CurrentThread.ManagedThreadId);

        // Simulate sending email
        Thread.Sleep(2000); // Simulate workload
        Console.WriteLine("Email Notification Job completed.");
    }

    public void GenerateReport()
    {
        Console.WriteLine("Report Generation Job started on Thread: " + Thread.CurrentThread.ManagedThreadId);

        // Simulate report generation process
        Thread.Sleep(3000); // Simulate workload
        Console.WriteLine("Report Generation Job completed.");
    }
}

Let’s enqueue them.

BackgroundJob.Enqueue(() => _jobService.ImportData());
BackgroundJob.Enqueue(() => _jobService.SendEmailNotification());
BackgroundJob.Enqueue(() => _jobService.GenerateReport());

When we run the application, here hangfire will execute each job in parallel on a separate thread. The WorkerCount controls how many jobs can run in parallel based on your system’s resources (CPU cores).

Output

Jobs have been scheduled.
Data Import Job started on Thread: 5
Email Notification Job started on Thread: 6
Report Generation Job started on Thread: 7
Email Notification Job completed.
Report Generation Job completed.
Data Import Job completed.

Adding Priority

var options = new BackgroundJobServerOptions
{
    Queues = new[] { "critical", "default", "low" }
};

app.UseHangfireServer(options);

By setting the Queues property in the AddHangfireServer method, we dictate the order in which workers have to pick the job to execute from the queues.

Below is an example of how you can Queue the job based on their priority.

// Critical priority queue
BackgroundJob.Enqueue(() => Console.WriteLine("Critical job executed"), "critical");

// Default queue
BackgroundJob.Enqueue(() => Console.WriteLine("Default job executed"));

// Low priority queue
BackgroundJob.Enqueue(() => Console.WriteLine("Low priority job executed"), "low");

Hangfire runs background worker threads that continuously check the queues for new jobs. The worker will,

  • Check the "critical" queue first.
  • If jobs are present in the "critical" queue, the worker picks one to execute immediately.
  • If the "critical" queue is empty, the worker checks the "default" queue next.
  • Lastly, it checks the "low" queue.

Job Monitoring, Dashboard, and Best Practices for Job Management

Hangfire gives us the Dashboard feature, which allows us to monitor our jobs and debug them.

Dashboard feature

Job Monitoring in Hangfire allows developers and system administrators to track the status of background jobs, check their progress, and identify any issues that arise during execution.

Job States

Hangfire tracks several states for each job.

  • Enqueued: The job is waiting to be processed.
  • Processing: The job is currently being executed.
  • Succeeded: The job was completed successfully.
  • Failed: The job encountered an error during execution.
  • Deleted: The job has been removed from the queue.

Best Practices

  • Use specific queues to organize multiple jobs by giving them priority.
  • Set Up Automatic Retries, configure jobs to automatically retry a certain number of times when jobs fail due to exceptions, etc. We can specify the number of automatic retries using [AutomaticRetry(Attempts = 0)] attributes on the job.
  • Implement timeouts.
  • Log Errors, you can also visit the hangfire dashboard to get the stack trace of the error as well.
  • Continuation Jobs, for dependent tasks, use continuation jobs to run after one after another.
  • Because jobs are running in the background, they are time-consuming and resource utilizing and sometimes cost you need to monitor, so test them before going to production.

Conclusion

Hangfire is a great library, and so far, I have used it to manage and organize background jobs efficiently. In this article, we explored the management of background jobs in .NET Core using Hangfire, emphasizing key concepts such as job monitoring and best practices.


Similar Articles