Введение

После перехода Microsoft с .NET Framework на .NET Core программирование на C# стало более увлекательным, хотя некоторые аспекты изменились.

В частности, шаблон проекта Служба Windows (.NET Framework) создаёт класс, наследник от ServiceBase в пространстве имен System.ServiceProcess. Прогеру предлагается  реализовать виртуальные методы базового класса OnStart и OnStop, которые задают действия, подлежащие выполнению при запуске (остановке) службы, что собственно и есть суть и назначение Службы Виндовз. Регистрация Службы в этом случае производится с помощью утилиты installUtil.exe, в .Net Core это делается утилитой SC.exe.

Реализовать службу на .NET Core (в моем случае .NET 9) не сложнее, но по другому, шаблон проекта теперь называется Worker Service (Microsoft), а рабочий класс наследуется от BackgroundService.

В этой статье я подробно опишу процесс создания, публикации и регистрации Службы в .Net 9 на примере службы для Telegram-бота (сокращенно — Телебот). Почему бот? Во-первых, писать Телебота на C# — это действительно приятно. Во-вторых, чтобы обеспечить его круглосуточную доступность на сервере под управлением Windows, логично использовать именно Службу Windows, которая будет поддерживать его работу в фоновом режиме и запускаться может сама при перезагрузке сервера.

В заключении рассмотрим как добавить логирование в стандартный виндовый EventLog и немного обсудим функционал самого Телебота.

1. Создание проекта

И так, поехали. В Visual Studio (у меня Community 2022, версия 17.12.1) выбираем шаблон проекта Worker Service (Microsoft). Жмем Далее, пишем имя проекта у меня Svc2, снова Далее и получаем пустой проект Рабочего сервиса.

В шаблонном проекте Рабочего сервиса нам уже добавили зависимость Microsoft.Extensions.Hosting и два файла Program.cs и Worker.cs. В первом дается подсказка как правильно внедрять зависимости (Dependency Injections), второй это сам Воркер, то место, где будем запускать Телебота. Для простоты восприятия я почищу файл класса Worker от лишних подробностей касательно логирования.

namespace Svc2;

public class Worker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            Console.WriteLine($"Worker running at: {time}");
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Запустим, просто F5, проверим.

Все отлично, работает. Работает как обычное консольное приложение, теперь надо сделать из этого Службу.

2. Добавление пакета поддержки службы Windows

Класс Worker наследован от BackgroundService и наш проект запускается и работает как консольное приложение но это еще не служба. Что бы сделать из него Windows Service надо добавить в зависимости пакет Microsoft.Extensions.Hosting.WindowsServices, это сделает наш  класс Worker способным реагировать на команды запуска и остановки сервиса через стандартную консоль.

В поиске менеджера пакетов NuGet, по запросу WindowsServices находим то что нам надо. Microsoft.Extensions.Hosting.WindowsServices. Хороший пакет, 44 миллиона скачиваний, солидно, заслуживает доверия.

Вот эти два пакета, это все что нам нужно для реализации Службы.

3. Редактирование кода

3.1 Programm.cs

После добавления пакета Microsoft.Extensions.Hosting.WindowsServices поле Services объекта builder было расширено методом AddWindowsService. Сделаем вызов этого метода указав имя нашей Службы, так как оно должно будет отображаться в общем списке Служб. 

using Svc2;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

builder.Services.AddWindowsService(options =>
    options.ServiceName = "AM Telebot"
);

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

Это у нас так Microsoft реализует Внедрение Зависимости (Dependency Injection).

3.2 Worker.cs

Класс Worker изменим следующим образом, добавим кое что

namespace Svc2;

public class Worker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Service started
    }
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        // Service stoped

        await base.StopAsync(cancellationToken);
    }
}

К имеющемуся изначально в шаблоне методу ExecuteAsync, добавим переопределение метода StopAsync. Именно эти два метода будут получать управление при запуске и остановке службы соответственно.

4. Публикация сервиса

Собственно код для сервиса у нас готов, теперь надо его выложить куда то, где он будет работать, то есть Опубликовать (Publish) наш проект.

В контекстном меню проекта выберем Опубликовать… в Папку и жмем далее.

Пропишем удобную для нас папку.

Ну и жмем Опубликовать.

Если публикация прошла успешно в папке D:\Projects\CoreService\_pub появится большая куча файлов. Что бы привести в порядок эту кучу добавим кое что в файл проекта. Вот эти несколько строк в тэге PropertyGroup сделают папку с публикацией более компактной

<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<DebugType>embedded</DebugType>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>

