Всем привет! Недавно я написал Discord бота для World of Warcraft гильдии. Он регулярно забирает данные об игроках с серверов игры и пишет сообщения в Discord о том что к гильдии присоединился новый игрок или о том что гильдию покинул старый игрок. Между собой мы прозвали этого бота Батрак. В этой статье я решил поделиться опытом и рассказать как сделать такой проект. По сути мы будем реализовывать микросервис на .NET Core: напишем логику, проведем интеграцию с api сторонних сервисов, покроем тестами, упакуем в Docker и разместим в Heroku. Кроме этого я покажу как реализовать continuous integration с помощью Github Actions. От вас не потребуется никаких знаний об игре. Я написал материал так чтобы можно было абстрагироваться от игры и сделал заглушку для данных об игроках. Но если у вас есть учетная запись в Battle.net, то вы сможете получать реальные данные. Для понимания материала, от вас ожидается хотя бы минимальный опыт создания веб сервисов с помощью фреймворка ASP.NET и небольшой опыт работы с Docker. На каждом шаге будем постепенно наращивать функционал. Создадим новый web api проект с одним контроллером /check. При     обращении к этому адресу будем отправлять строку “Hello!” в Discord чат. Научимся получать данные о составе гильдии с помощью готовой библиотеки или заглушки. Научимся сохранять в кэш полученный список игроков чтобы при следующих проверках находить различия с предыдущей версией списка.     Обо всех изменениях будем писать в Discord. Напишем Dockerfile для нашего проекта и разместим проект на     хостинге Heroku. Посмотрим на несколько способов сделать периодическое выполнение кода. Реализуем автоматическую сборку, запуск тестов и публикацию проекта после каждого коммита в master Нам потребуется создать новый ASP.NET Core Web API проект.   Создание нового проекта - это одна из фундаментальных вещей которые я не буду подробно расписывать. При работе над проектом используйте сервис Github для хранения кода. В дальнейшем мы воспользуемся несколькими возможностями Github. Добавим к проекту новый контроллер Затем нам понадобится webhook от вашего Discord сервера. Webhook - это механизм отправки событий. В данном случае, то это адрес к которому можно слать простые http запросы с сообщениями внутри. Получить его можно в пункте integrations в настройках любого текстового канала вашего Discord сервера. Добавим webhook в appsettings.json нашего проекта. Позже мы унесем его в переменные окружения Heroku. Если вы не знакомы с тем как работать с конфигурацией в ASP Core проектах предварительно изучите эту тему. Теперь создадим новый сервис DiscordBroker, который умеет отправлять сообщения в Discord. Создайте папку Services и поместите туда новый класс, эта папка нам еще пригодится. По сути этот новый сервис делает post запрос по адресу из webhook и содержит сообщение в теле запроса. Как видите, мы используем внедрение зависимостей. IConfiguration позволит нам достать webhook из конфигов, а IHttpClientFactory создать новый HttpClient. Кроме того, я извлек интерфейс этого класса, чтобы в дальнейшем можно было сделать его заглушку при тестировании. Делайте это для всех сервисов которые мы будем создавать далее. Не забудьте что новый класс нужно будет зарегистрировать в Startup. А также нужно будет зарегистрировать HttpClient, для работы IHttpClientFactory. Теперь можно воспользоваться новым классом в контроллере. Запустите проект, зайдите по адресу /check в браузере и убедитесь что в Discord пришло новое сообщение. У нас есть два варианта: получать данные из настоящих серверов battle.net или из моей заглушки. Если у вас нет аккаунта в battle.net, то пропустите следующий кусок статьи до момента где приводится реализация заглушки. Получаем реальные данные Вам понадобится зайти на https://develop.battle.net/ и получить там две персональных строки BattleNetId и BattleNetSecret. Они будут нужны нам чтобы авторизоваться в api перед отправкой запросов. Поместите их в appsettings. Подключим к проекту библиотеку ArgentPonyWarcraftClient. Создадим новый класс BattleNetApiClient в папке Services. В конструкторе мы создаем новый экземпляр класса WarcraftClient.Этот класс относится к библиотеке, которую мы установили ранее. С его помощью можно получать данные об игроках. Кроме этого, нужно создать в appsettings проекта две новых записи RealmName и GuildName. RealmName это название игрового мира, а GuildName это название гильдии. Их будем использовать как параметры при запросе. Сделаем метод GetGuildMembers чтобы получать состав гильдии и создадим модель WowCharacterToken которая будет представлять собой информацию об игроке. Класс WowCharacterToken следует поместить в папку Models. Не забудьте подключить BattleNetApiClient в Startup. Берем данные из заглушки Для начала создадим модель WowCharacterToken и поместим ее в папку Models. Она представляет собой информацию об игроке. Дальше сделаем вот такой класс Он возвращает зашитый в него список игроков. При первом вызове метода мы вернем один список, при последующих другой. Это нужно нам что смоделировать изменчивое поведение api. Этой заглушки хватит чтобы продолжить делать проект. Сделайте интерфейс и подключите все что мы создали в Startup. Выведем результаты в Discord После того как мы сделали BattleNetApiClient, им можно воспользоваться в контроллере чтобы вывести кол-во игроков в Discord. Нужно научиться определять какие игроки появились или пропали из списка при последующих запросах к api. Для этого мы можем закэшировать список в InMemory кэше (в оперативной памяти) или во внешнем хранилище. Если закэшировать список в InMemory кэше, то мы потеряем его при перезапуске приложения. Поэтому позже мы подключим базу данных Redis как аддон в Heroku и будем кешировать туда. А пока что подключим InMemory кэш в Startup. Теперь в нашем распоряжении есть IDistributedCache, который можно подключить через конструктор. Я предпочел не использовать его напрямую , а написать для него обертку. Создайте класс GuildRepository и поместите его в новую папку Repositories. GuildRepository нужно зарегистрировать как Singletone зависимость, чтобы кэш не пересоздавался на каждый запрос. Теперь можно написать сервис который будет сравнивать новый список игроков с сохраненным. В качестве возвращаемого результата используется модель Report. Ее нужно создать и поместить в папку Models. Применим GuildService в контроллере. Теперь отправим в Discord какие игроки присоединились или покинули гильдию. Эту логику я добавил в GuildService в конец метода Check. Писать бизнес логику в контроллере не стоит, у него другое назначение. В самом начале мы делали там отправку сообщения в Discord потому что еще не существовало GuildService. Можно вывести больше информации об игроке. Ее можно получить если воспользоваться библиотекой ArgentPonyWarcraftClient Я решил не добавлять в статью больше кода в BattleNetApiClient, чтобы статья не разрослась до безумных размеров. Unit тесты У нас появился класс GuildService с нетривиальной логикой, который будет изменяться и расширяться в будущем. Стоит написать на него тесты. Для этого нужно будет сделать заглушки для BattleNetApiClient, GuildRepository и DiscordBroker. Я специально просил создавать интерфейсы для этих классов чтобы можно было сделать их фейки. Создайте новый проект для Unit тестов. Заведите в нем папку Fakes и сделайте три фейка. Эти фейки позволяют заранее задать возвращаемое значение для методов. Для этих же целей можно использовать популярную библиотеку Moq. Но для нашего простого примера достаточно самодельных фейков. Первый тест на GuildService будет выглядеть так: Как видно из названия, тест позволяет проверить что мы сохраним список игроков, если кэш пуст. Заметьте, в конце теста используется специальный набор методов Should, Be... Это методы из библиотеки FluentAssertions, которые помогают нам сделать Assertion более читабельным. Теперь у нас есть база для написания тестов. Я показал вам основную идею, дальнейшее написание тестов оставляю вам. Главный функционал проекта готов. Теперь можно подумать о его публикации. Мы будем размещать проект на платформе Heroku. Heroku не позволяет запускать .NET проекты из коробки, но она позволяет запускать Docker образы. Чтобы упаковать проект в Docker нам понадобится создать в корне репозитория Dockerfile со следующим содержимым peon.dll это название моего Solution. Peon переводится как батрак. О том как работать с Docker и Heroku можно прочитать здесь. Но я все же опишу последовательность действий. Вам понадобится создать аккаунт в Heroku, установить Heroku CLI. Создайте новый проект в heroku и свяжите его с вашим репозиторием. Теперь нам необходимо создать файл heroku.yml в папке с проектом. У него будет такое содержимое: Дальше выполним небольшую череду команд: Можете открыть приложение в браузере с помощью команды: После того как мы разместили приложение в Heroku, нужно подключить базу данных Redis для кэша. Как вы помните InMemory кэш будет исчезать после перезапуска приложения. Установите для нашего Heroku приложения бесплатный аддон RedisCloud. Строку подключения для Redis можно будет получить через переменную окружения REDISCLOUD_URL. Она будет доступна, когда приложение будет запущено в экосистеме Heroku. Нам нужно получить эту переменную в коде приложения. Установите библиотеку Microsoft.Extensions.Caching.StackExchangeRedis. С помощью нее можно зарегистрировать Redis реализацию для IDistributedCache в Startup. В этом коде мы получили переменную REDISCLOUD_URL из переменных окружения системы. После этого мы извлекли адрес и пароль базы данных с помощью класса RedisUtils. Его написал я сам: На этот класс можно сделать простой Unit тест. После того что мы сделали, GuildRepository будет сохранять кэш не в оперативную память, а в Redis. Нам даже не нужно ничего менять в коде приложения. Опубликуйте новую версию приложения. Нам нужно сделать так чтобы проверка состава гильдии происходила регулярно, например каждые 15 минут. Есть несколько способов это реализовать: Самый простой способ - это сделать задание на сайте https://cron-job.org. Этот сервис будет слать get запрос на /check вашего приложения каждые N минут.  Второй способ - это использовать Hosted Services. В этой статье подробно описано как создать повторяющееся задание в ASP.NET Core проекте. Учтите, бесплатный тариф в Heroku подразумевает что ваше приложение будет засыпать после того как к нему некоторое время не делали запросов. Hosted Service перестанет работать после того как приложение заснет. В этом варианте вам следует перейти на платный тариф. Кстати, так сейчас работает мой бот. Третий способ - это подключить к проекту специальные Cron аддоны. Например Heroku Scheduler. Можете пойти этим путем и разобраться как создать cron job в Heroku. Во-первых, зайдите в настройки приложения в Heroku. Там есть пункт Deploy. Подключите там свой Github аккаунт и включите Automatic deploys после каждого коммита в master. Поставьте галочку у пункта Wait for CI to pass before deploy. Нам нужно чтобы Heroku дожидался сборки и прогонки тестов. Если тесты покраснеют, то публикация не случится. Сделаем сборку и прогонку тестов в Github Actions. Зайдите в репозиторий и перейдите в пункт Actions. Теперь создайте новый workflow на основе шаблона .NET В репозитории появится новый файл dotnet.yml. Он описывает процесс сборки. Как видите по его содержимому, задание build будет запускаться после пуша в ветку master. Содержимое самого задания нас полностью устраивает. Если вы вчитаетесь в то что там происходит, то увидите что там происходит запуск команд dotnet build и dotnet test. Менять в этом файле ничего не нужно, все уже будет работать из коробки. Запушьте что-нибудь в master и посмотрите что задание запускается. Кстати, оно уже должно было запуститься после создания нового workflow. Отлично! Вот мы и сделали микросервис на .NET Core который собирается и публикуется в Heroku. У проекта есть множество точек для развития: можно было бы добавить логирование, прокачать тесты, повесить метрики и. т. д. Надеюсь данная статья подкинула вам пару новых идей и тем для изучения. Спасибо за внимание. Удачи вам в ваших проектах! .NET Разработчик