Как запускать фоновые задачи в приложении NET Core

  • Михаил
  • 8 мин. на прочтение
  • 29
  • 24 Feb 2024
  • 24 Feb 2024

В этой записи блога я расскажу, как запускать фоновые задачи в веб-приложениях net Core, используя инфраструктуру и API, предоставляемые платформой ASP.Net Core. Существует множество сценариев, в которых вы хотите постоянно выполнять задачу в фоновом режиме. Часто мы создаем собственный интерфейс и классы и каким-то образом подключаем их к классу Startup для достижения этой функциональности. В этой записи блога я собираюсь рассказать о двух основных способах выполнения фоновых задач в веб-приложениях ASP.Net Core. Оба способа предоставляются базовой платформой ASP.Net «из коробки». Как я упоминал ранее, мы можем запускать фоновые задачи в ASP.NET Core, используя две разные конструкции, предоставляемые платформой ASP.NET Core.

Они заключаются в следующем:

  • Во-первых, мы можем реализовать IHostedServiceинтерфейс
  • Во-вторых, мы можем получить производный от BackgroundServiceабстрактного базового класса

Фоновые задачи с использованием IHostedService

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

Для создания постоянно выполняющейся фоновой задачи давайте представим, что у нас есть процесс печати. Он будет печатать возрастающее целое число.

Для этого я создам новый класс BackgroundPrinter. Этот класс будет реализовывать IHostedServiceинтерфейс.

Интерфейс IHostedServiceпредоставляет два метода: StartAsyncи StopAsync. Метод StartAsync— это место, с которого должна быть запущена задача. В то время как StopAsyncметод — это то, где мы должны реализовать логику, пока задача остановлена.

Несколько важных моментов, которые следует помнить о StartAsyncметоде:

  • Во-первых, StartAsyncметод вызывается платформой до вызова Configureметода класса.Startup
  • Во-вторых, StartAsyncметод вызывается до запуска сервера.

Наконец, если реализация класса IHostedServiceиспользует какой-либо неуправляемый объект, класс должен реализовать IDisposableинтерфейс для удаления неуправляемых объектов.

Реализация IHostedService с объектом Timer

Для первой версии кода я реализую Timerвнутри BackgroundPrinterкласса. И через интервал таймера я распечатаю увеличенное число.

Во-первых, я объявлю в классе Timerобъект и целочисленную переменную .number

Во-вторых, внутри StartAsyncя создам новый экземпляр файла Timer.

В-третьих, для Timerделегата я определю анонимную функцию, в которой буду увеличивать и печатать целое число, используя ILoggerопределенную как переменную уровня класса.

В-четвертых, я настрою таймер на работу с 5-секундным интервалом.

Наконец, я реализую IDisposableинтерфейс и Disposeметод для вызова метода Timerобъекта Dispose.

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
    public class BackgroundPrinter : IHostedService, IDisposable
    {
        private readonly ILogger<BackgroundPrinter> logger;
        private Timer timer;
        private int number;
        public BackgroundPrinter(ILogger<BackgroundPrinter> logger,
            IWorker worker)
        {
            this.logger = logger;
        }
        public void Dispose()
        {
            timer?.Dispose();
        }
        public Task StartAsync(CancellationToken cancellationToken)
        {
            timer = new Timer(o => {
                Interlocked.Increment(ref number);
                logger.LogInformation($"Printing the worker number {number}");
            },
            null,
            TimeSpan.Zero,
            TimeSpan.FromSeconds(5));
            return Task.CompletedTask;
        }
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

Настройка класса BackgroundPrinter

Как только BackgroundPrinterкласс будет готов, мы настроим его для контейнера внедрения зависимостей, используя AddHostedServiceметод расширения в IServiceCollectionинтерфейсе.

Теперь мы можем сделать это либо в Startupклассе, либо в Programклассе. Я предпочитаю делать это в классе программы, просто для четкого разделения.

Поэтому я собираюсь обновить Programкласс, чтобы добиться этого. Для этого в CreateHostBuilderметоде я буду использовать ConfigureServicesметод расширения интерфейса .IHostBuilder

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundTask.Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                }).ConfigureServices(services =>
                    services.AddHostedService<BackgroundPrinter>());
    }
}

Запуск приложения

Теперь я собираюсь запустить приложение, чтобы увидеть вывод в консоли.

Как только я запущу приложение, я смогу получить доступ к конечным точкам API. В то же время консоль будет печатать автоматически увеличивающееся число каждые 5 секунд в фоновом режиме.

Вывод фоновой задачи

Внедрение зависимостей с помощью фоновой задачи

В приложении промышленного масштаба мы, вероятно, не будем использовать логику внутри класса, реализующего IHostedService. Мы не будем делать это для того, чтобы обеспечить разделение задач и правильное определение ответственности.

В большинстве случаев логика того, что происходит при выполнении задачи, вероятно, будет инкапсулирована в отдельный класс. Следовательно, очень важно, чтобы внедрение зависимостей работало с фоновыми задачами.

