IHostedService и BackgroundService
Реализуйте IHostedService и абстрактный BackgroundService для выполнения фоновых задач внутри узла .NET.
«IHostedService и BackgroundService» — бесплатный урок C# Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Что такое фоновые службы
Фоновые службы выполняют длительные задачи параллельно с приложением ASP.NET Core: обрабатывают очереди, запускают запланированные задачи или контролируют ресурсы. В .NET доступны два основных варианта: IHostedService (минимальный) и абстрактный BackgroundService (удобный для циклической работы).
Интерфейс IHostedService
IHostedService содержит два метода: StartAsync (вызывается при запуске хоста) и StopAsync (вызывается при корректном завершении работы). Он идеально подходит для однократных задач запуска и завершения.
public class DatabaseMigratorService : IHostedService
{
private readonly IServiceScopeFactory _scopeFactory;
public DatabaseMigratorService(IServiceScopeFactory sf) => _scopeFactory = sf;
public async Task StartAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync(ct);
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
builder.Services.AddHostedService<DatabaseMigratorService>();BackgroundService: длительные циклы
BackgroundService — это абстрактный класс, реализующий IHostedService и предоставляющий ExecuteAsync(CancellationToken) — метод, который выполняется на протяжении всего времени жизни приложения.
public class HeartbeatService : BackgroundService
{
private readonly ILogger<HeartbeatService> _logger;
public HeartbeatService(ILogger<HeartbeatService> l) => _logger = l;
protected override async Task ExecuteAsync(CancellationToken ct)
{
_logger.LogInformation("Heartbeat service started");
while (!ct.IsCancellationRequested)
{
_logger.LogInformation("Heartbeat: {Time}", DateTime.UtcNow);
await Task.Delay(TimeSpan.FromSeconds(30), ct);
}
_logger.LogInformation("Heartbeat service stopping");
}
}
builder.Services.AddHostedService<HeartbeatService>();Использование сервисов с ограниченным временем жизни
Фоновые службы являются одиночными экземплярами. Чтобы использовать сервисы с ограниченным временем жизни (например, DbContext), всегда создавайте новую область для каждой единицы работы с помощью IServiceScopeFactory.
public class DataSyncService : BackgroundService
{
private readonly IServiceScopeFactory _factory;
private readonly ILogger<DataSyncService> _log;
public DataSyncService(IServiceScopeFactory f, ILogger<DataSyncService> l)
{ _factory = f; _log = l; }
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using (var scope = _factory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await SyncDataAsync(db, ct);
} // scope and DbContext disposed here
await Task.Delay(TimeSpan.FromMinutes(5), ct);
}
}
}Корректное завершение работы
При завершении работы хост активирует токен отмены. Ваш метод ExecuteAsync должен отслеживать этот токен и выполнять очистку. По умолчанию хост ожидает до 5 секунд, после чего принудительно останавливает процесс.
protected override async Task ExecuteAsync(CancellationToken ct)
{
try
{
while (!ct.IsCancellationRequested)
{
await DoWorkAsync(ct);
await Task.Delay(1000, ct);
}
}
catch (OperationCanceledException)
{
// Normal shutdown — not an error
_logger.LogInformation("Service stopping due to cancellation");
}
finally
{
// Cleanup
await CleanupAsync();
}
}Фоновая очередь на основе каналов
Распространённый шаблон: фоновая служба читает элементы из очереди на основе каналов, а конечные точки HTTP добавляют в неё новые задачи. Это отделяет обработку запросов от выполнения задач.
public class BackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, Task>> _queue
= Channel.CreateBounded<Func<CancellationToken, Task>>(100);
public ValueTask QueueAsync(Func<CancellationToken, Task> job)
=> _queue.Writer.WriteAsync(job);
public IAsyncEnumerable<Func<CancellationToken, Task>> DequeueAllAsync(
CancellationToken ct)
=> _queue.Reader.ReadAllAsync(ct);
}
// Register and use:
builder.Services.AddSingleton<BackgroundTaskQueue>();
builder.Services.AddHostedService<QueueProcessorService>();Обработка ошибок в фоновых службах
Необработанные исключения в ExecuteAsync могут незаметно остановить службу. Оберните основной цикл в try/catch и журналируйте ошибки, чтобы служба продолжала работу несмотря на отдельные сбои.
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await ProcessNextBatchAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
break; // normal shutdown
}
catch (Exception ex)
{
_logger.LogError(ex, "Background processing error. Retrying in 5s.");
await Task.Delay(5000, ct); // backoff before retry
}
}
}StopAsync и тайм-аут завершения
Переопределите StopAsync, чтобы выполнить очистку. Хост передаёт токен завершения — если очистка занимает больше времени, чем указано в тайм-ауте, процесс в любом случае будет завершён.
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Service is stopping...");
// Signal internal work to stop
_internalCts.Cancel();
// Wait for the Execute loop to complete (up to timeout)
await base.StopAsync(stoppingToken);
_logger.LogInformation("Service stopped");
}
// Increase the shutdown timeout if needed:
builder.Services.Configure<HostOptions>(opt =>
opt.ShutdownTimeout = TimeSpan.FromSeconds(30));Несколько фоновых служб
Зарегистрируйте несколько размещённых служб — все они будут выполняться одновременно. Хост запускает их по порядку, а останавливает в обратном порядке.
builder.Services.AddHostedService<DatabaseMigratorService>(); // startup task
builder.Services.AddHostedService<MetricsCollectorService>(); // continuous
builder.Services.AddHostedService<EmailNotificationService>(); // continuous
builder.Services.AddHostedService<CacheWarmupService>(); // startup task
// All run concurrently after app start
// Stopped in reverse registration order on shutdownПрактический пример: обработчик заказов
Фоновая служба, обрабатывающая ожидающие заказы из очереди базы данных с правильным созданием областей и обработкой ошибок.
public class OrderProcessorService : BackgroundService
{
private readonly IServiceScopeFactory _factory;
private readonly ILogger<OrderProcessorService> _log;
public OrderProcessorService(IServiceScopeFactory f, ILogger<OrderProcessorService> l)
{ _factory = f; _log = l; }
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
using var scope = _factory.CreateScope();
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
int count = await processor.ProcessPendingAsync(ct);
_log.LogInformation("Processed {Count} orders", count);
}
catch (Exception ex) when (!ct.IsCancellationRequested)
{
_log.LogError(ex, "Order processing failed");
}
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
}
}Быстрая проверка
Почему фоновые службы должны использовать IServiceScopeFactory, а не внедрять DbContext напрямую?
Итоги: IHostedService и BackgroundService
Главные выводы:
IHostedService: StartAsync/StopAsync — для задач запуска и завершенияBackgroundService: цикл ExecuteAsync — для непрерывной фоновой работы- Всегда используйте
IServiceScopeFactoryдля зависимостей с ограниченным временем жизни внутри фоновых служб - Отслеживайте токен отмены при каждом await для корректного завершения работы
- Оберните тело цикла в try/catch, чтобы служба не завершалась из-за ошибок
- Используйте
HostOptions.ShutdownTimeout, если очистка занимает больше 5 секунд
Часто задаваемые вопросы
Урок «IHostedService и BackgroundService» бесплатный?
Да — полный текст урока «IHostedService и BackgroundService» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «IHostedService и BackgroundService»?
Реализуйте IHostedService и абстрактный BackgroundService для выполнения фоновых задач внутри узла .NET. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «IHostedService и BackgroundService»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- IHostedService и BackgroundService
- Проекты рабочих служб
- Периодические задачи и таймеры
- Плановые задания Quartz.NET