Создание службы Windows с помощью BackgroundService
Разработчики .NET Framework, вероятно, знакомы с приложениями службы Windows. До .NET Core и .NET 5+ разработчики, которые опирались на .NET Framework, могли создавать службы Windows для выполнения фоновых задач или выполнения длительных процессов. Эта функция по-прежнему доступна и вы можете создавать рабочие службы, которые выполняются в качестве службы Windows.
В этом руководстве описано, как:
- Публикация рабочего приложения .NET в виде одного исполняемого файла.
- Создайте службу Windows.
- Создайте приложение
BackgroundService
в качестве службы Windows. - Запустите и остановите службу Windows.
- Просмотр журналов событий.
- Удалите службу Windows.
Совет
Все примеры исходного кода из "Workers in .NET" доступны в обзоре примеров для загрузки. Дополнительные сведения см. в разделе Примеры кода: рабочие процессы в .NET.
Важный
Установка пакета SDK для .NET также устанавливает Microsoft.NET.Sdk.Worker
и рабочий шаблон. Другими словами, после установки .NET SDK можно создать новый рабочий процесс с помощью команды dotnet new worker. Если вы используете Visual Studio, шаблон скрыт до установки необязательных компонентов ASP.NET и веб-разработки.
Необходимые условия
- Пакет SDK .NET 8.0 или более поздней версии
- ОС Windows
- Интегрированная среда разработки .NET (IDE)
- Не стесняйтесь использовать Visual Studio
Создание проекта
Чтобы создать проект рабочей службы с помощью Visual Studio, выберите Файл>Новый проект>.... В диалоговом окне Создание нового проекта найдите "Worker Service" и выберите шаблон "Служба обработчик". Если вы предпочитаете использовать интерфейс командной строки .NET, откройте любимый терминал в рабочем каталоге. Выполните команду dotnet new
и замените <Project.Name>
нужным именем проекта.
dotnet new worker --name <Project.Name>
Дополнительные сведения о команде создания нового проекта рабочей службы в .NET CLI см. в разделе dotnet new worker.
Совет
Если вы используете Visual Studio Code, вы можете запускать команды CLI .NET из интегрированного терминала. Дополнительные сведения см. в Visual Studio Code: интегрированный терминал.
Установка пакета NuGet
Чтобы взаимодействовать с собственными службами Windows из реализаций .NET IHostedService, необходимо установить пакет NuGet Microsoft.Extensions.Hosting.WindowsServices
.
Чтобы установить это из 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="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.3" />
</ItemGroup>
Обновление файла проекта
Этот рабочий проект использует null-ссылочные типы C#. Чтобы включить их для всего проекта, обновите файл проекта соответствующим образом:
<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="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.3" />
</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
. Оба становятся доступными для класса в виде полей. В методе ExecuteAsync
сервис шуток запрашивает шутку и записывает ее в логгер. В этом случае средство ведения журнала реализовано через журнал событий Windows - Microsoft.Extensions.Logging.EventLog.EventLogLoggerProvider. Журналы записываются в и доступны для просмотра в средстве просмотра событий.
Заметка
По умолчанию серьезность журнала событий — это 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 Worker Service в качестве службы Windows, рекомендуется опубликовать приложение в виде одного исполняемого файла. Самодостаточный исполняемый файл менее подвержен ошибкам, так как в файловой системе отсутствуют зависимые файлы. Но вы можете выбрать другую модаль публикации, которая является совершенно приемлемой, если вы создаете файл *.exe, который может быть предназначен диспетчером управления службами Windows.
Важный
Альтернативный подход к публикации заключается в создании *.dll (вместо *.exe), а при установке опубликованного приложения с помощью диспетчера управления службами Windows вы делегируете в .NET CLI и передаете DLL. Дополнительные сведения см. в разделе .NET CLI: команда dotnet.
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="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.3" />
</ItemGroup>
</Project>
Предыдущие выделенные строки файла проекта определяют следующее поведение:
-
<OutputType>exe</OutputType>
: создает консольное приложение. -
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
: позволяет выполнять публикацию в один файл. -
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
: указывает RID объектаwin-x64
. -
<PlatformTarget>x64</PlatformTarget>
: укажите ЦП целевой платформы 64-разрядной.
Чтобы опубликовать приложение из Visual Studio, можно создать сохраненный профиль публикации. Профиль публикации имеет расширение файла .pubxml и основан на XML. Visual Studio использует этот профиль для публикации приложения неявно, в то время как если вы используете .NET CLI, необходимо явно указать профиль публикации для его использования.
Щелкните проект правой кнопкой мыши в обозревателя решенийи выберите Опубликовать. Затем выберите Добавить профиль публикации, чтобы создать профиль. В диалоговом окне Публикации выберите папку в качестве вашей цели.
Оставьте положение по умолчанию , а затем выберите Готово. После создания профиля выберите Показать все параметрыи проверьте параметры профиля.
Убедитесь, что указаны следующие параметры:
- Режим развертывания: автономный
- Создание одного файла: установлен флажок
- Включить компиляцию 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 создайте.
Настройка службы 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 :
Команда выводит конфигурацию восстановления, которая является значениями по умолчанию, так как они еще не настроены.
Чтобы настроить восстановление, используйте 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.
Вы увидите настроенные значения перезапуска.
Варианты восстановления служб и экземпляры .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. Чтобы убедиться, что это приложение работает должным образом, необходимо использовать учетную запись администратора.
Чтобы убедиться, что служба работает должным образом, необходимо:
- Запуск службы
- Просмотр журналов
- Остановка службы
Важный
Чтобы выполнить отладку приложения, убедитесь, что вы не пытаетесь выполнить отладку исполняемого файла, активно работающего в процессе служб Windows.
Запуск службы 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>узел приложения. Вы должны увидеть запись категории Предупреждение с источником, соответствующим пространству имен приложений. Дважды щелкните запись или щелкните правой кнопкой мыши и выберите Свойства события, чтобы просмотреть детали.
После просмотра записей в журнале событий следует остановить службу. Он предназначен для регистрации случайной шутки один раз в минуту. Это намеренное поведение, но не практично для производственных служб.
Остановка службы 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, используйте команду удаления в диспетчере управления службами Windows (sc.exe). Запустите PowerShell от имени администратора.
Важный
Если служба не находится в состоянии остановлено, она не будет немедленно удалена. Перед выполнением команды удаления убедитесь, что служба остановлена.
sc.exe delete ".NET Joke Service"
Вы увидите выходное сообщение:
[SC] DeleteService SUCCESS
Дополнительные сведения см. в разделе sc.exe удалите.
См. также
- создание установщика службы Windows
- рабочие службы в .NET
- создание службы очередей
-
Использовать службы с областью действия в
BackgroundService
-
Реализация интерфейса
IHostedService