DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for .NET 6 - Background Jobs with Hangfire πŸ”₯πŸ”₯πŸ”₯
Mohamad Lawand
Mohamad Lawand

Posted on

.NET 6 - Background Jobs with Hangfire πŸ”₯πŸ”₯πŸ”₯

In This article we will be discussing how we can implement background jobs in .Net with Hangfire library

You can watch the full video on youtube:

If you would like to have full access to the source code please consider supporting me on Patreon
https://www.patreon.com/mohamadlawand

Will start by doing an over view of what are background jobs

Background Jobs

They are tasks that runs in the background without any user interventions which help the application to run smoothly.

Some of the examples of the background

  • for long and lengthy operations like data processing .
  • Batch import from APIs, files …
  • Synchronisation functionalities
  • Reporting functionalities.
  • Notification services.

Hangfire Overview

  • Hangfire is open-source and used to schedule background jobs at a particular event and time.
  • Hangfire will do background processing without user intervention.
  • Hangfire provides a dashboard which helps manage everything

Hangfire job types

Hangfire support different job types which are listed below

Fire-and-Forget Job: are executed only one time after certain conditions, we don’t care about the outcome of the job.

Delayed Job: are executed only once but after a specific interval of time.

Recurring Job: are ****executed many times after specified condition and time interval

Continuations Job: are ****executed when its parent job is executed and finished.

Coding

Now lets create the app

dotnet new webapi -n "FireAPI"
Enter fullscreen mode Exit fullscreen mode

Next we need to install the packages

dotnet add package Hangfire 
dotnet add package Hangfire.Core 
dotnet add package Hangfire.Storage.SQLite 
dotnet add package Hangfire.Dashboard.Basic.Authentication
Enter fullscreen mode Exit fullscreen mode

We need to add the connection string to our Sqlite inside the appsettings.json

"ConnectionStrings": {
    "DefaultConnection": "app.db"
  }
Enter fullscreen mode Exit fullscreen mode

Now we need to configure our Hangfire configuration inside our Program.cs

builder.Services.AddHangfire(configuration => configuration
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UseSQLiteStorage(builder.Configuration.GetConnectionString("DefautConnection")));

// Add the processing server as IHostedService
builder.Services.AddHangfireServer();

app.UseHangfireDashboard();
app.MapHangfireDashboard();
Enter fullscreen mode Exit fullscreen mode

Now let us add a Models folder and add our first Model Driver.cs

public class Driver
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
    public int DriverNumber { get; set; }
    public int Status {get;set;}
}
Enter fullscreen mode Exit fullscreen mode

Next we need to add a controller which will allow us to manage drivers, we will be utilising an in-memory database.

using FireApp.Models;
using Microsoft.AspNetCore.Mvc;

namespace FireApp.Controllers;

[ApiController]
[Route("[controller]")]
public class DriversController : ControllerBase
{
    private static List<Driver> drivers = new List<Driver>();

    private readonly ILogger<DriversController> _logger;

