Создание службы Windows с помощью BackgroundService

Разработчики на платформе .NET Framework, вероятно, знакомы с приложениями служб Windows. До .NET Core и .NET 5+ разработчики, использующие платформу .NET Framework, могли создавать службы Windows для выполнения фоновых задач или выполнения долгосрочных процессов. Эта функциональность по-прежнему доступна, и вы можете создавать рабочие службы, запускаемые в качестве службы Windows.

Из этого руководства вы узнаете, как выполнять следующие задачи:

  • Публикация приложения рабочей роли .NET как единого исполняемого файла.
  • Создание службы Windows.
  • Создание приложения BackgroundService как службы Windows.
  • Запуск и завершение работы службы Windows.
  • Просмотр журналов событий.
  • Удаление службы Windows.

Совет

Весь пример исходного кода "Рабочие роли в .NET" доступен для скачивания в Обозревателе примеров. Дополнительные сведения см. в разделе Обзор примеров кода: рабочие роли в .NET.

Важно!

Установка пакета SDK для .NET также устанавливает Microsoft.NET.Sdk.Worker и шаблон рабочей роли. Другими словами, после установки пакета SDK для .NET можно создать рабочую роль с помощью команды dotnet new worker . Если вы используете Visual Studio, шаблон скрыт до установки необязательной ASP.NET и рабочей нагрузки веб-разработки.

Необходимые компоненты

Создание нового проекта

Чтобы создать новый проект службы рабочей роли с помощью Visual Studio, выберите Файл>Создать>Проект. В диалоговом окне Создание нового проекта выполните поиск по запросу "служба рабочей роли" и выберите шаблон "Служба рабочей роли". Если вы предпочитаете использовать .NET CLI, откройте используемый терминал в рабочем каталоге. Выполните команду dotnet new и замените <Project.Name> именем проекта.

dotnet new worker --name <Project.Name>

Дополнительные сведения о команде .NET CLI для создания проекта службы рабочей роли см. здесь.

Совет

Если вы используете Visual Studio Code, вы можете выполнять команды .NET CLI из интегрированного терминала. Дополнительные сведения см. в статье Visual Studio Code: интегрированный терминал.

Установка пакета NuGet

Чтобы взаимодействовать с собственными службами Windows из реализаций .NET IHostedService , необходимо установить Microsoft.Extensions.Hosting.WindowsServices пакет NuGet.

Чтобы установить его из Visual Studio, откройте диалоговое окно Управление пакетами NuGet. Выполните поиск по запросу "Microsoft.Extensions.Hosting.WindowsServices" и установите пакет. Если вы используете интерфейс командной строки .NET, выполните команду dotnet add package:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Дополнительные сведения о команде добавления пакета в .NET CLI см. в разделе dotnet add package.

После успешного добавления пакетов файл проекта должен содержать следующие ссылки на пакеты:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>

Обновление файла проекта

Этот рабочий проект использует ссылочные типы C#, допускающие значение NULL. Чтобы включить их для всего проекта, обновите файл проекта соответствующим образом:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
  </ItemGroup>
</Project>

В приведенном выше изменении файла проекта добавляется узел <Nullable>enable<Nullable>. Дополнительные сведения см. в разделе Задание контекста, допускающего значение NULL.

Создание службы

Добавьте новый класс в проект с именем JokeService.cs и замените его содержимое следующим кодом C#:

namespace App.WindowsService;

public sealed class JokeService
{
    public string GetJoke()
    {
        Joke joke = _jokes.ElementAt(
            Random.Shared.Next(_jokes.Count));

        return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
    }

    // Programming jokes borrowed from:
    // https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
    private readonly HashSet<Joke> _jokes = new()
    {
        new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
        new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
        new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
        new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
        new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
        new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
        new Joke("['hip', 'hip']", "(hip hip array)"),
        new Joke("To understand what recursion is...", "You must first understand what recursion is"),
        new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
        new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
        new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
        new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
        new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
        new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
        new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
        new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
        new Joke("Knock-knock.", "A race condition. Who is there?"),
        new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
        new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
        new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
        new Joke("What did the router say to the doctor?", "It hurts when IP."),
        new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
        new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
    };
}

readonly record struct Joke(string Setup, string Punchline);

Предыдущий исходный код службы шутки предоставляет один элемент функциональности, GetJoke метод. Это string возвращающий метод, представляющий шутку случайного программирования. Поле класса область используется _jokes для хранения списка шуток. Случайная шутка выбирается из списка и возвращается.

Перезапись класса Worker

Замените содержимое Worker из шаблона следующим кодом C# и переименуйте файл в WindowsBackgroundService.cs:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

В приведенном выше коде JokeService вставляется вместе с ILogger. Оба доступны для класса в качестве полей private readonly. В методе ExecuteAsync служба joke запрашивает шутку и записывает ее в средство ведения журнала. В этом случае средство ведения журнала реализуется журналом событий Windows — Microsoft.Extensions.Logging.EventLog.EventLogLogger. Записанные журналы сохраняются и доступны для ознакомления в Просмотре событий.

Примечание.

