Что такое .NET Generic Host — основа для DI, конфигурации и логирования

· Обновлено: · · C#, .NET, Generic Host, Worker, Проектирование

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619677)

Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.

Го Комура (2026). Что такое .NET Generic Host — основа для DI, конфигурации и логирования. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619677 https://comcomponent.com/ru/blog/2026/03/14/000-dotnet-generic-host-what-is/

DOI (последняя версия)
10.5281/zenodo.21619677
DOI (эта версия)
10.5281/zenodo.21619678

Когда начинаешь писать консольное приложение или worker на .NET, поначалу хватает немного кода в Main. Но стоит приложению чуть подрасти, как обычно появляется вот это.

  • Хочется читать appsettings.json
  • Хочется переопределять значения через переменные окружения
  • Хочется писать логи через ILogger
  • Не хочется, чтобы создание сервисов превращалось в сплошные new
  • Хочется крутить цикл в фоне
  • Хочется корректно завершаться по Ctrl+C или при остановке службы

Вот тут появляется Generic Host. Правда, само имя легко смешать с соседними понятиями.

  • Чем Host.CreateApplicationBuilder отличается от Host.CreateDefaultBuilder?
  • Это то же самое, что DI-контейнер, или нет?
  • Как это связано с BackgroundService?
  • Это отдельная вещь от WebApplicationBuilder в ASP.NET Core?
  • Стоит ли использовать это в консольном приложении?

Когда всё это смешивается, Generic Host кажется то «чем-то для веб-приложений», то «тем, во что нужно оборачивать вообще всё». Оба взгляда слишком грубые.

В этой статье, опираясь в основном на текущую практику .NET 6 и новее, сначала разберём четыре вещи.

  • Что такое Generic Host на самом деле
  • За что он берёт ответственность
  • Как связаны Host.CreateApplicationBuilder, Host.CreateDefaultBuilder и WebApplication.CreateBuilder
  • С чего спокойнее начинать

Содержание

  1. Сначала вывод (коротко)
    • 1.1. Сначала договоримся о терминах
  2. Сначала — сводные таблицы
    • 2.1. Что держит Generic Host
    • 2.2. Различия между builder
    • 2.3. Почему точек входа несколько
  3. Общая картина Generic Host (схема)
  4. Что даёт Generic Host
    • 4.1. Логику запуска можно собрать в одном месте
    • 4.2. DI, конфигурация и логирование связаны с самого начала
    • 4.3. Проще управлять корректным завершением и постоянной работой
  5. Минимальная конфигурация
    • 5.1. Минимальный пример для консольного приложения
    • 5.2. appsettings.json
    • 5.3. Как добавить BackgroundService
  6. Типичные схемы
    • 6.1. Короткоживущая консольная утилита
    • 6.2. worker / фоновые службы
    • 6.3. Он же стоит и под ASP.NET Core
  7. Где это уместно
  8. Где это не стоит / лишнее
  9. Типичные ловушки
  10. Итог
  11. Источники

Карта знаний этой статьи

.NET Generic Host собирает на одной основе внедрение зависимостей, конфигурацию, логирование, фоновую работу через IHostedService и BackgroundService, а также управление временем жизни в ответ на Ctrl+C и SIGTERM. Для нового не-Web приложения естественная точка сборки — Host.CreateApplicationBuilder. Для существующего кода остаётся путь через Host.CreateDefaultBuilder, а WebApplication.CreateBuilder в ASP.NET Core — это та же идея, расширенная под Web вместо отдельного Web Host, который существовал раньше. У BackgroundService нет области (scope) по умолчанию, поэтому для scoped-сервисов нужно явно создавать scope через IServiceScopeFactory — это типичная ловушка.

Карта знаний .NET Generic HostСхема, которая показывает, как Generic Host собирает DI, конфигурацию, логирование, IHostedService и BackgroundService и управление временем жизни, и как он связан с Host.CreateApplicationBuilder и WebApplication.CreateBuilderиспользуетиспользуетиспользуетиспользуетиспользуетреализуетнастраиваетсянастраиваетсяпреемникиспользуеттребуетиспользуетиспользуеттребуеттребуетиспользуетGeneric Hostвнедрение зависимостей (DI)система конфигурации .NET (IConfiguration)Microsoft.Extensions.Logging (ILogger)IHostedServiceуправление временем жизни Host (IHostApplicationLifetime)BackgroundServiceHost.CreateApplicationBuilderHost.CreateDefaultBuilderWebApplication.CreateBuilderWeb Host (IWebHostBuilder)IServiceScopeFactoryшаблон Optionsслужба Windows.NET (начиная с Core)HostApplicationBuilder

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 16, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

