В этой статье мы обсудим, как мы можем реализовать фоновые задания в .Net с библиотекой Hangfire.

Полное видео можно посмотреть на ютубе:

Если вы хотите получить полный доступ к исходному коду, поддержите меня на Patreon.

Начнем с обзора того, что такое фоновые задания.


Фоновые задания

Это задачи, которые выполняются в фоновом режиме без какого-либо вмешательства пользователя, что помогает приложению работать без сбоев.

Некоторые примеры фона

  • для длительных и продолжительных операций, таких как обработка данных.
  • Пакетный импорт из API, файлов…
  • Функции синхронизации
  • Функции отчетности.
  • Службы уведомлений.


Обзор Hangfire

  • Hangfire имеет открытый исходный код и используется для планирования фоновых заданий на определенное событие и время.
  • Hangfire будет выполнять фоновую обработку без вмешательства пользователя.
  • Hangfire предоставляет панель инструментов, которая помогает управлять всем


Типы заданий Hangfire

Hangfire поддерживает различные типы заданий, перечисленные ниже.

Работа «Выстрелил и забыл»: выполняются только один раз после определенных условий, нас не волнует результат работы.

Отложенная работа: выполняются только один раз, но через определенный интервал времени.

Повторяющаяся работа: ****выполняются много раз после заданного условия и интервала времени

Продолжение работы: ****выполняются, когда его родительское задание выполнено и завершено.


Кодирование

Теперь давайте создадим приложение

dotnet new webapi -n "FireAPI"
Войти в полноэкранный режим

Выйти из полноэкранного режима

Далее нам нужно установить пакеты

dotnet add package Hangfire 
dotnet add package Hangfire.Core 
dotnet add package Hangfire.Storage.SQLite 
dotnet add package Hangfire.Dashboard.Basic.Authentication
Войти в полноэкранный режим

Выйти из полноэкранного режима

Нам нужно добавить строку подключения к нашему Sqlite внутри appsettings.json.

"ConnectionStrings": {
    "DefaultConnection": "app.db"
  }
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь нам нужно настроить нашу конфигурацию Hangfire внутри нашего 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();
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте добавим папку Models и добавим нашу первую модель 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;}
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Далее нам нужно добавить контроллер, который позволит нам управлять драйверами, мы будем использовать базу данных в памяти.

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);
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте создадим сервис, который будет отвечать за отправку писем, обновление базы данных…

В корневом каталоге приложения нам нужно создать новую папку с именем Services и добавить следующий интерфейс IServiceManagement.

namespace FireApp.Services;

public interface IServiceManagement
{
    void SendEmail();
    void UpdateDatabase();
    void GenerateMerchandise();
    void SyncRecords();
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте создадим реализацию этого интерфейса.

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")}");
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Нам нужно добавить реализацию сервиса в FireApp

builder.Services.AddScoped<IServiceManagement, ServiceManagement>();
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте добавим долго работающую службу, которую мы хотим выполнять каждые 4 часа.

RecurringJob.AddOrUpdate<IServiceManagement>(x => x.SyncRecords(), "0 * * ? * *");
Войти в полноэкранный режим

Выйти из полноэкранного режима

Чтобы получить помощь с выражением cron, посетите следующий веб-сайт:

Теперь давайте обновим контроллер, чтобы добавить службы

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);
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Защита Hangfire

AppSetting.json нам нужно обновить его до следующего

"HangfireCredentials": {  
    "UserName": "mohamad",  
    "Password": "Passw0rd"  
  }
Войти в полноэкранный режим

Выйти из полноэкранного режима

И нам нужно обновить program.cs до следующего

app.UseHangfireDashboard("/hangfire", new DashboardOptions()
{
        DashboardTitle = "Hangfire Dashboard",  
    Authorization = new[]{  
    new HangfireCustomBasicAuthenticationFilter{  
        User = Configuration.GetSection("HangfireCredentials:UserName").Value,  
        Pass = Configuration.GetSection("HangfireCredentials:Password").Value  
    }
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Пожалуйста, задавайте любые вопросы в комментариях внизу.