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