1. Сначала вывод (коротко)

  • Generic Host — основа, которая собирает запуск и время жизни .NET-приложения в одном месте.
  • В неё входят DI, конфигурация, логирование, IHostedService / BackgroundService и обработка остановки приложения.
  • Для нового не-Web приложения естественно начинать с Host.CreateApplicationBuilder(args).
  • WebApplicationBuilder в ASP.NET Core — не отдельный мир, а та же идея host, расширенная под Web.
  • То есть Generic Host — это не разговор только про DI-контейнер, а механизм, который собирает точку сборки приложения и управление его жизненным циклом.

Проще говоря, как только приложение чуть выходит за сценарий «прочитать аргументы, один раз напечатать и выйти», Generic Host начинает давать заметный эффект. И наоборот, тащить его в каждый маленький инструмент, который до этого уровня ещё не дорос, не обязательно.

Где Generic Host начинает давать эффектВ маленький инструмент, который один раз печатает и выходит, Generic Host каждый раз тащить не нужно; эффект появляется, как только приложение чуть выходит за этот уровень.Не тащить каждый разДаёт заметный эффектИнструмент, который один раз печатает и выходитGeneric HostПриложение чуть выше этого уровня

Рис. 1: Generic Host начинает работать, как только приложение чуть выходит за «один раз напечатать и выйти».

1.1. Сначала договоримся о терминах

Дальше в статье метафор будет много, поэтому сначала зафиксируем точные слова.

Термин Точнее Метафора в этой статье
DI (внедрение зависимостей / Dependency Injection) Класс не создаёт нужных ему объектов через new, а получает их снаружи. Место, где эти объекты регистрируют заранее, — DI-контейнер (IServiceProvider); Generic Host держит его с самого начала проводка
Builder (HostApplicationBuilder) Объект, которым собирают host. У него есть свойства вроде Services, Configuration, Logging, и сюда идут регистрации. Пока не вызван Build(), приложение не работает стол сборки
Host (IHost) Уже собранное приложение, результат Build(). Оно держит DI-контейнер, конфигурацию, логи и hosted service и ведёт от Run() / RunAsync() до остановки основа
Hosted service (IHostedService / BackgroundService) Контейнер для работы, которая стартует и останавливается вместе с host. Когда host запускается, вызывается StartAsync; у BackgroundService идёт ExecuteAsync фоновая работа
Lifetime Управление от старта приложения до остановки. Сигналы вроде Ctrl+C, SIGTERM и остановки службы сводят к одному способу завершаться жизненный цикл

Чаще всего путают Builder и Host. Builder собирает, Host — уже собранный результат, а Build() — граница между ними. Если это держать в голове, метафоры вроде «основа», «коробка», «точка входа» дальше читаются без путаницы, на что они указывают.

Граница между Builder и HostBuilder — сторона сборки, IHost — уже собранный результат, а вызов Build — граница между ними.Build()Builder (собирает)IHost (уже собранный результат)Run / RunAsync ведут от старта до остановки

Рис. 2: Builder собирает, IHost — уже собранный результат, граница между ними — Build().

Если DI ещё непривычен, достаточно такой картины. Вместо цепочки new вы на старте регистрируете: «когда понадобится этот тип, передайте эту реализацию», а получатель просто принимает её аргументом конструктора. Место регистрации — builder.Services.

2. Сначала — сводные таблицы

2.1. Что держит Generic Host

Сначала разложить содержимое этой коробки — так будет заметно проще.

Элемент За что берётся Generic Host В чём польза
DI Собирает сервисы из IServiceCollection Проще сократить цепочки new
Configuration Собирает appsettings.json, переменные окружения, аргументы командной строки и т. п. Проще обрабатывать различия между средами
Logging Строит основу для ILogger<T> Проще потом сменить назначение логов
Hosted service Ведёт запуск и остановку IHostedService / BackgroundService Проще отделить фоновую работу от тела приложения
Lifetime Ведёт старт и остановку через IHostApplicationLifetime, IHostEnvironment и соседние API Проще унифицировать завершение по Ctrl+C, SIGTERM или остановке службы

Важно: Generic Host — это не «удобная обёртка над одним DI». Надёжнее смотреть на него как на коробку, которая сразу собирает проводку вокруг точки входа в приложение.

2.2. Различия между builder

И здесь быстрее один раз посмотреть таблицу.

Точка входа Основное назначение Стиль записи Первый выбор
Host.CreateApplicationBuilder(args) Новые не-Web приложения вроде консоли или worker Пишем напрямую в builder.Services / builder.Configuration / builder.Logging Для нового проекта — это
Host.CreateDefaultBuilder(args) Существующий код или конфигурация на старых методах-расширениях Цепочка вызовов вроде ConfigureServices Если есть существующие наработки — это
WebApplication.CreateBuilder(args) Веб-приложения / API на ASP.NET Core Точка входа Generic Host плюс веб-специфика Для Web — это