По умолчанию в Журнале событий установлен уровень серьезности Warning. Его можно настроить, но в целях демонстрации в журналах WindowsBackgroundService используется метод расширения LogWarning. Чтобы специально указать уровень EventLog, добавьте запись в раздел appsettings.{Environment}.json или укажите значение EventLogSettings.Filter.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    },
    "EventLog": {
      "SourceName": "The Joke Service",
      "LogName": "Application",
      "LogLevel": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

Дополнительные сведения о настройке уровней ведения журнала см. в разделе Поставщики ведения журнала в .NET: настройка журнала событий Windows.

Перезапись класса Program

Замените содержимое файла шаблона Program.cs следующим кодом C#:

using App.WindowsService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = ".NET Joke Service";
});

LoggerProviderOptions.RegisterProviderOptions<
    EventLogSettings, EventLogLoggerProvider>(builder.Services);

builder.Services.AddSingleton<JokeService>();
builder.Services.AddHostedService<WindowsBackgroundService>();

IHost host = builder.Build();
host.Run();

Метод расширения AddWindowsService настраивает приложение для работы в качестве службы Windows. Для имени службы задано значение ".NET Joke Service". Размещенная служба зарегистрирована для внедрения зависимостей.

Дополнительные сведения о регистрации служб см. в статье Внедрение зависимостей в .NET.

Публикация приложения

Чтобы создать приложение службы рабочей роли .NET в качестве службы Windows, лучше всего публиковать приложение как один исполняемый файл. Наличие автономного исполняемого файла с меньшей вероятностью приводит к ошибкам, так как в этом шаблоне не нужно размещать в файловой системе зависимые файлы. Но вы можете выбрать другую модальность публикации, и это совершенно допустимо, если вы правильно создаете файл *.exe, который может быть целевым для диспетчера управления службами Windows.

Важно!

Вместо этого для публикации можно скомпилировать файл *.dll вместо *.exe и тогда при установке опубликованного приложения в диспетчере управления службами Windows этот файл DLL и управление установкой передается интерфейсу командной строки .NET. Дополнительные сведения см. в справочнике по команде dotnet интерфейса командной строки .NET.

sc.exe create ".NET Joke Service" binpath="C:\Path\To\dotnet.exe C:\Path\To\App.WindowsService.dll"
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
    <OutputType>exe</OutputType>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
  </ItemGroup>
</Project>

Ранее выделенные строки файла проекта определяют следующие варианты поведения:

  • <OutputType>exe</OutputType>. Создание консольного приложения.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>. Включение однофайловой публикации.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: определяет RID для win-x64.
  • <PlatformTarget>x64</PlatformTarget>: определяет 64-разрядный ЦП целевой платформы.

Чтобы опубликовать приложение из Visual Studio, можно создать профиль публикации, который сохраняется. Профиль публикации основан на XML и имеет расширение файла .pubxml. Visual Studio использует этот профиль для публикации приложения неявно, в то время как при использовании .NET CLI необходимо явно указать профиль публикации для его использования.

Щелкните правой кнопкой проект в Обозревателе решения, а затем выберите команду Опубликовать.... После этого выберите Добавить профиль публикации, чтобы создать профиль. В диалоговом окне Публикация выберите значение Папка в поле Цель.

The Visual Studio Publish dialog

В поле Расположение оставьте значение по умолчанию, а затем щелкните Готово. После создания профиля выберите Показать все настройки и проверьте Настройки профиля.

The Visual Studio Profile settings

Убедитесь, что заданы такие параметры:

  • Режим развертывания: автономно
  • Производить один файл: проверено
  • Включить компиляцию ReadyToRun: проверено
  • Обрезать неиспользуемые сборки (в предварительной версии): флажок не установлен

Наконец, нажмите кнопку Опубликовать. Приложение компилируется, а полученный файл EXE публикуется в выходном каталоге /publish.

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

dotnet publish --output "C:\custom\publish\directory"

Дополнительные сведения см. в разделе dotnet publish.

Важно!

При попытке отладки приложения с <PublishSingleFile>true</PublishSingleFile> помощью параметра .NET 6 вы не сможете выполнить отладку приложения. Дополнительные сведения см. в разделе "Не удалось подключиться к CoreCLR" при отладке приложения .NET 6 "PublishSingleFile".

Создание службы Windows

Если вы не знакомы с помощью PowerShell и хотите создать установщик для службы, см. статью "Создание установщика службы Windows". В противном случае для создания службы Windows используйте собственную команду диспетчера управления службами Windows (sc.exe). Откройте PowerShell от имени администратора.

sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.WindowsService.exe"

Совет

Если вам нужно изменить корень содержимого конфигурации узла, вы можете передать его в качестве аргумента командной строки при указании binpath:

sc.exe create "Svc Name" binpath="C:\Path\To\App.exe --contentRoot C:\Other\Path"

Вы увидите выходное сообщение:

[SC] CreateService SUCCESS

Дополнительные сведения см. в разделе sc.exe create.

Настройка службы Windows

После создания службы ее можно дополнительно настроить. Если вас устраивают настройки службы по умолчанию, перейдите к разделу Проверка функциональности службы.

