# Services background in ASP. NET Core - Part 1: The Pages

<!--category-- ASP.NET Core, IHostedService, BackgroundService, Hangfire -->
<datetime class="hidden">2025-11-27T09:00</datetime>

Кожна сучасна веб- програма має роботу, яка не повинна блокувати HTTP- pythens, що відповідає електронним листам, обробляти файли, синхронізувати з зовнішніми службами, запущена за розкладом. ASP. NET Core надає декілька підходів для виконання цієї фонової роботи, з простого `IHostedService` Реалізація таких складних структур, як Ганг-файт. і коли використовувати кожну з них.

# Вступ

Сповідання; мені подобається фонова служба, цей сайт (на сайті BLOG!) має понад півдюжини з них, які виконують серйозні фонові завдання, але як і все, що вони мають деякі ODDITIONS та практики, що зробить ваше використання ними набагато приємніше.
Служби тла є героями unsunge у сучасних веб- програмах. Зберіть ваші регулятори з запитами HTTP на передньому плані, фонові служби спокійно оброблятимуть електронні листи з черги, індексувати вміст для пошуку, перевірте зовнішні API, чи немає у вас тимчасових файлів, і скористайтеся іншими завданнями, які б у іншому випадку блокували потік даних вашого запиту.

У цій серії з двох частин ми розглянемо різні підходи до впровадження фонових служб в Ядрі АСП.NET, з вбудованого `IHostedService` і `BackgroundService` У частині 1 ми розглянемо фундаментальні підходи та їх характеристики. [Частина 2](/blog/background-services-in-aspnetcore-part2)Ми зануримося в реалізацію реального світу з виробничої бази коду.

> **Важливість:** Ми звернемо особливу увагу на життєвий окрасний режим `StopAsync` Метод, у якому багато розробників стикаються з загадковими винятками, коли їхні програми завершуються.

[TOC]

# Чому служби виховання дітей?

Перш ніж зануритись у "як," давайте коротко розглянемо "чому." Служби тла дозволяють вам:

1. **Перезавантаження повільних операційNoun, a list of items** - Не примушуйте користувачів чекати, поки ви надсилаєте повідомлення електронної пошти або створюєте PDFs
2. **Розклад регулярних завдань** - Прибираю старі записи щоночі о другій годині.
3. **Черги обробки** - Опрацьовувати повідомлення з каналів або повідомлень брокерів
4. **Спостерігати за зовнішнім станом** - Опитування API або спостереження за змінами у файлових системах
5. **Координатний потік** - Керування багатокроковими процесами, які протяжують хвилини або години

Ядро ASPNET надає декілька підходів до впровадження цих послуг, кожен з яких має різні компроміси.

# The Historical context: Why Services background is now feasible

У " старі дні " (до 2010 року) запущена фонова робота у вашій веб- програмі вважалася поганою ідеєю. Звичайна думка така: " Сервери інтернету виконують веб- запити. Тло роботи належить окремому серверу ."

Це не було просто вантажом-сектною мудрістю, вона базувалася на реальних технічних обмеженнях:

## Епоха безшлюбних