CreateApplicationBuilder и CreateDefaultBuilder — это не история про то, что один из них новая функция, а другой — что-то иное.

У обоих один и тот же базовый функционал и одно и то же поведение по умолчанию. Разница в основном в стиле записи.

Для нового не-Web приложения сейчас естественно начинать с Host.CreateApplicationBuilder(args). WebApplication.CreateBuilder(args) удобно держать в голове как ту же линию, расширенную под Web.

Связь трёх точек входаCreateApplicationBuilder и CreateDefaultBuilder имеют один базовый функционал и одно поведение по умолчанию и отличаются стилем записи; WebApplication.CreateBuilder — та же линия, расширенная под Web.Точка входа, расширенная под WebCreateApplicationBuilderОдин базовый функционал и поведение по умолчаниюCreateDefaultBuilderСтиль прямой записиСтиль цепочкиWebApplication.CreateBuilder

Рис. 3: Два builder стоят на одном базовом функционале и отличаются стилем записи; для Web есть расширенная точка входа.

2.3. Почему точек входа несколько

Точек входа несколько, потому что Web-сторона и не-Web сторона сначала росли отдельно, а потом сошлись.

  • Изначально в ASP.NET Core был отдельный Web Host (IWebHostBuilder) только для Web, а Generic Host (IHostBuilder) для не-Web приложений готовили отдельно.
  • Потом ASP.NET Core сдвинули к Generic Host, и Web, и не-Web оказались на одной идее host.
  • Дальше к стилю цепочки колбэков (ConfigureServices и т. п.) добавилась точка входа с прямой записью в свойства (builder.Services и т. п.). Сюда относятся Host.CreateApplicationBuilder и WebApplication.CreateBuilder.

В текущей официальной документации линия Host.CreateApplicationBuilder (IHostApplicationBuilder) описана как ориентир для новых проектов и значение по умолчанию в нынешних шаблонах, а линия Host.CreateDefaultBuilder (IHostBuilder) — как прежний способ, оставленный ради совместимости с существующим кодом. Там же прямо сказано, что у обоих один базовый функционал и одно поведение по умолчанию.

Если прийти из кода времён .NET Framework или .NET Core 3.1, вопрос «почему способов записи два» звучит естественно, но это не два разных новых мира рядом, а больше точек входа, появившихся по ходу схождения. Если нет нужды подстраиваться под существующие наработки, для нового проекта достаточно Host.CreateApplicationBuilder.

Откуда взялось несколько точек входаОтдельный Web Host для Web и Generic Host для не-Web сначала существовали порознь, затем ASP.NET Core сошёлся с Generic Host, и поверх этого появилась точка входа с прямой записью в свойства.Web Host (IWebHostBuilder)ASP.NET Core сошёлся с Generic HostGeneric Host (IHostBuilder)Добавилась точка входа с прямой записью в свойстваCreateApplicationBuilder и WebApplication.CreateBuilder

Рис. 4: Отдельно выросшие Web Host и Generic Host сошлись, и по ходу этого появился стиль прямой записи.

3. Общая картина Generic Host (схема)

Если грубо нарисовать общую картину, получится так.

Общая схема Generic HostВ builder регистрируют конфигурацию, сервисы и логи, Build() даёт IHost, Run/RunAsync запускают его, и lifetime связывается с стартом и остановкой hosted service.args / переменные окружения / appsettings.jsonHost.CreateApplicationBuilder(args)builder.Configurationbuilder.Servicesbuilder.LoggingIHostedService / BackgroundServicebuilder.Build()IHostRun / RunAsyncСтарт, остановка, Ctrl+C, SIGTERM

Рис. 5: В builder регистрируют конфигурацию, сервисы и логи, Build() даёт IHost, Run/RunAsync запускают его, и lifetime доходит до старта и остановки hosted service.

Обычно builder создают в Program.cs, добавляют сервисы в builder.Services, при необходимости настраивают builder.Configuration и builder.Logging, затем вызывают Build(), получают IHost и запускают его через Run() / RunAsync().

Скромный, но важный момент: уже в момент Host.CreateApplicationBuilder(args) подключено немало. По умолчанию, например, входит следующее.

  • Корень содержимого — текущий каталог
  • Конфигурация хоста — переменные окружения с префиксом DOTNET_ и аргументы командной строки
  • Конфигурация приложения — appsettings.json, appsettings.{Environment}.json, user secrets в Development, переменные окружения и аргументы командной строки
  • Логи — Console / Debug / EventSource / EventLog (только Windows)
  • В среде Development — проверка scope и проверка зависимостей

