Как лучше всего запускать фоновый сервис в ASP.NET Core?

Ответ

Предпочтительный способ — использование IHostedService и его удобной абстракции BackgroundService. Они интегрированы в систему зависимостей и жизненный цикл хоста приложения, что обеспечивает корректный запуск и graceful shutdown.

1. Реализация через BackgroundService (рекомендуется для большинства задач):

public class TimedBackgroundService : BackgroundService
{
    private readonly ILogger<TimedBackgroundService> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Background Service запущен.");

        // Периодическая фоновая задача
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                // Работа, выполняемая каждые 5 секунд
                _logger.LogInformation("Выполняется фоновая задача в: {time}", DateTimeOffset.Now);
                // await DoWorkAsync(stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Background Service остановлен.");
        }
    }
}

2. Регистрация в DI-контейнере:

// В файле Program.cs
builder.Services.AddHostedService<TimedBackgroundService>();

Ключевые преимущества этого подхода:

  • Управление жизненным циклом: Сервис автоматически запускается при старте приложения и останавливается при его завершении.
  • Graceful Shutdown: Параметр stoppingToken автоматически отменяется при попытке остановки хоста, давая сервису время на корректное завершение операций.
  • Внедрение зависимостей: Полноценная поддержка DI, можно инжектировать любые зарегистрированные сервисы (репозитории, клиенты API, логгеры).
  • Асинхронность: Базовая реализация асинхронна.

Альтернативы и когда их использовать:

  • IHostedService (прямая реализация): Если нужен более тонкий контроль над процессом запуска/остановки.
  • Специализированные библиотеки:
    • Hangfire: Для сложных фоновых заданий с планировщиком, повторными попытками, панелью мониторинга.
    • Quartz.NET: Для высокоточного планирования по cron-выражениям.
  • Timer или Task.Run в контроллере: Антипаттерн. Задачи могут быть потеряны при перезапуске приложения, сложно управлять, нет graceful shutdown.

Важные моменты для BackgroundService:

  • Переопределяйте ExecuteAsync для основной логики.
  • Всегда используйте переданный stoppingToken для проверки отмены в долгих циклах или асинхронных операциях.
  • Обрабатывайте исключения внутри сервиса, чтобы одно упавшее фоновое задание не привело к падению всего хоста приложения.

Ответ 18+ 🔞

Ну, слушай, тут народ постоянно спрашивает, как бы им такую штуку запилить, чтобы что-то там на фоне тикало само по себе, типа отправки писем или чистки базы. И знаешь, какие дикие костыли иногда предлагают? Прям волосы дыбом, ей-богу.

Так вот, есть нормальный, каноничный способ, который все умные дядьки используют — это BackgroundService. Это такая удобная абстракция, она уже встроена в сам движок приложения, и с ней всё работает как часы: запускается с программой, останавливается с ней же, и зависимости туда можно запихнуть любые. Красота, а не подход.

Смотри, как это выглядит на практике:

public class TimedBackgroundService : BackgroundService
{
    private readonly ILogger<TimedBackgroundService> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Background Service запущен.");

        // Таймер, который будет будить нас каждые 5 секунд
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

        try
        {
            // Крутимся в цикле, пока приложение живо
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                // Вот тут делаем свою полезную работу
                _logger.LogInformation("Выполняется фоновая задача в: {time}", DateTimeOffset.Now);
                // await DoWorkAsync(stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // Нас вежливо попросили завершиться
            _logger.LogInformation("Timed Background Service остановлен.");
        }
    }
}

А чтобы эта магия заработала, нужно всего одну строчку добавить:

// В файле Program.cs
builder.Services.AddHostedService<TimedBackgroundService>();

Вот и вся магия, блядь. Сервис встроится в жизненный цикл, и когда приложение будут выключать, ему передадут специальный сигнал (stoppingToken) — это чтобы он мог не обрывать всё на полуслове, а аккуратно завершить текущую итерацию. Очень цивилизованно.

А теперь, блядь, что НЕ надо делать, а то руки оторву:

  • Timer или Task.Run всунуть прямо в контроллер — это пиздец какой антипаттерн. Приложение перезапустится, а твои таймеры повиснут в памяти как неприкаянные, или вообще все задачи похерятся. Никакого graceful shutdown, никакого контроля. Делать так — это себя не уважать.
  • Писать свою сложную систему потоков — зачем, если всё уже придумано? Только времени потратишь дохуя, а потом ещё и баги ловить.

Короче, запомни: для 95% задач хватит BackgroundService. Если нужна адская сложность с расписанием по крону, повторными попытками и мониторингом — тогда смотри в сторону Hangfire или Quartz.NET. Но для начала бери BackgroundService — не прогадаешь.

Главное, не забудь токен (stoppingToken) в свои долгие операции прокидывать и исключения внутри ловить, чтобы из-за одной ерунды весь сервис не лег.