Ответ
В 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для гибкости.
Ответ 18+ 🔞
А, ну это же классика, блядь! Фоновые задачи в ASP.NET Core — это как та собака Муму: вроде и нужна, но если неправильно сделать, она тебе всю квартиру засрет, а потом ещё и утопить её придётся, самому же хуже будет.
Смотри, вот тебе нормальный человеческий вариант, без этих заумных формулировок, которые в документации пишут люди, никогда в продакшене не работавшие.
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)
{
// Новый модный таймер, не надо этих Thread.Sleep, которые всю систему тормозят
using PeriodicTimer timer = new PeriodicTimer(_period);
// Крутимся, пока нас не попросят нахуй
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
// Вот тут самое важное, блядь! DbContext — он как одноразовый стаканчик.
// Нельзя один стаканчик на всё приложение использовать, иначе получишь гонки данных и исключения.
// Поэтому создаём новый scope для каждой итерации.
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>();
Теперь про подводные камни, о которые все расшибаются:
IServiceScopeFactory— это твой лучший друг. Без негоDbContextбудет жить как синглтон, и ты получишь такие гонки данных и исключения, что мало не покажется. Это как одну зубную щётку на всю семью использовать — гигиенический пиздец.- Ошибки ловить надо, ёпта! Без
try-catchлюбая мелочь (потеря соединения с БД, deadlock) убьёт твою службу нахуй, и она больше не запустится, пока приложение не перезагрузишь. CancellationToken— не просто для галочки. Когда приложение останавливается, оно вежливо просит задачи завершиться. Если его игнорировать, то при остановке будет долгий и мучительный timeout, пока система не прибьёт процесс молотком.- Если у тебя несколько серверов (нод), то эта служба запустится на каждом из них и будет делать одну и ту же работу. Это как нанять пять уборщиц убирать одну комнату — деньги на ветер. Для таких случаев нужны распределённые блокировки (через Redis или хотя бы блокировку в самой БД).
- Период выполнения (
_period) лучше выноси в настройки (appsettings.json). Сегодня раз в минуту, а завтра заказчик скажет "делай раз в 5 секунд", и не придётся пересобирать весь проект.
Вот и вся магия. Главное — не переусложняй, и всё будет работать как швейцарские часы. Ну, или как минимум как старые советские, которые тикают, но время показывают.