То есть речь не о том, что вы собираете проводку с нуля: с самого начала уже лежит основа, «которой для обычного использования вполне хватает».

Что уже подключено с самого началаУже в момент Host.CreateApplicationBuilder подключены конфигурация хоста, конфигурация приложения и логи по умолчанию, так что основа для обычного использования лежит сразу.CreateApplicationBuilder(args)Конфигурация хоста (DOTNET_ и аргументы)Конфигурация приложения (appsettings.json и прочее)Логи по умолчанию (Console и прочее)Основа, которой для обычного использования хватает

Рис. 6: Уже при создании builder подключены конфигурация и логи по умолчанию — проводку с нуля собирать не нужно.

4. Что даёт Generic Host

4.1. Логику запуска можно собрать в одном месте

Самый скромный и при этом самый весомый эффект Generic Host — точка входа приложения перестаёт расползаться.

Когда приложение чуть подрастает, вокруг Main обычно копится вот это.

  • Чтение файлов конфигурации
  • Подмена значений по среде
  • Инициализация логгера
  • Сборка HttpClient, репозиториев и сервисов
  • Запуск фоновой работы
  • Уборка при сигнале завершения

Если соединять всё это вручную без host, поначалу легко, но со временем точка входа тяжелеет и начинает «липнуть».

С Generic Host Program.cs чётко становится «местом, где собирают зависимости». Уже одно это заметно облегчает код-ревью.

Логика запуска собирается в одном местеЕсли всё соединять вручную без host, точка входа со временем тяжелеет; с Generic Host Program.cs становится местом, где собирают зависимости.Соединять вручную без hostТочка входа со временем тяжелеетИспользовать Generic HostProgram.cs становится местом сборкиКод-ревью проще

Рис. 7: При ручной проводке точка входа тяжелеет; если сдвинуть работу на host, Program.cs становится местом сборки.

4.2. DI, конфигурация и логирование связаны с самого начала

С Generic Host DI, конфигурация и логирование с самого начала стоят на одной основе.

На стороне класса, например, можно как само собой разумеющееся получать вот это.

  • ILogger<T>
  • IConfiguration
  • IHostEnvironment
  • IOptions<T>

Здесь важно, что способ читать конфигурацию и способ создавать сервисы реже расходятся в разные стили.

Если настроек одна-две, достаточно читать IConfiguration["Section:Key"] напрямую — это тоже работает. Но когда в реальном проекте настроек становится больше, спокойнее собирать их по секциям в классы через IOptions<T>. Ориентир — около пяти строковых ключей и больше. На таком масштабе опечатка доходит до сбоя только во время выполнения, и становится труднее понять, какой ключ где читается.

Точно так же с логами: вместо того чтобы вручную собирать ILoggerFactory в разных местах, внедрение ILogger<T> в нужные классы делает картину прозрачнее.

Удобство Generic Host в том, что он не разносит это по отдельным историям, а ведёт всё вместе, как основу всего приложения.

Как растить чтение конфигурацииЕсли настроек одна-две, достаточно читать IConfiguration напрямую; когда строковых ключей больше пяти, безопаснее собирать их по секциям в классы через IOptions.Настроек 1–2Читать IConfiguration напрямуюСтроковых ключей больше 5Собрать в классы через IOptionsОпечатку видно только во время выполнения

Рис. 8: Пока настроек мало, прямое чтение достаточно; когда ключей больше пяти, безопаснее собирать их через IOptions.

4.3. Проще управлять корректным завершением и постоянной работой

Generic Host заботится не только о том, «как запуститься», но и о том, «как остановиться».

Когда host запускается, вызывается StartAsync каждого зарегистрированного IHostedService. В worker-службе выполняется ExecuteAsync hosted service, включая BackgroundService.

«Корректное завершение» здесь значит не оборвать работу сразу, а пройти порядок:

  • передать сигнал остановки
  • выйти из циклов и ожиданий
  • освободить соединения и ресурсы

Для долго работающих приложений это очень важно. События вроде Ctrl+C, SIGTERM или остановки службы позволяют унифицировать, как останавливается всё приложение.

Кроме того, когда само приложение хочет запросить завершение, можно использовать IHostApplicationLifetime.StopApplication(). Сигнал «работа уже закончена, завершись аккуратно» подаётся в контексте host.

Порядок корректного завершенияПо Ctrl+C, SIGTERM или остановке службы сначала передают сигнал остановки, затем выходят из циклов и ожиданий, затем освобождают соединения и ресурсы.StopApplication()Ctrl+C / SIGTERM / остановка службыПередать сигнал остановкиВыйти из циклов и ожиданийОсвободить соединения и ресурсыЗапрос завершения со стороны приложения