Хорошая новость заключается в том, что, поскольку фоновые задачи настраиваются в файле IServiceCollection, любые типы, добавленные в контейнер внедрения зависимостей, будут доступны для фоновой задачи.

Рабочий класс

Чтобы продемонстрировать это, я создам класс Workerи интерфейс IWorker, который будет иметь логику увеличения и печати числа. И BackgroundPrinterкласс будет просто использовать интерфейс IWorkerдля выполнения логики.

Интерфейс IWorkerбудет иметь единственный метод DoWork. Примет DoWorkодин параметр — экземпляр CancellationTokenкласса.

Для рабочего класса внутри DoWorkметода вместо запуска таймера я создам цикл while. И whileцикл будет ждать запроса на отмену экземпляра CancellationToken. Экземпляр CancellationTokenбудет передан из StartAsyncметода BackgroundPrinterкласса.

Внутри whileцикла я увеличу целое число уровня класса и выведу его на консоль, используя метод ILogger. И в конце цикла whileя подожду Task.Delay5 секунд, прежде чем цикл выполнится снова.

using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
    public interface IWorker
    {
        Task DoWork(CancellationToken cancellationToken);
    }
}
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
    public class Worker : IWorker
    {
        private readonly ILogger<Worker> logger;
        private int number = 0;
        public Worker(ILogger<Worker> logger)
        {
            this.logger = logger;
        }
        public async Task DoWork(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                Interlocked.Increment(ref number);
                logger.LogInformation($"Worker printing number {number}");
                await Task.Delay(1000 * 5);
            }
        }
    }
}

Как только Workerкласс будет готов, я обновлю его BackgroundPrinter, чтобы использовать IWorkerинтерфейс бизнес-логики.

Помимо инъекции ILogger, теперь я буду внедрять IWorkerеще и в конструктор класса BackgroundPrinter.

using Microsoft.Extensions.Hosting;

using Microsoft.Extensions.Logging;

using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
    public class BackgroundPrinter : IHostedService
    {
        private readonly ILogger<BackgroundPrinter> logger;
        private readonly IWorker worker;
        public BackgroundPrinter(ILogger<BackgroundPrinter> logger,
            IWorker worker)
        {
            this.logger = logger;
            this.worker = worker;
        }
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            await worker.DoWork(cancellationToken);
        }
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

Наконец, я зарегистрирую Workerкласс в контейнере внедрения зависимостей внутри класса Startup. Я обновлю ConfigureServicesметод, чтобы добавить Workerкласс как одноэлементный экземпляр в контейнер внедрения зависимостей.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundTask.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSingleton<IWorker, Worker>();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Я не буду вносить никаких изменений в Programкласс, так как мы по-прежнему используем BackgroundPrinterкласс для фоновой задачи. Теперь, если я запущу приложение, я увижу в консоли тот же ответ, что и раньше.

Фоновые задачи с использованием BackgroundService

Использование BackgroundServiceабстрактного базового класса — это второй способ запуска фоновых задач. Реализация BackgroundServiceотносительно проще по сравнению с IHostedService. Но в то же время у вас меньше контроля над тем, как запускать и останавливать задачу.

Чтобы продемонстрировать, как BackgroundServiceработает абстрактный базовый класс, я создам новый класс DerivedBackgroundPrinter. Этот новый класс уедет из BackgroundServiceкласса.

И на этот раз я просто буду использовать IWorkerинтерфейс для бизнес-логики. Следовательно, я создам зависимость конструктора от IWorkerинтерфейса. Поскольку IWorkerинтерфейс уже настроен в контейнере внедрения зависимостей, он будет автоматически передан этому классу.

Абстрактный BackgroundServiceбазовый класс предоставляет единственный абстрактный метод ExecuteAsync. У нас будет реализация для вызова DoWorkинтерфейса IWorkerвнутри этого ExecuteAsyncметода.

using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
    public class DerivedBackgroundPrinter : BackgroundService
    {
        private readonly IWorker worker;
        public DerivedBackgroundPrinter(IWorker worker)
        {
            this.worker = worker;
        }
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await worker.DoWork(stoppingToken);   
        }
    }
}

Как только этот класс будет готов, я обновлю его Program, чтобы использовать DerivedBackgroundPrinterего вместо BackgroundPrinterсредства запуска фоновых задач.

Следовательно, внутри CreateHostBuilderметода класса Programя заменю на BackgroundPrinterдля DerivedBackgroundPrinterвызова AddHostedServiceуниверсального метода расширения.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundTask.Demo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                }).ConfigureServices(services =>
                    services.AddHostedService<DerivedBackgroundPrinter>());
    }
}

Теперь, если я запущу приложение, я увижу тот же ответ, что и раньше.

Заключение

Как видите, создавать фоновые задачи с использованием IHostedServiceинтерфейса ASP.NET Core и BackgroundServiceабстрактного базового класса чрезвычайно просто. Самое приятное то, что нет никакой внешней зависимости от пакета NuGet.