Файл проекта теперь должен выглядеть вот так:

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

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>dotnet-Svc2-d4d482f3-62dd-4f7d-99cd-b6c3c130f446</UserSecretsId>

    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <DebugType>embedded</DebugType>
    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
  </PropertyGroup>

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

Еще раз пересоберем и опубликуем проект. Теперь видим, что в папке с публикацией всего 3-и файлика.

Да, думаю так удобнее.

5. Регистрация сервиса

Для того, что бы наш сервис был виден в Консоли Управления Службами Windows воспользуемся утилитой SC.EXE. Откроем окно командной строки под админской учеткой и выполним следующую команду: 

sc create "AM Telebot" binPath=D:\Projects\CoreService\_pub\Svc2.exe

Тут написано: create значит создать сервис с именем AM Telebot, и вот там, куда указывает binPath, лежит его исполняемый файл. Выполним это в окне командной строки (запущенной под админской учеткой).

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

И таки да, наш AM Telebot появился в списке служб. Тут уже, в свойствах Службы, можно поменять ему пользователя и тип запуска, например на Автомат, что бы сам запускался при перезагрузке сервера. Еще можно задать режим восстановления при непредвиденной ошибке и падении, в общем там есть полезные настройки.

Теперь проверим как она запустится.

Вроде как все норм, работает служба.

Кстати, для удаления службы используем ту же утилиту SC.exe с опцией delete и указав имя службы, вот так.

sc delete "AM Telebot"

6. Добавление Тедебота в проект

Телебот у меня уже реализован в отдельной dll и я просто добавляю его в решение. Как реализован сам Телебот покажу как нибудь подробнее в следующем опусе, а для добавления его в наш сервис я просто включаю зависимость от этого проекта, вот так как на картинке.

Телебот сам по себе тоже зависит от трех проектов: amSecrets, amLogger и amFireWall. В последнем зашита как раз полезная часть функционала бота, именно там происходит взаимодействие с Брэндмауаром сервера. С его помощью можно добавить IP в белый список для доступа к серверу по RDP, удалить IP адрес из белого списка, получить список всех разрешенных IP адресов, а еще можно полностью отрубить правило доступа по RDP, это на случай когда надо срочно закрыть доступ к серверу вообще всем, такой вот Аларм батон.

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

Класс Worker службы чуть изменим, добавим строки запуска и остановки Телебота вот так.

namespace Svc2;

public class Worker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Service started
        await amTelebot.Worker.Start("Телебот запущен");
    }
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        // Service stoped
        await amTelebot.Worker.Stop("Телебот выключен");

        await base.StopAsync(cancellationToken);
    }
}

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

7. Запуск и тестирование Телебота

После публикации проекта службы идем в Консоль Управления Службами windows и запускаем наш Телебот.

В телеграмм появилось сообщение – Бот запущен.

Остановим службу.

Пришло сообщение, что Телебот выключен. Все отлично работает.

8. Добавим логирование

Служба Windows это стандартное средство этой операционной системы, поэтому для логирования, с учетом того, что мы пишем приложение определенно под Windows, будем использовать стандартный Журнал Событий Windows (Windows Event Log). Всего три небольшие правки в коде помогут нам это сделать легко и непринужденно.

Во первых в файле appsettings.json надо добавить секцию

“EventLog”: {
     “LogLevel”: {
          “Default”: “Information”
     }
}

В результате файл должен выглядеть примерно как то так

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "EventLog": {
      "LogLevel": {
        "Default": "Information"
      }
    }
  }
}

Во вторых, в файле Program.cs добавим внедрение зависимости (Dependency Injection) для сервиса EventLog следующим образом

builder.Logging.AddEventLog(c => {
    c.LogName = "AM Telebot LN";
    c.SourceName = "AM Telebot SRC";
});

таким образом, создаем журнал AM Telebot LN и источник AM Telebot SRC

Файл Program.cs теперь будет выглядеть так

using Svc2;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

builder.Services.AddWindowsService(options =>
    options.ServiceName = "AM Telebot"
);

builder.Logging.AddEventLog(c => {
    c.LogName = "AM Telebot LN";
    c.SourceName = "AM Telebot SRC";
});

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

После того как отработает этот код в Консоли Просмотра Событий в папочке Журналы приложений и служб появится новый журнал AM Telebot.

В этот журнал и будут писаться наши логи. Писать в лог будем в классе Worker в файле Worker.cs, для этого поменяем код в этом файле следующим образом