Рис. 9: Корректное завершение идёт как сигнал остановки → выход из циклов → уборка; со стороны приложения в тот же поток можно подать StopApplication().

5. Минимальная конфигурация

5.1. Минимальный пример для консольного приложения

Первое, что важно: использование Generic Host вовсе не значит, что обязательно нужно создавать BackgroundService.

Даже для консольной утилиты, которая выполняется один раз, Generic Host вполне уместен, если нужны DI, конфигурация и логирование.

Чтобы добавить его в обычный консольный проект позже, сначала подключите пакет Microsoft.Extensions.Hosting.

dotnet add package Microsoft.Extensions.Hosting

Минимальный пример Program.cs выглядит, например, так.

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<JobRunner>();

using IHost host = builder.Build();

try
{
    JobRunner runner = host.Services.GetRequiredService<JobRunner>();
    await runner.RunAsync();
    return 0;
}
catch (Exception ex)
{
    ILogger logger = host.Services
        .GetRequiredService<ILoggerFactory>()
        .CreateLogger("Program");

    logger.LogError(ex, "Unhandled exception occurred during job execution.");
    return 1;
}

internal sealed class JobRunner(
    ILogger<JobRunner> logger,
    IConfiguration configuration,
    IHostEnvironment hostEnvironment)
{
    public Task RunAsync()
    {
        string message = configuration["Sample:Message"] ?? "(no message)";

        logger.LogInformation("Environment: {EnvironmentName}", hostEnvironment.EnvironmentName);
        logger.LogInformation("Message: {Message}", message);

        return Task.CompletedTask;
    }
}

После dotnet run в консоли будет примерно так (значение Message приходит из appsettings.json, который положим в следующем разделе 5.2).

info: JobRunner[0]
      Environment: Production
info: JobRunner[0]
      Message: hello from Generic Host

Справа от info: — категория лога (здесь ILogger<JobRunner>, то есть имя типа) и идентификатор события. Консольный логгер по умолчанию печатает в форме «первая строка — категория, вторая — текст». Environment равен Production, потому что это значение по умолчанию, когда не заданы ни DOTNET_ENVIRONMENT, ни ASPNETCORE_ENVIRONMENT. Для разработки запускайте с DOTNET_ENVIRONMENT=Development.

Если приложение не остаётся работать надолго, доходить до RunAsync() не обязательно. Можно вызвать Build(), получить нужные сервисы, сделать работу и просто завершиться. Даже в этом случае преимущества Generic Host используются вполне.

Это на удивление важный момент. Не нужно каждый раз тащить шаблон Worker даже в короткоживущую задачу.

Как использовать Generic Host в короткоживущей задачеЕсли приложение не остаётся работать надолго, до RunAsync доходить не нужно: достаточно Build(), получить сервисы, сделать работу и завершиться — преимущества Generic Host остаются.Вызвать Build()Получить нужные сервисыСделать работуПросто завершитьсяДоходить до RunAsync() не обязательно

Рис. 10: Для короткоживущей задачи достаточно Build(), получить сервисы, выполнить работу и выйти — преимущества host остаются.

5.2. appsettings.json

Для примера выше файла конфигурации в такой минимальной форме вполне достаточно.

{
  "Sample": {
    "Message": "hello from Generic Host"
  }
}

Есть одна классическая ловушка. В консольном проекте сам факт добавления appsettings.json не копирует файл в выходной каталог. В свойствах проекта поставьте «Копировать в выходной каталог» в «Копировать, если новее» или добавьте в csproj следующее.

<ItemGroup>
  <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Полезно сразу знать, как это выглядит, если забыть. Generic Host читает appsettings.json как необязательный файл, поэтому без него исключения не будет. Просто не будет значения. В минимальном примере выше напечатается Message: (no message). Если «ошибки нет, а настройки не действуют», сначала проверьте, есть ли appsettings.json в выходном каталоге.

Что видно, когда нет appsettings.jsonДаже если appsettings.json не скопирован в выходной каталог, его читают как необязательный файл, поэтому исключения нет — просто нет значения.Файла нет в выходном каталогеЧитают как необязательный файлИсключения нетПросто нет значенияСначала проверить выходной каталог

Рис. 11: Без appsettings.json исключения нет, есть только «значение не получено», поэтому сначала смотрите выходной каталог.

В этом примере значение читается напрямую как configuration["Sample:Message"]. Если нужно посмотреть одно-два значения, этого достаточно.

