Как запускать фоновые задачи в приложении NET Core
В этой записи блога я расскажу, как запускать фоновые задачи в веб-приложениях 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.Delay
5 секунд, прежде чем цикл выполнится снова.
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.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.