namespace Svc2;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Service started
        var msg = "Телебот запущен";
        _logger.LogInformation(msg);
        await amTelebot.Worker.Start(msg);
    }
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        // Service stoped
        var msg = "Телебот выключен";
        _logger.LogInformation(msg);
        await amTelebot.Worker.Stop(msg);

        await base.StopAsync(cancellationToken);
    }
}

Если откомпилируем и опубликуем этот код, то при запуске и остановке сервиса получим сообщения Телебот запущен и Телебот выключен не только уже в Телеграм, но и в Консоли Просмотра событий Windows, в Журнале AM Telebot LN появятся те же сообщения.

С помощью своей библиотечки amLogger.dll (которую более подробно опишу в отдельной статейке, а тут покажу только как его можно использовать) изменю файлик Worker.cs в проекте службы следующим образом.

using amLogger;

namespace Svc2;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Service started
        Logger.Instance.Init(log => OnLog(log));

        var msg = "Телебот запущен";
        await amTelebot.Worker.Start(msg);
        Log.Info(1, "Svc2.Worker.ExecuteAsync", msg);
    }
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        // Service stoped
        var msg = "Телебот выключен";
        await amTelebot.Worker.Stop(msg);
        Log.Info(1, "Svc2.Worker.StopAsync", msg);

        await base.StopAsync(cancellationToken);
    }
    void OnLog(Log log)
    {
        // Писать лог для внешнего просмотра будем тут
        // удобнее всего писать в журнал событий windows (Windows Event Log)
        switch (log.lvl)
        {
            case Level.Info:
                _logger.LogInformation(log.id, log.msg, log.src);
                break;
            case Level.Error:
                _logger.LogError(log.id, log.msg, log.src);
                break;
            case Level.Debug:
                _logger.LogDebug(log.msg);
                break;
        }
    }
}

Более подробно опишу идею такого логгера в отдельной статейке, а тут покажу только как его можно использовать.

Для начала при старте службы надо инициализировать наш Logger вот так.

Logger.Instance.Init(log => OnLog(log));

Лямбда в параметре Init указывает куда будет переходить управление при логировании. В мрем случае это будет метод OnLog, и в него будет передаваться объект log вот такой структуры:

public class Log
{
    public int id;          // Some ID
    public int type;        // Type of message
    public Level lvl;
    public string src = ""; // Source
    public string msg = ""; // Message
}

У класса Log есть еще набор статических методов, которые после инициализации можно вызывать в любой части, любого модуля солюшена, одинаково во всех DLL. Все эти методы соответствуют определенному уровню логирования:

  • Trace

  • Info

  • Warn

  • Error

  • Fatal

Весь код логера можно посмотреть тут

https://github.com/amizerov/CoreService/blob/master/amLogger/Logger.cs

После инициализации в проекте Службы, использовать amLogger можно в любом модуле решения, и запись выглядит довольно локанично.

Log.Info("Svc2.Worker.ExecuteAsync", msg);

Имя метода Info соответствует уровню логирования Info, первый параметр это сорс, второй сам мессэдж. При этом вызов этого метода передает управление в OnLog, указанный при инициализации, это единая точка входа для всех вызовов логирования, и тут мы будем выводить лог наружу, часть сообщений в телеграм, а основные в Windows Event Log.

В модуле Телебота логирование ошибки может выглядеть так.

static Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken token)
{
    Log.Fatal("Telebot error", $"An error occurred: {exception.Message}");
    return Task.CompletedTask;
}

Весть код Телебота можно посмотреть тут

https://github.com/amizerov/CoreService/tree/master/amTelebot

Заключение

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

На всякий случай, кому интересно ниже ссылка на проект целиком.

https://github.com/amizerov/CoreService.git

Комментарии (5)


  1. Marsezi
    04.12.2024 12:42

    А если async await не использовать?

    1) при старте создавать таск и просто возвращать управление, то сервис пишет что windows service завершил работу и служба останавливается

    2} если создать task и в конце бесконечно ждать, то windows service долго пытается запустить службу и потом ошибка что сервис не ответил


    1. Nicotino Автор
      04.12.2024 12:42

      Мне кажется вы зря не любите async await. Но скорее всего в случае 1) так и должно быть, а вот в случае 2) честно говоря не верю, покажите код.


  1. xi-tauw
    04.12.2024 12:42

    А зачем боту сервис, а не просто приложение?


    1. agranom555
      04.12.2024 12:42

      Возможно автоматический перезапуск в случае ошибки


      1. Refridgerator
        04.12.2024 12:42

        А также отсутствие необходимости входа в сеанс.