Но когда в реальном проекте настроек становится больше, лучше сдвигаться к такой форме — так проще не разбрасывать строковые ключи по коду:

  • разделить по классам на каждую секцию
  • внедрять через IOptions<T>
  • проверять при запуске

Кроме того, по умолчанию Generic Host подключает не только appsettings.json, но и appsettings.{Environment}.json, переменные окружения и аргументы командной строки, поэтому сценарии «подменить только при разработке» и «в продакшене переопределить через переменные окружения» получаются естественно.

5.3. Как добавить BackgroundService

Для долго выполняющейся работы BackgroundService — довольно естественный выбор.

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddScoped<PollingJob>();
builder.Services.AddHostedService<PollingWorker>();

using IHost host = builder.Build();
await host.RunAsync();

internal sealed class PollingWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<PollingWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            using IServiceScope scope = scopeFactory.CreateScope();
            PollingJob job = scope.ServiceProvider.GetRequiredService<PollingJob>();

            await job.RunAsync(stoppingToken);
            logger.LogInformation("Polling completed.");
        }
    }
}

internal sealed class PollingJob(ILogger<PollingJob> logger)
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Do work here.");
        return Task.CompletedTask;
    }
}

В этом примере стоит смотреть на два момента.

  1. Тело BackgroundService — это ExecuteAsync
  2. Если нужны scoped-зависимости, создавайте scope через IServiceScopeFactory

У самого BackgroundService нет области (scope) по умолчанию. Если, например, нужен scoped-сервис вроде DbContext, безопасный вариант — получать сервис задачи внутри scope, как выше.

Как использовать scoped в BackgroundServiceУ самого BackgroundService нет scope по умолчанию, поэтому безопасный путь — внедрить IServiceScopeFactory, создать scope внутри ExecuteAsync и уже там получить сервисы задачи.BackgroundService (нет scope по умолчанию)Внедрить IServiceScopeFactoryСоздать scope внутри ExecuteAsyncПолучить задачу внутри scope

Рис. 12: У BackgroundService нет scope по умолчанию, поэтому scope создают через IServiceScopeFactory и уже там получают задачу.

Выбор самого инструмента периодического выполнения — отдельная тема, но если писать в стиле async, PeriodicTimer — довольно спокойный вариант. Это перекликается со связанной статьёй про таймеры.

6. Типичные схемы

6.1. Короткоживущая консольная утилита

Generic Host вполне уместен и для приложений, которые делают работу один раз и выходят, — пакетных заданий, инструментов преобразования, команд обслуживания.

Он хорошо подходит в таких ситуациях.

  • Нужно читать файлы конфигурации
  • Нужно писать логи
  • Нужно внедрять HttpClient или репозитории
  • Нужно вернуть код завершения

Если в таком приложении сразу тащить BackgroundService и RunAsync(), получается тяжеловато и управление временем жизни host используется избыточно.

Для короткоживущей задачи достаточно получить JobRunner и выполнить его, как в предыдущем минимальном примере.

Как выбирать в короткоживущей консольной утилитеДля приложения, которое делает работу один раз и выходит, BackgroundService и RunAsync обычно избыточны: достаточно получить сервис и выполнить его.ДостаточноЛегко становится лишнимПриложение, которое делает работу один раз и выходитПолучить JobRunner и выполнитьBackgroundService и RunAsync()

Рис. 13: Тащить BackgroundService в одноразовое приложение избыточно; достаточно получить сервис и выполнить его.

6.2. worker / фоновые службы

Для постоянно работающего worker, опроса, потребления очереди, мониторинга и периодического выполнения сочетание Generic Host и BackgroundService довольно естественно.

Особенно ценно вот это.

  • Поток запуска и остановки унифицирован на стороне host
  • Логи, конфигурация и DI доступны с самого начала
  • Легко пробрасывать отмену по Ctrl+C или сигналу остановки
  • Легко отделить тело фоновой работы от Program.cs

Кроме того, это легко связать с контекстом Windows Service или контейнеров. Если приложение планируется растить как постоянно работающее, Generic Host — вполне естественная основа.

При превращении в Windows Service меньше сюрпризов, если искать файлы не от текущего каталога, а от IHostEnvironment.ContentRootPath. «Базовый путь приложения» задаётся именно в контексте host.

Основа постоянно работающего workerДля постоянно работающего worker и периодического выполнения естественно сочетание Generic Host и BackgroundService, и его легко связать с Windows Service и контейнерами.Постоянно работающий worker / периодическое выполнениеGeneric Host и BackgroundServiceWindows ServiceПостоянная работа в контейнереИскать файлы от ContentRootPath

Рис. 14: Для постоянной работы естественно сочетание host и BackgroundService, и его проще растить в Windows Service или контейнер.