    public DriversController(ILogger<DriversController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult GetDrivers()
    {
        var items = drivers.Where(x => x.Status == 1).ToList();
        return Ok(items);
    }

    [HttpPost]
    public IActionResult CreateDriver(Driver data)
    {
        if(ModelState.IsValid)
        {
            drivers.Add(data);

            return CreatedAtAction("GetDriver", new {data.Id}, data);
        }

        return new JsonResult("Something went wrong") {StatusCode = 500};
    }

    [HttpGet("{id}")]
    public IActionResult GetDriver(Guid id)
    {
        var item = drivers.FirstOrDefault(x => x.Id == id);

        if(item == null)
            return NotFound();

        return Ok(item);
    }

    [HttpPut("{id}")]
    public IActionResult UpdateDriver(Guid id, Driver item)
    {
        if(id != item.Id)
            return BadRequest();

        var existItem = drivers.FirstOrDefault(x => x.Id == id);

        if(existItem == null)
            return NotFound();

        existItem.Name = item.Name;
        existItem.DriverNumber = item.DriverNumber;

        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult DeleteDriver(Guid id)
    {
        var existItem = drivers.FirstOrDefault(x => x.Id == id);

        if(existItem == null)
            return NotFound();

        existItem.Status = 0;

        return Ok(existItem);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let us create a service which will be responsible to sending emails, updating database …

In the root directory of the app we need to create a new folder called Services and add the following interface IServiceManagement

namespace FireApp.Services;

public interface IServiceManagement
{
    void SendEmail();
    void UpdateDatabase();
    void GenerateMerchandise();
    void SyncRecords();
}
Enter fullscreen mode Exit fullscreen mode

Now let us build an implementation of this interface

namespace FireApp.Services;

public class ServiceManagement : IServiceManagement
{
    public void GenerateMerchandise()
    {
       Console.WriteLine($"Generate Merchandise: long running service at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
    }

    public void SendEmail()
    {
        Console.WriteLine($"Send Email: delayed execution service at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
    }

    public void SyncRecords()
    {
        Console.WriteLine($"Sync Records: at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
    }

    public void UpdateDatabase()
    {
        Console.WriteLine($"Update Database: at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
    }
}
Enter fullscreen mode Exit fullscreen mode

We need to add the service implementation into the FireApp

builder.Services.AddScoped<IServiceManagement, ServiceManagement>();
Enter fullscreen mode Exit fullscreen mode

Now let us add a long running service which we want to execute every 4h

RecurringJob.AddOrUpdate<IServiceManagement>(x => x.SyncRecords(), "0 * * ? * *");
Enter fullscreen mode Exit fullscreen mode

For help with cron expression, check the following website: https://www.freeformatter.com/cron-expression-generator-quartz.html

Now let us update the controller to add the services

using FireApp.Models;
using FireApp.Services;
using Hangfire;
using Microsoft.AspNetCore.Mvc;

namespace FireApp.Controllers;

[ApiController]
[Route("[controller]")]
public class DriversController : ControllerBase
{
    private static List<Driver> drivers = new List<Driver>();

    private readonly ILogger<DriversController> _logger;

    public DriversController(
        ILogger<DriversController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult GetDrivers()
    {
        var items = drivers.Where(x => x.Status == 1).ToList();
        return Ok(items);
    }

    [HttpPost]
    public IActionResult CreateDriver(Driver data)
    {
        if(ModelState.IsValid)
        {
            drivers.Add(data);

            // Fire and Forget Job
            var jobId = BackgroundJob.Enqueue<IServiceManagement>(x => x.SendEmail());
            Console.WriteLine($"Job id: {jobId}");

            return CreatedAtAction("GetDriver", new {data.Id}, data);
        }

        return new JsonResult("Something went wrong") {StatusCode = 500};
    }

    [HttpGet("{id}")]
    public IActionResult GetDriver(Guid id)
    {
        var item = drivers.FirstOrDefault(x => x.Id == id);

        if(item == null)
            return NotFound();

        return Ok(item);
    }

    [HttpPut("{id}")]
    public IActionResult UpdateDriver(Guid id, Driver item)
    {
        if(id != item.Id)
            return BadRequest();

        var existItem = drivers.FirstOrDefault(x => x.Id == id);

        if(existItem == null)
            return NotFound();

        existItem.Name = item.Name;
        existItem.DriverNumber = item.DriverNumber;

        var jobId = BackgroundJob.Schedule<IServiceManagement>(x => x.UpdateDatabase(), TimeSpan.FromSeconds(20));
        Console.WriteLine($"Job id: {jobId}");

        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult DeleteDriver(Guid id)
    {
        var existItem = drivers.FirstOrDefault(x => x.Id == id);

        if(existItem == null)
            return NotFound();

        existItem.Status = 0;

        RecurringJob.AddOrUpdate<IServiceManagement>(x => x.SyncRecords(), Cron.Hourly);

        return Ok(existItem);
    }
}
Enter fullscreen mode Exit fullscreen mode

Securing Hangfire

AppSetting.json we need to update it to the following

"HangfireCredentials": {  
    "UserName": "mohamad",  
    "Password": "Passw0rd"  
  }
Enter fullscreen mode Exit fullscreen mode

And we need to update the program.cs to the following

app.UseHangfireDashboard("/hangfire", new DashboardOptions()
{
        DashboardTitle = "Hangfire Dashboard",  
    Authorization = new[]{  
    new HangfireCustomBasicAuthenticationFilter{  
        User = Configuration.GetSection("HangfireCredentials:UserName").Value,  
        Pass = Configuration.GetSection("HangfireCredentials:Password").Value  
    }
});
Enter fullscreen mode Exit fullscreen mode

Please ask any questions in the comments down below.

Top comments (0)

Super Useful CSS Resources

A collection of 70 hand-picked, web-based tools which are actually useful.
Each will generate pure CSS without the need for JS or any external libraries.