Службы Windows предоставляют параметры конфигурации восстановления. Чтобы прочитать текущие значения конфигурации восстановления, можно запросить текущую конфигурацию, используя команду sc.exe qfailure "<Service Name>" (где <Service Name> — имя службы).

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :

Команда выводит конфигурацию восстановления, которая является значениями по умолчанию, так как они еще не настроены.

The Windows Service recovery configuration properties dialog.

Чтобы настроить восстановление, используйте команду sc.exe failure "<Service Name>", где <Service Name> — имя службы:

sc.exe failure ".NET Joke Service" reset=0 actions=restart/60000/restart/60000/run/1000
[SC] ChangeServiceConfig2 SUCCESS

Совет

Чтобы настроить параметры восстановления, необходимо запустить сеанс терминала от имени администратора.

После успешной настройки можно снова запросить значения с помощью команды sc.exe qfailure "<Service Name>":

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :
        FAILURE_ACTIONS              : RESTART -- Delay = 60000 milliseconds.
                                       RESTART -- Delay = 60000 milliseconds.
                                       RUN PROCESS -- Delay = 1000 milliseconds.

Вы увидите настроенные значения перезапуска.

The Windows Service recovery configuration properties dialog with restart enabled.

Параметры восстановления службы и экземпляры .NET BackgroundService

При использовании .NET 6 в .NET добавлены новые функции обработки исключений для размещения. Перечисление BackgroundServiceExceptionBehavior было добавлено в Microsoft.Extensions.Hosting пространство имен и используется для указания поведения службы при возникновении исключения. Доступные параметры перечислены в следующей таблице:

Параметр Описание
Ignore Игнорировать исключения, создаваемые в BackgroundService.
StopHost При IHost возникновении необработанного исключения будет остановлено.

Поведение по умолчанию до .NET 6 Ignore, которое привело к зомби-процессам (выполняющийся процесс, который ничего не делал). При использовании .NET 6 по умолчанию используется StopHostповедение по умолчанию, которое приводит к остановке узла при возникновении исключения. Но она останавливается чисто, что означает, что система управления службами Windows не перезагрузит службу. Чтобы правильно разрешить перезапуску службы, можно вызвать Environment.Exit с помощью кода выхода, отличного от нуля. Рассмотрим следующий выделенный catch блок:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

Проверка функциональности службы

Чтобы увидеть приложение, созданное как служба Windows, откройте апплет Службы. Выберите ключ Windows (или нажмите сочетание клавиш Ctrl + Esc) и выполните поиск по запросу "службы". В апплете Службы вы сможете найти службу по ее имени.

Важно!

По умолчанию обычные (неадминистрированные) пользователи не могут управлять службами Windows. Чтобы убедиться, что это приложение работает должным образом, необходимо использовать учетную запись Администратор.

The Services user interface.

Чтобы убедиться, что служба работает должным образом, необходимо выполнить следующие действия.

  • Запуск службы
  • Просмотр журналов
  • Остановить службу

Важно!

Чтобы выполнить отладку приложения, убедитесь, что вы не пытаетесь выполнить отладку исполняемого файла, который активно выполняется в процессе служб Windows.

Unable to start program.

Запуск службы Windows

Чтобы запустить службу Windows, используйте команду sc.exe start:

sc.exe start ".NET Joke Service"

Появится результат, аналогичный указанному ниже.

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 2  START_PENDING
                            (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x7d0
    PID                : 37636
    FLAGS

Состояние службы изменится с START_PENDING на Выполняется.

Просмотреть журналы

Чтобы просмотреть журналы, откройте Средство просмотра событий. Выберите ключ Windows (или нажмите сочетание клавиш Ctrl + Esc) и выполните поиск по запросу "Event Viewer". Выберите узел Средство просмотра событий (локально)>Журналы Windows>Приложение. Вы должны увидеть запись уровня Предупреждения и Источник, который соответствует пространству имен приложений. Дважды щелкните запись или щелкните правой кнопкой мыши и выберите Свойства события, чтобы просмотреть подробные сведения.

The Event Properties dialog, with details logged from the service

После просмотра журналов в Журнале событий службу следует остановить. Она предназначена для записи одной случайной шутки в минуту. Такая реакция на событие и задумана, но это непрактично для рабочих служб.

Останов службы Windows

Чтобы остановить службу Windows, используйте команду sc.exe stop:

sc.exe stop ".NET Joke Service"

Появится результат, аналогичный указанному ниже.

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 3  STOP_PENDING
                            (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x0

Состояние службы перейдет с STOP_PENDING остановленного.

Удаление службы Windows

Чтобы удалить службу Windows, используйте собственную команду удаления (sc.exe) диспетчера служб Windows. Откройте PowerShell от имени администратора.

Важно!

Если служба не находится в состоянии Остановлена, она не будет немедленно удалена. Перед выполнением команды удаления убедитесь, что служба остановлена.

sc.exe delete ".NET Joke Service"

Вы увидите выходное сообщение:

[SC] DeleteService SUCCESS

Дополнительные сведения см. в разделе sc.exe delete.

См. также

Следующий