6.3. Он же стоит и под ASP.NET Core

В веб-приложениях / API используют WebApplication.CreateBuilder(args), поэтому на первый взгляд может казаться, что это отдельный от Generic Host мир.

Но по ощущению они довольно тесно связаны.

  • builder.Services
  • builder.Configuration
  • builder.Logging

Именно поэтому стиль записи похож.

В ASP.NET Core запуск HTTP-сервера тоже входит во время жизни host. То есть понимание Generic Host помогает и в том смысле, что при чтении веб-версии Program.cs становится понятнее, «почему здесь вообще трогают DI, конфигурацию и логи».

Web тоже стоит на том же hostПриложение ASP.NET Core через WebApplication.CreateBuilder стоит на той же идее host, и запуск HTTP-сервера тоже входит во время жизни host.Приложение ASP.NET CoreWebApplication.CreateBuilderТа же идея hostЗапуск HTTP-сервера тоже внутри lifetime

Рис. 15: Builder на стороне Web стоит на той же идее host, и запуск HTTP-сервера тоже входит в lifetime.

7. Где это уместно

Перечислим ситуации, где Generic Host обычно ложится особенно хорошо.

  • Консольные приложения, которым нужны конфигурация, логи и DI
  • worker вроде потребителя очереди, поллера, watchdog, планировщика
  • Долго работающие приложения, которым нужна уборка по Ctrl+C или SIGTERM
  • Приложения, которые в будущем могут вырасти в Windows Service или постоянную работу в контейнере
  • Приложения, которые хочется выстроить в том же стиле расширений, что и ASP.NET Core

Их объединяет одно: не хочется относиться небрежно к точке входа и управлению временем жизни.

Одной этой фразы мало, чтобы провести границу, поэтому ниже ориентир. Если из следующего списка совпадают два пункта и больше, с самого начала сесть на Generic Host обычно потом проще.

Ориентир, брать ли Generic HostЕсли совпадают два пункта ориентира и больше, Generic Host берут с самого начала; если не совпадает ни один, его можно не брать.Два и большеНи одногоСчитать, сколько пунктов ориентира совпадаетС самого начала сесть на Generic HostGeneric Host можно считать ненужным

Рис. 16: Если совпадают два пункта и больше — садитесь сразу; если ни одного — тащить не нужно.

Ориентир Конкретная линия
Число настроек Настроек, которые меняются по среде, три и больше (куда подключаться, пороги, куда писать и т. п.)
Логи Нужно оставлять в файле или Event Log. Не «написал в стандартный вывод и хватит»
Форма выполнения Работает постоянно. Или запускается не реже раза в день с заданным интервалом
Зависимости Через конструктор хочется получать трёх и больше собеседников. Есть то, что хочется подменять в тестах
Время жизни По Ctrl+C или остановке службы нужна уборка на середине работы
Будущее Есть шанс запускать как Windows Service или в контейнере

8. Где это не стоит / лишнее

И наоборот, есть ситуации, где Generic Host не обязательно делать главным действующим лицом с самого начала.

  • Маленький инструмент, который один раз читает аргументы, один раз печатает результат и выходит
  • Наспех написанный проверочный код на несколько десятков минут
  • Библиотечный проект
  • Случаи, когда нужно прочитать одну настройку, а DI, логи и управление временем жизни не нужны

Здесь проще писать прямо в Main, без host: и читать меньше, и файлов меньше. Как ориентир: если из таблицы главы 7 не совпадает ни один пункт, Generic Host можно считать ненужным.

Важно: то, что Generic Host сильный инструмент, не делает его обязательным для каждого исполняемого файла.

9. Типичные ловушки

Напоследок — куда легко наступить при первом знакомстве с Generic Host.

  • Смотреть на Generic Host только как на DI-контейнер
    • На деле это основа, в которую входят запуск, остановка, конфигурация, логи и hosted service.
  • По инерции начинать новое приложение с Host.CreateDefaultBuilder
    • Если нет нужды подстраиваться под существующий код, для начала естественнее Host.CreateApplicationBuilder.
  • Внедрять scoped-сервис напрямую в BackgroundService
    • У hosted service нет области (scope) по умолчанию. Безопаснее создавать scope через IServiceScopeFactory.
  • Worker, который должен завершиться после одного запуска, не сообщает об этом host
    • Если делать «run once» через шаблон Worker и не вызвать IHostApplicationLifetime.StopApplication() по окончании работы, host продолжит работать.
  • Хотеть корректного завершения, но обрывать работу через Environment.Exit
    • Если вы используете host, для аккуратной остановки правильнее StopApplication().
  • В Windows Service полагаться на текущий каталог (current directory)
    • Поиск файлов стабильнее строить от IHostEnvironment.ContentRootPath.
  • С самого начала оборачивать короткоживущий CLI в BackgroundService
    • Для одноразовой работы достаточно получить обычный класс-сервис и выполнить его.
  • Небрежно ставить callback-таймер для периодического выполнения в BackgroundService
    • Если писать в стиле async, PeriodicTimer обычно читается лучше и реже приводит к беспорядку.

