Как реализовать фоновую службу (Background Service) в ASP.NET Core для периодического изменения данных в базе данных?

«Как реализовать фоновую службу (Background Service) в ASP.NET Core для периодического изменения данных в базе данных?» — вопрос из категории ASP.NET Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В ASP.NET Core для выполнения фоновых задач используется интерфейс IHostedService или его удобная абстракция BackgroundService. Вот пошаговая реализация службы, которая раз в минуту обновляет записи в БД.

1. Создаем класс фоновой службы:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class DatabaseCleanupService : BackgroundService
{
    private readonly ILogger<DatabaseCleanupService> _logger;
    private readonly IServiceScopeFactory _scopeFactory; // Для создания скоупов
    private readonly TimeSpan _period = TimeSpan.FromMinutes(1); // Период выполнения

    public DatabaseCleanupService(
        ILogger<DatabaseCleanupService> logger,
        IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new PeriodicTimer(_period);

        while (!stoppingToken.IsCancellationRequested &&
               await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                // Создаем новый scope для каждой итерации, чтобы корректно
                // получать scoped сервисы (как DbContext).
                using (var scope = _scopeFactory.CreateScope())
                {
                    var dbContext = scope.ServiceProvider
                        .GetRequiredService<ApplicationDbContext>();

                    await PerformCleanupAsync(dbContext, stoppingToken);
                }

                _logger.LogInformation("Фоновая задача выполнена успешно.");
            }
            catch (Exception ex)
            {
                // Важно логировать ошибки, чтобы служба не "молча" падала
                _logger.LogError(ex, "Ошибка при выполнении фоновой задачи.");
            }
        }
    }

    private async Task PerformCleanupAsync(
        ApplicationDbContext context,
        CancellationToken ct)
    {
        // Пример: архивация старых записей
        var cutoffDate = DateTime.UtcNow.AddDays(-30);
        var oldRecords = await context.LogEntries
            .Where(e => e.CreatedAt < cutoffDate && !e.IsArchived)
            .ToListAsync(ct);

        foreach (var record in oldRecords)
        {
            record.IsArchived = true;
        }

        // Пример: обновление агрегированных данных
        var today = DateTime.UtcNow.Date;
        var dailyStats = await context.Orders
            .Where(o => o.Date == today)
            .GroupBy(o => 1)
            .Select(g => new { Total = g.Sum(o => o.Amount), Count = g.Count() })
            .FirstOrDefaultAsync(ct);

        // Сохранение статистики в отдельную таблицу...

        await context.SaveChangesAsync(ct);
    }
}

2. Регистрируем службу в Program.cs:

// Регистрируем DbContext (обычно уже есть)
builder.Services.AddDbContext<ApplicationDbContext>(options => 
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Регистрируем фоновую службу
builder.Services.AddHostedService<DatabaseCleanupService>();

Критически важные моменты:

  • Внедрение зависимостей: DbContext имеет scoped lifetime, поэтому его нельзя напрямую внедрять в singleton BackgroundService. Используйте IServiceScopeFactory для создания scope в каждом цикле.
  • Обработка ошибок: Всегда оборачивайте логику в try-catch. Непойманное исключение приведет к падению всей фоновой задачи.
  • Отмена операций: Всегда передавайте CancellationToken в асинхронные методы EF Core и другие операции, чтобы задача могла корректно завершиться при остановке приложения.
  • Распределенные блокировки: Для сценариев, где запущено несколько экземпляров приложения, используйте механизмы распределенных блокировок (например, через Redis или БД), чтобы задача выполнялась только на одной ноде.
  • Конфигурация: Выносите период выполнения (_period) в appsettings.json для гибкости.