Перші веб- сервери (і, чесно кажучи, служби " cheap " azure "), зазвичай запускалися на **одночинні або подвійні процесори**. Якщо ви виконали фонове завдання з наповнення процесора, воно безпосередньо конкурувало з запитами щодо веб для одного ядра:

```
Single Core (2005):
┌─────────────────────┐
│  Background Task    │  ← Uses 80% CPU
│  (80% of core)      │
├─────────────────────┤
│  Web Requests       │  ← Only 20% left!
│  (20% of core)      │  ← Slow responses
└─────────────────────┘
```

Результат: ваш веб - сайт став лінивим за час, коли було прокладено роботу у фоновому режимі.

## Порожня кількість гілочок

Використано класичний ASP. NET **thread-per- request**. Коледж гілок був відносно маленьким (типово, 25- 100 ниток), а завдання у тлі вкрали гілки, які мають обробляти веб- запити:

```csharp
// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
    // This steals a thread from the pool!
    ProcessLongRunningTask();
});

// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable
```

## Реклінг програм IIS Pool

IIS буде агресивно рефінансувати програми (перезапустити вашу програму) на основі обмежень на пам' ять, кількості запитів або розкладу часу. Робота з тлом буде припинено у середині роботи:

```
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
      - Background task killed
      - Work lost, must start again
```

## Обмежена підтримка Async/Await

Перед . NET 4,5 (20202) асинхронізацією була (♪) болісна програма. Задача тла часто заблокувала гілки без потреби:

```csharp
// Pre-async (2008)
void ProcessEmails()
{
    foreach (var email in GetEmails())
    {
        smtp.Send(email);  // Blocks thread for 500ms per email
    }
}
// 100 emails = 50 seconds of blocked thread time
```

## Що змінилося: сучасна доба

Сьогоднішній пейзаж разюче відрізняється:

### 1. Багатокорабельність є доступною

Економіка перегорнулася. Хмари ВМС з декількома ядрами досить дорого оцінилися, а голі металеві сервери на диво дешеві. Цей блог працює на присвяченому 8-нісному сервері, який коштує менше, ніж Azure VMand Я отримую всі ці ядра до себе, без галасливих сусідів. Фонове завдання на одному ядрі не значно впливає на веб- запити на інші ядра:

```
8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task
```

### 2. Асинхронно/ Чекати всюди

Сучасна версія. NET робить асинхронізоване програмування тривіальним. Задачі тла можуть зачекати на В/ В без гілок блокування:

```csharp
// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
    await foreach (var email in GetEmailsAsync(ct))
    {
        await smtp.SendAsync(email, ct);  // Doesn't block thread!
    }
}
// 100 emails processed efficiently, thread returns to pool during I/O
```

### 3. Кращий прихисток процесів

- [**Панель**](https://www.docker.com/) - Службу на задньому плані в контейнерах не переробляють довільно.
- [**Kubernetes**](https://kubernetes.io/) - Доречно граційне керування завершенням роботи з `SIGTERM`
- [**systemd**](https://systemd.io/) - Служби Linux, які повторно перезапускають
- **Служби WindowsComment** - Належна модель довгострокового процесу

### 4 Канали і сучасні примітиви

. NET тепер має підтримку першого класу для конструктивного програмування з [`System.Threading.Channels`](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels):

```csharp
// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);

// Producer (web request)
await channel.Writer.WriteAsync(email);  // Fast, non-blocking

// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(email);  // Efficient, async
}
```

### 5. Обмеження ресурсів і групи

Сучасні оркестродавці дозволяють вам мати контейнери. **обмежити використання ресурсів**:

```yaml
# Kubernetes resource limits
resources:
  limits:
    cpu: "500m"        # Background task can't use more than 0.5 CPU
    memory: "512Mi"    # Or more than 512 MB RAM
```

Это значит, что взбещенное фоновое задание не может голодать из-за твоего веб-дзвенника.

Питання не в тому, "Чи можемо ми запустити фонові служби в нашому веб-допомоді?" а "**А варто?**"Ми дослідимо це рішення у розділі "Коли НЕ використовувати фонові служби" пізніше.

# Вбудовані параметри

## IHosedService: The Foundation

В основі кожної фонової служби в агенції ядра ASP.NET [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice). Цей інтерфейс дуже простий:

```csharp
public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}
```

Ось так. Два методи. `StartAsync` викликається після запуску вашої програми і `StopAsync` коли він зачиняється.

Зареєструйте вашу службу у `Program.cs`:

```csharp
builder.Services.AddHostedService<MyBackgroundService>();
```

Ось вам візуалізація життєвого циклу:

```mermaid
graph LR
    A[Application Starts] --> B[StartAsync Called]
    B --> C[Service Running]
    C --> D[Application Shutting Down]
    D --> E[StopAsync Called]
    E --> F[Application Stopped]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style F stroke:#dc2626,stroke-width:3px,color:#ef4444
```

### StartAync: Синхронний/ Асинхронний початок

При впровадженні критичне рішення `IHostedService` те, чи ваша `StartAsync` метод повинен блокувати або повертати негайно.

**Синхронний (блокування) початок:**

```csharp
public class BlockingStartService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // This blocks application startup until complete
        await InitializeDatabaseAsync(cancellationToken);
        await LoadConfigurationAsync(cancellationToken);

        // Only now will the application continue starting
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

**Асинхронний (не- блокування) початок:**

```csharp
public class NonBlockingStartService : IHostedService
{
    private Task _backgroundTask;
    private readonly CancellationTokenSource _cts = new();

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start background work but return immediately
        _backgroundTask = Task.Run(async () =>
        {
            // Give other services time to initialise
            await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
            await DoLongRunningWorkAsync(_cts.Token);
        }, _cts.Token);

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        await _backgroundTask; // Wait for completion
    }
}
```

**Під час використання кожного підходу:**

- **Блокування:** Якщо служба має завершити ініціалізацію, перш ніж програма зможе виконувати запити (наприклад, завантаження критичних налаштувань, кеші розігрівання)
- **Не блокування:** Якщо служба може ініціюватися у тлі під час запуску інших служб (наприклад, індексування існуючого вмісту, синхронізація з зовнішніми службами)

### StopAync: The Common Pittall

Ось де речі стають цікавими і де багато розробників стикаються з проблемами. `StopAsync` на всіх службах, які приймаються. У вас є обмежене вікно (типово, 5 секунд), для того, щоб обережно прибрати. Ви можете розширити це вікно у `Program.cs`:

```csharp
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
```

**Найпоширеніша помилка:**

```csharp
public class BrokenService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WRONG: The channel is still open, ProcessMessagesAsync
        // will hang on WaitToReadAsync forever!
        return Task.CompletedTask;
    }

    private async Task ProcessMessagesAsync()
    {
        // This will never exit because the channel is never completed
        await foreach (var message in _channel.Reader.ReadAllAsync())
        {
            await ProcessAsync(message);
        }
    }
}
```

Після запуску цієї служби і зупинки вашої програми ви побачите помилки на зразок:

```
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
```

Або програма просто повісить слухавку на час очікування завершення роботи перед потужним завершенням роботи.

**Правильний підхід:**

```csharp
public class CorrectService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly CancellationTokenSource _cts = new();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync(_cts.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // CORRECT: Signal cancellation and complete the channel
        await _cts.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            // Wait for processing to finish or for the shutdown timeout
            await Task.WhenAny(_processingTask,
                Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            // Expected when shutdown timeout is reached
        }
    }

    private async Task ProcessMessagesAsync(CancellationToken token)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(token))
        {
            try
            {
                await ProcessAsync(message);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested, exit gracefully
                break;
            }
        }
    }
}
```

**Коефіцієнт ключа для правильної реалізації stopAync:**

1. **Скасовування сигналу** - Використовувати `CancellationTokenSource` і скасовує
2. **Завершувати канали** - Якщо ви використовуєте канали, дзвоніть `Writer.Complete()`
3. **Чекати на фонові завдання** - Використання `Task.WhenAny` з позначкою завершення роботи
4. **Обробка скасованої операції** - Очікується, що буде спіймано.
5. **Не викидайте винятків** - Винятки в `StopAsync` може викликати непередбачувану поведінку.

## SpaceService: Зручний базовий клас

Записую `IHostedService` Реалізація може бути одноманітною. Вам завжди потрібні фонові завдання, джерело ключа скасування і той самий шаблон очищення. [`BackgroundService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) Оправляйте цю філіжанку для вас:

```csharp
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executeTask;
    private CancellationTokenSource _stoppingCts;

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executeTask = ExecuteAsync(_stoppingCts.Token);
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executeTask == null) return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

    public virtual void Dispose()
    {
        _stoppingCts?.Cancel();
    }
}
```

Ви просто реалізуєте `ExecuteAsync` і нехай базовий клас керує водопроводом:

```csharp
public class SimpleBackgroundService : BackgroundService
{
    private readonly ILogger<SimpleBackgroundService> _logger;

    public SimpleBackgroundService(ILogger<SimpleBackgroundService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Service starting");

        // Wait for app to finish starting
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }

        _logger.LogInformation("Service stopping");
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        _logger.LogInformation("Doing work...");
        // Your actual work here
        await Task.Delay(1000, token);
    }
}
```

### Коли використовувати SpaceService vs IHosedService

**Користування `BackgroundService` коли:**

- Вам потрібен фоновий цикл з довгостроковим відставанням
- Вам потрібна проста періодична виконання
- Тобі не потрібен контроль над `StartAsync`/`StopAsync` час

**Користування `IHostedService` коли:**

- Вам потрібно контролювати те, що відбувається в `StartAsync` Фонова робота vs
- Ви встановлюєте обробники подій або спостерігачі замість неперервного циклу
- Вам слід координувати роботу з іншими службами під час запуску

# Додатково: Запустити координацію

Іноді вам потрібні служби, щоб зачекати один на одного. Наприклад, ви можете бажати, щоб ваш інструмент індексування семантики чекав на завершення початкового навантаження на обробник файлів markdown.

Ось зразок координування запуску служби:

```csharp
public interface IStartupCoordinator
{
    void RegisterService(string serviceName);
    void SignalReady(string serviceName);
    bool IsServiceReady(string serviceName);
    Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
    Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}

public class StartupCoordinator : IStartupCoordinator
{
    private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
    private readonly ILogger<StartupCoordinator> _logger;

    public void RegisterService(string serviceName)
    {
        _services.TryAdd(serviceName, new TaskCompletionSource());
    }

    public void SignalReady(string serviceName)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            tcs.TrySetResult();
            _logger.LogInformation("{Service} is ready", serviceName);
        }
    }

    public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            await tcs.Task.WaitAsync(ct);
        }
    }

    public async Task WaitForAllServicesAsync(CancellationToken ct = default)
    {
        await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
    }
}
```

Використання в службі:

```csharp
public class DependentService : IHostedService
{
    private readonly IStartupCoordinator _coordinator;
    private readonly ILogger<DependentService> _logger;

    public DependentService(
        IStartupCoordinator coordinator,
        ILogger<DependentService> logger)
    {
        _coordinator = coordinator;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Wait for another service to be ready
        await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);

        _logger.LogInformation("Dependencies ready, starting work");

        // Do your work...

        // Signal you're ready for services that depend on you
        _coordinator.SignalReady("DependentService");
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

Цей шаблон стане особливо корисним, якщо у вас є декілька фонових служб з взаємозалежними можливостями.

# Розподілити координацію з Redis

Координатор запуску працює у окремому екземплярі програми. Але що станеться, якщо ви зміните масштаб до декількох екземплярів? Вам не потрібно, щоб всі три екземпляри виконували одне і те саме заплановане завдання одночасно.

[Redis](https://redis.io/) є простим розв' язком: використовувати прапорці (ключі) для координування того, хто що робить.

## Простий лідер вибору

```csharp
public class DistributedBackgroundService : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<DistributedBackgroundService> _logger;
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LeaderKey = "background:newsletter:leader";

    public DistributedBackgroundService(
        IConnectionMultiplexer redis,
        ILogger<DistributedBackgroundService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var db = _redis.GetDatabase();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Try to become the leader (SET NX with expiry)
            var acquired = await db.StringSetAsync(
                LeaderKey,
                _instanceId,
                TimeSpan.FromMinutes(5),
                When.NotExists);

            if (acquired)
            {
                _logger.LogInformation("This instance is the leader, running task");

                try
                {
                    await DoScheduledWorkAsync(stoppingToken);
                }
                finally
                {
                    // Release leadership
                    await db.KeyDeleteAsync(LeaderKey);
                }
            }
            else
            {
                var leader = await db.StringGetAsync(LeaderKey);
                _logger.LogDebug("Another instance ({Leader}) is the leader", leader);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

## Розподілити блокування для критичних розділів

Для завдань, які не можуть одночасно виконуватися у випадках:

```csharp
public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
    var db = _redis.GetDatabase();
    var lockKey = "locks:critical-task";
    var lockValue = _instanceId;

    // Try to acquire lock
    if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
    {
        try
        {
            _logger.LogInformation("Lock acquired, processing...");
            await DoCriticalWorkAsync(cancellationToken);
        }
        finally
        {
            await db.LockReleaseAsync(lockKey, lockValue);
        }
    }
    else
    {
        _logger.LogDebug("Could not acquire lock, another instance is processing");
    }
}
```

## Коли використовувати координацію

- **Заплановані завдання** - Тільки один примірник повинен надіслати щоденний інформаційний бюлетень.
- **Обробка черги за порядком** - Забезпечити повідомлення буде оброблено за порядком
- **Налаштовані для ресурсів операції** - Захищати декілька екземплярів від непереборного зовнішнього API
- **Міграція бази даних** - При запуску має запускатися лише один екземпляр міграцій

Для складніших сценаріїв (багато- крокових завдань, надійного планування через перезапуски) скористайтеся Hangfire, який керує автоматичним блокуванням з сервером бази даних.

# Якщо не використовувати фонові служби

Перш ніж ми зануримося у більш складні інструменти, такі як Hangfire, давайте поговоримо про те, коли ви *не повинна* використовувати фонові служби у вашій основній веб- програмі.

## Підписи, які ви маєте розділити на окремі проекти

Служби тла, запущені у вашій веб- програмі, мають спільні ресурси з вашим каналом HTTP- запитів. Причиною цього можуть бути проблеми:

### 1. Насичення ресурсів

**Проблема:** Ваша фонова служба споживає значні процесори, пам' ять або з' єднання з базою даних.

```csharp
// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var video = await _queue.DequeueAsync();
            // This uses 100% of 4 CPU cores for 5 minutes
            await TranscodeVideoAsync(video);
        }
    }
}
```

**Коли веб-попити надсилаються під час перекодування, вони сповільнюються через зайнятість процесора.**

**Вирішення:** Перейти до окремої служби:

```bash
# Your solution structure
/YourApp.Web          # ASP.NET Core web app - no background services
/YourApp.Worker       # .NET Worker Service - handles background work
/YourApp.Shared       # Shared models, interfaces
```

### Різні вимоги до масштабування

**Проблема:** Ваша фонова робота потребує різної масштабації, ніж ваша веб-диспетчер.

- **Веб- зв' язувач:** Масштаб для HTTP трафіку (можна потребувати 10 екземплярів вдень, 2 вночі)
- **Програма для прив' язки тла:** Масштабувати глибину черги (можна потребувати 1 екземпляр, 20 під час обробки пакету)

Якщо вони в одному і тому ж процесі, їх неможливо розрахувати незалежно.

**Скрипт прикладу:**

```
09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers
```

Обмежити фонові служби у інтернет-забезпечення означає, що вам доведеться запустити 20 веб-аудиторів, щоб опрацювати інформаційний бюлетень, змарнувати ресурси.

### Незалежність від виховання

**Проблема:** Ви бажаєте розгортати зміни у мережі без перезапуску фонових служб (або навпаки).

```csharp
// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // This import takes 2 hours
        await ImportMillionsOfRecordsAsync(stoppingToken);
    }
}
```

Кожне впровадження перериває імпорт. Пересуньте його на окрему службу робітників, яку ви запускаєте незалежно.

### 4. Різні домени невдач

**Проблема:** Вада у вашій фоновій службі аварійно завершує роботу всієї веб- програми.

```csharp
// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string value = null;
        // Unhandled exception - takes down the whole app
        await ProcessAsync(value.Length);
    }
}
```

Якщо фонова робота виконується у окремому процесі, вона може аварійно завершити роботу і перезапустити роботу, не впливаючи на веб- запити.

## Як розбивати фонові служби

Коли ви вирішите розділити, ось рекомендована архітектура:

### Параметр 1:. NET Worker Service

Створити новий проект за допомогою шаблона Робочої служби:

```bash
dotnet new worker -n YourApp.Worker
```

Структура:

```
/YourApp.Worker
  /Services
    VideoTranscodingService.cs
    EmailSenderService.cs
  /Program.cs
  /appsettings.json
```

Програма. cs:

```csharp
var builder = Host.CreateApplicationBuilder(args);

// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();

// Share configuration with web app
builder.Services.Configure<VideoConfig>(
    builder.Configuration.GetSection("Video"));

// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

var host = builder.Build();
host.Run();
```

Роз' єднати окремо:

```bash
# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3

# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2
```

### Параметр 2: відокремити проект спільною чергою

Скористайтесь чергою повідомлень для вилучення мережі і робочих місць:

```mermaid
graph LR
    A[Web App] --> B[Message Queue]
    B --> C[Worker 1]
    B --> D[Worker 2]
    B --> E[Worker N]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
```

Робота з чергами веб- програм:

```csharp
// In your web controller
public class VideoController : ControllerBase
{
    private readonly IMessageQueue _queue;

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile video)
    {
        await _storage.SaveAsync(video);

        // Queue for processing - don't process in web app
        await _queue.PublishAsync(new VideoTranscodeJob
        {
            VideoId = video.Id,
            Priority = Priority.Normal
        });

        return Accepted(); // Return immediately
    }
}
```

Робоча програма споживає з черги:

```csharp
// In your worker service
public class VideoWorker : BackgroundService
{
    private readonly IMessageQueue _queue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
        {
            await TranscodeAsync(job);
        }
    }
}
```

**Параметри популярної черги повідомлень:**

- [**КроликMQComment**](https://www.rabbitmq.com/) - Найпоширеніші можливості
- [**Service Bus Azure**](https://azure.microsoft.com/en-us/products/service-bus/) - Якщо ви на Азурі
- [**AWS SQS**](https://aws.amazon.com/sqs/) - Если вы на AWS
- [**Потоки Редіс**](https://redis.io/docs/data-types/streams/) - Просте, добре для меншого масштабу

### Варіант 3.

Для складних систем, розділені відповідальністю:

```
/YourApp.Web              # HTTP requests only
/YourApp.EmailWorker      # Sends emails
/YourApp.VideoWorker      # Transcodes videos
/YourApp.ReportWorker     # Generates reports
/YourApp.Scheduler        # Runs scheduled jobs (Hangfire)
```

Кожен робітник може:

- Незалежний масштаб
- Впорядковувати незалежно
- Використовувати інші ресурси (користувача пошти потребує SMTP, для роботи з відеоінформацією потрібна програма GPU)
- Слідкуйте за ними по - іншому.

## Коли зберігати фонові служби у мережі

Незважаючи на вищесказане, у деяких сценаріях для служб фонової обробки все гаразд:

### Легкі періодичні завдання

```csharp
// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cache.WarmupAsync(); // Quick operation
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

### Слухачі подій

```csharp
// Fine to keep in web app
public class FileWatcherService : IHostedService
{
    // Reacts to events, doesn't consume significant resources
    private FileSystemWatcher _watcher;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher = new FileSystemWatcher("/config");
        _watcher.Changed += OnConfigChanged;
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }
}
```

### Черги, позначені на каналах (для некритичних завдань)

```csharp
// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
    // Sends emails in background, but each email takes < 1 second
    // If the app restarts, losing a few queued emails is acceptable
}
```

### Запуск Координації

```csharp
// Fine to keep in web app
public class WarmupService : IHostedService
{
    // Runs once at startup, then does nothing
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _database.WarmupConnectionPoolAsync();
        await _cache.LoadCriticalDataAsync();
    }
}
```

## Матриця визначення

♪Septain in Web App ♪ Move to Work' s Service ♪
|---------------|-----------------|------------------------|
Д_ д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. . . . . . . . . . . . . . . . . . д. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . д. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
♪0000 * } {\cH00FF00} {\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ >
Дівоча година/ д/ д/ д/ д або з високою частотою
Дівчата
Дівоча секунда@ info: whatsthis
 йде з'єднанням веб-трафіку* Робочої години ♪
}Інтерактивне розігрівання, налаштування перезавантаження} Процесія, великі імпорти}

## Приклад реального світу: Платформа блогу

На платформі блогу, чий код ми переглядаємо у частині 2:

**Зберігся у веб- програмі:**

- `MarkdownDirectoryWatcherService` - Незначна програма для спостереження за файлами
- `UmamiBackgroundSender` - Швидка аналітична подія
- `EmailSenderHostedService` - Малий об' єм, некритичний
- `MarkdownReAddPostsService` - Тільки для запуску, зайняте налаштування

**Потрібно перейти до служби праці, якщо збільшення масштабів:**

- `BrokenLinkCheckerBackgroundService` - Робить багато запитів HTTP
- `SemanticIndexingBackgroundService` - Викликає зовнішній API вбудовування

**Вже у окремій службі:**

- `Mostlylucid.SchedulerService` - Коробка поджога и отправка бюлетенок.

Це прагматичний підхід: почати простий (в процесі), розділяти, якщо у вас є потрібні докази.

# Поза основами: вогонь

Whilst `IHostedService` і `BackgroundService` є чудовим для служб, якими ви володієте і керуєте, іноді вам потрібні складніші планування. Ось де бібліотеки на зразок [Пожежа](https://www.hangfire.io/) Заходьте.

Погашення вогню:

- **Постійні черги завдань** - Завдання витримують перезапуск програми
- **Повторні завдання** - Планування у стилі Cron
- **Інтерфейс дошки** - Подивитися, що працює, що не вдалося, повторити завдання
- **Розподілити виконання** - Декілька серверів можуть працювати з тією ж чергою завдань
- **Автоматичні повторення** - Невдалі завдання автоматично повторюються експоненційним зворотним зв' язком

Ось простий приклад:

```csharp
// In Program.cs
builder.Services.AddHangfire(config => config
    .UsePostgreSqlStorage(connectionString)
    .UseRecommendedSerializerSettings());

builder.Services.AddHangfireServer();

var app = builder.Build();

// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
    .AddOrUpdate<NewsletterService>(
        "send-daily-newsletter",
        x => x.SendDailyNewsletter(),
        Cron.Daily(17)); // 5 PM every day
```

Ваша служба - це звичайний клас:

```csharp
public class NewsletterService
{
    private readonly IEmailService _emailService;
    private readonly ISubscriberRepository _subscribers;

    public NewsletterService(
        IEmailService emailService,
        ISubscriberRepository subscribers)
    {
        _emailService = emailService;
        _subscribers = subscribers;
    }

    public async Task SendDailyNewsletter()
    {
        var subscribers = await _subscribers.GetDailySubscribersAsync();

        foreach (var subscriber in subscribers)
        {
            await _emailService.SendNewsletterAsync(subscriber);
        }
    }
}
```

Керування почерговістю:

- Зміна часу виконання завдання
- Повторні спроби, якщо спроба зазнала невдачі
- Збереження журналу виконання
- Надання панелі приладів для спостереження за всім

```mermaid
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style E stroke:#059669,stroke-width:3px,color:#10b981
    style H stroke:#dc2626,stroke-width:3px,color:#ef4444
```

**Коли використовувати Hangfire:**

- Вам потрібні постійні черги завдань, які витримують перезапуск
- Вам потрібна панель приладів для спостереження і вмикання завдань вручну
- Вам слід розподіляти завдання на декількох серверах
- Вам потрібна вбудована логіка і обробка помилок
- Вам потрібне планування у стилі cron для регулярних завдань

**Коли зберігатися разом з IHosedService/ BackgroundService:**

- Вам потрібно доброго контролю над життєвим циклом
- Ваша служба має реагувати на події в реальному часі
- Ви хочете мінімізувати залежності
- Ви створюєте просте періодичне завдання, яке не потребує наполегливості.

# Інші параметри

Wilst Hangfire є популярною, існують інші бібліотеки, які варто розглянути:

[**Quartz. NET**](https://www.quartz-scheduler.net/):

- Поліпшене планування, ніж пожежа
- Підтримує вираз cron і планування засноване на календарях
- Може зберігатися декілька баз даних
- Складніший API, але потужніший

[**MassTransit**](https://masstransit.io/)/[**NServiceBus**](https://particular.net/nservicebus):

- Реалізація повнофункціональних автобусів повідомлень
- Краще для розподілених систем та мікрослужб
- Підтримка sagas (довгі робочі дані)
- Крива навчання уніфікатора

[**Функції азимуту**](https://azure.microsoft.com/en-us/products/functions/)/[**AWS Lamba**](https://aws.amazon.com/lambda/):

- Якщо ви знаходитесь у хмарі, подумайте про те, що без сервера.
- Платити за виконання, а не продовжувати виконання служби
- Автоматичне масштабування
- Якийсь прикрий для холоду.

# Зведення

У частині 1 ми розглянули основні підходи до фонових служб у ядрах ASP.NET:

1. **IHosedService** - Основа, максимальна гнучкість
2. **FroneService** - Зручний базовий клас для довгих циклів
3. **Координація запуску** - Виготовляючи послуги, чекають один на одного.
4. **Розподілити координацію** - Використовується Redis для сценаріїв мультиінстанції
5. **Пожежа** - Коли тобі потрібні постійні робочі місця і складний розклад.

Найважливіші уроки:

- **Завжди завершувати канали і скасовувати позначки у StopAync**
- **Визначає, чи слід блокувати або повертати зараз StartAync**
- **Опрацьовувати операційну редакційну систему граціозно**
- **Використовувати пункт " Задача ." Якщо буде позначено пункт завершення роботи, програма зберігатиме значення часу очікування завершення роботи**

Вхід [Частина 2](/blog/background-services-in-aspnetcore-part2)Ми дослідимо реалізацію реального світу з виробничої платформи блогу:

- Спостереження за файловою системою, які синхронізують файли з відміткою до бази даних
- Відсилання пошти відправникам з правилами повторення та розривами схем
- Аналітичні додавання до черги подій, які надсилаються пакетними запитами
- Індексатори семантичних пошуків, які обробляють вміст асинхронно
- Перевірки пошкоджених посилань, які періодично перевірятимуть зовнішні адреси URL

Ці приклади демонструють шаблони частини 1 у дії, зокрема шаблон координації стартапу і належне керування завершенням роботи.

# Подальше читання

- [Microsoft Docs: фонові завдання зі службами з вузлом](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)
- [Документація з Hangfire](https://docs.hangfire.io/)
- [Канали у C# Description of a condition. Do not translate key words (# V1S #, # V1 #,)](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels)
- [Документація з Quartz. NET](https://www.quartz-scheduler.net/)
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) - Клієнт Reredis для .NET