В случае с Generic Host достаточно с самого начала разделить, «это короткоживущая задача или постоянно работающая», чтобы заметно реже сомневаться в выборе.

Вопрос, который стоит задать первымСначала разделяют короткоживущую задачу и постоянно работающую: для короткоживущей достаточно получить обычный класс-сервис и выполнить его, для постоянной — BackgroundService и управление lifetime у host.КороткоживущаяПостоянно работающаяКороткоживущая задача или постоянно работающая?Достаточно получить сервис и выполнитьBackgroundService и управление lifetime

Рис. 17: Если сначала разделить короткоживущее и постоянное, проще понять, сколько инструментов host реально нужно.

10. Итог

Одной фразой Generic Host — это основа, которая собирает точку входа .NET-приложения и управление его временем жизни.

Ещё раз пройдёмся по ключевым пунктам.

  1. Generic Host включает не только DI, но и конфигурацию, логирование, обработку остановки и hosted service
  2. Для нового не-Web приложения естественно начинать с Host.CreateApplicationBuilder(args)
  3. Для короткоживущей задачи можно обойтись без BackgroundService: достаточно build и выполнения
  4. Для постоянной работы BackgroundService вместе с управлением lifetime у host дают заметный эффект
  5. У BackgroundService нет области (scope) по умолчанию, поэтому для scoped-сервисов scope создают явно
  6. WebApplicationBuilder в ASP.NET Core тоже стоит на той же линии мышления

Generic Host — не инструмент для тяжёлого ритуала. Как только конфигурация, логи, зависимости, запуск и завершение хоть немного разрастаются, это инструмент, который не даёт им расползтись по углам, а собирает их у входа.

И наоборот, для маленьких инструментов, которым это пока не нужно, тащить его не обязательно. Как только вы научитесь это различать, Generic Host перестаёт быть тем, что «добавляют на всякий случай», и становится практической основой с понятным местом применения.

11. Источники

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Как создать и эксплуатировать службу Windows — от выбора между Планировщиком заданий и службой до превращения BackgroundService в службу

Стоит ли держать постоянно работающую обработку как службу Windows или хватит Планировщика заданий. Практический разбор: таблица выбора, ...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое Generic Host?
Это основа, которая собирает запуск и время жизни .NET-приложения в одном месте. В неё входят DI, конфигурация (Configuration), логирование, IHostedService / BackgroundService и обработка остановки приложения. Это не просто обёртка над DI-контейнером — надёжнее смотреть на неё как на механизм, который собирает точку сборки приложения и управление его жизненным циклом. Эффект появляется, как только конфигурация, логи, зависимости, запуск и завершение хоть немного разрастаются.
Что выбрать — Host.CreateApplicationBuilder или Host.CreateDefaultBuilder?
Для нового не-Web приложения естественно начинать с Host.CreateApplicationBuilder(args). У обоих один и тот же базовый функционал и одно и то же поведение по умолчанию: это не «новая функция» против «чего-то другого». Разница в основном в стиле записи. CreateApplicationBuilder пишет напрямую в builder.Services и соседние свойства, CreateDefaultBuilder выстраивает цепочку вроде ConfigureServices. Если нужно подстроиться под существующий код или конфигурацию на старых методах-расширениях, берите CreateDefaultBuilder.
Есть ли смысл использовать Generic Host в консольном приложении?
Если нужны DI, конфигурация и логирование, Generic Host вполне уместен даже для консольной утилиты, которая выполняется один раз. Создавать BackgroundService не обязательно: можно вызвать Build(), получить нужные сервисы и просто завершиться после работы — преимущества Generic Host остаются. И наоборот, для маленького инструмента, который один раз читает аргументы, один раз печатает результат и выходит, или для наспех написанного проверочного кода это лишнее, так что тащить его каждый раз не нужно.
Как использовать scoped-сервисы в BackgroundService?
У BackgroundService нет области (scope) по умолчанию, поэтому внедрять scoped-сервис напрямую через конструктор небезопасно. Безопасный способ — внедрить IServiceScopeFactory, явно создать scope внутри ExecuteAsync и уже там получать сервисы для задачи. Особенно это важно, если нужен scoped-сервис вроде DbContext.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог