ThinkingHome.Core.Plugins
Этот пакет содержит классы, реализующие базовые возможности плагинов. Если вы хотите написать новый плагин, вам нужно подключить этот пакет в свой проект.
Создайте в своем проекте новый класс, унаследованный от ThinkingHome.Core.Plugins.PluginBase
. PluginBase
- это абстрактный класс, который содержит общую функциональность для всех плагинов: логирование, настройки, локализация, взаимодействие с другими плагинами.
using Microsoft.Extensions.Logging;
using ThinkingHome.Core.Plugins;
...
public class MyPlugin : PluginBase
{
public override void InitPlugin()
{
Logger.LogInformation("my plugin: init");
}
}
Чтобы подключить сборку с плагином в систему, добавьте её название в список assemblies
в файле appsettings.json.
При старте система автоматически найдет все доступные плагины внутри перечисленных сборок и создаст экземпляр каждого из них.
В базовом классе PluginBase
описаны виртуальные методы InitPlugin
, StartPlugin
и StopPlugin
.
Они будут вызваны автоматически на нужных этапах работы плагина. Вы можете переопределить их и добавить туда собственную логику.
Метод InitPlugin
вызывается у каждого плагина при старте приложения. Это подходящее место, чтобы задать начальное состояние плагина.
public virtual void InitPlugin() { ... }
Метод StartPlugin
вызывается когда все плагины проинициализированы. Здесь можно подписаться на события других плагинов или запустить нужные дочерние потоки.
public virtual void StartPlugin() { ... }
Метод StopPlugin
вызывается при остановке приложения. Здесь можно сохранить несохраненные рабочие данные и освободить занятые ресурсы.
public virtual void StopPlugin() { ... }
Каждый плагин может обращаться к экземплярам других плагинов, вызывать их открытые (public) методы и подписываться на события.
Чтобы в своем плагине получить доступ к экземпляру другого плагина, укажите его в списке параметров конструктора.
public class MyPlugin : PluginBase
{
private readonly OtherPlugin otherPlugin;
public PluginBase(OtherPlugin otherPlugin) {
// сохраняем полученный экземпляр плагина в поле своего плагина
this.otherPlugin = otherPlugin;
}
public override void InitPlugin()
{
otherPlugin.ExecuteSomeMethod();
}
}
Также поддерживаются параметры первичного конструктора.
public class MyPlugin(OtherPlugin otherPlugin) : PluginBase
{
public override void InitPlugin()
{
otherPlugin.ExecuteSomeMethod();
}
}
Плагины могут подписываться на события друг друга. Например, плагин "будильник" может подписаться на событие "срабатывание таймера" плагина "таймер" и, если наступило нужное время, подать звуковой сигнал.
Плагин, который генерирует событие, в начале своей работы должен найти и запомнить все доступные обработчики этого события - методы других плагинов.
Чтобы отметить метод плагина как обработчик события, удобно использовать атрибуты. С помошью атрибута можно задать дополнительные параметры для обработчика события.
Чтобы плагин, генерирующий событие, мог легко найти методы других плагинов, отмеченных заданным атрибутом,
он может использовать метод расширения FindMethods
, для класса PluginBase
.
(TAttr Meta, TDelegate Method)[] FindMethods<TAttr, TDelegate>(this PluginBase plugin)
В метод FindMethods
нужно передать тип атрибута, который нужно найти, и делегат, определяющий сигнатуру методов.
На выходе вы получите массив, каждый элемент которого имеет поля:
Method
– ссылка на найденный обработчик события, отмеченный атрибутомMeta
– атрибут, которым отмечен обработчик события
// плагин, который обрабатывает событие
public class Plugin1: PluginBase
{
[MyAttribute]
public void OnMyEvent()
{
// обработка события
}
}
// плагин, который генерирует событие
public class Plugin2: PluginBase
{
private Action[] handlers;
// инициализация
public override void InitPlugin()
{
// ищем все обработчики и запоминаем в поле плагина
handlers = Context.GetAllPlugins() // получаем список плагинов
.SelectMany(p => p.FindMethods<MyAttribute, Action>()) // ищем все обработчики во всех плагинах
.Select(obj => obj.Method) // достаем метод из поля Method
.ToArray();
}
public void TestMethod()
{
// когда нужно сгенерировать событие,
// вызываем найденные обработчики
foreach(var handler in handlers)
{
handler();
}
}
}
Внутри обработчика события может произойти ошибка и, если плагин корректно её не обработает, его работа прервется и все последующие обработчики не будут выполнены.
Для безопасного вызова обработчиков событий в базовом классе плагина есть метод SafeInvoke
. В нем есть проверка обработчика на равенство null
и обработка исключений с записью в лог информации о них.
SafeInvoke(handlers, h => h(task.TaskId), true);
Параметры:
- Обработчик события (делегат) или коллекция обработчиков.
- Действие, которое нужно выполнить для каждого обработчика (например, вызвать его с заданными аргументами).
Для асинхронного параллельного выполнения обработчиков используйте метод SafeInvokeAsync
с аналогичными параметрами.
Плагинам доступны инструменты для логирования (используется пакет Microsoft.Extensions.Logging).
В базовом классе PluginBase
есть свойство Logger
(Microsoft.Extensions.Logging.ILogger
). Используйте его методы для записи в лог.
using Microsoft.Extensions.Logging;
public class MyPlugin: PluginBase
{
public override void InitPlugin()
{
Logger.LogInformation("Hello world!");
}
}
Для записи логируемой информации в конкретное место назначения (в консоль, файл, базу данных и др.)
используется библиотека Serilog. Её параметры находятся в файле с настройками
системы appsettings.json. По умолчанию настроен вывод информации в консоль и сохранение
в текстовый файл в папке logs
(отдельный файл для каждого плагина). Узнать подробнее о настройках библиотеки Serilog вы можете
в документации.
В систему уже встроены инструменты для работы со строками на нескольких языках. Вы можете разместить переводы для строк в ресурсах плагина (EmbeddedResource) и во время его работы он автоматически получит строки на нужном языке. Язык выбирается в настройках приложения. Все стандартные плагины поддерживают два языка: русский и английский.
Для хранения текстов на нескольких языках создайте в корне проекта папку Lang
и добавьте в нее
файлы ресурсов (.resx). Для каджого плагина создайте один главный .resx
файл (для текстов на английском языке)
и по одному файлу для каждого дополнительного языка (например, для русского).
Названия файлов должны совпадать с названием плагина. Файлы для дополнительных языков должный иметь суффикс, соответствующий коду их языка.
├─ Lang
│ ├─ MyPlugin.resx // тексты на английском
│ ├─ MyPlugin.ru-RU.resx // тексты на русском
│ └─ MyPlugin.uk-UA.resx // тексты на украинском
└─ MyPlugin.cs
Каждый файл .resx
хранит набор пар ключ-значение: значение – это текст на нужном языке, а ключ – идентификатор,
по которому можно ссылаться на этот текст в коде.
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="hello" xml:space="preserve">
<value>Hello!</value>
</data>
<data name="bye" xml:space="preserve">
<value>Bye!</value>
</data>
...
</root>
Для работы с переводами в каждом плагине доступно свойство StringLocalizer
. Объект, который в нем находится, реализует
интерфейс IStringLocalizer
из пакета Microsoft.Extensions.Localization.
Чтобы получить перевод для заданного ключа, вызовите метод GetString
или обратитесь по индексу.
// результат выполнения этих строк - одинаков
var translation1 = StringLocalizer.GetString("hello");
var translation2 = StringLocalizer["hello"];
Чтобы получить список всех переводов плагина, используйте метод GetAllStrings
.
foreach (LocalizedString str in StringLocalizer.GetAllStrings())
{
Logger.LogInformation($"{str.Name}: {str.Value} ({str.SearchedLocation})");
}
Все настройки системы хранятся в конфигурационном файле appsettings.json
, который находится в корневой папке приложения.
{
"culture": "ru-RU",
"assemblies": [ ... ],
"Serilog": { ... },
"plugins": {
"Namespace1.Plugin1": { ... },
"Namespace2.Plugin2": { ... }
}
}
- Поле
culture
(строка) задает используемый язык. - Поле
assemblies
(массив строк) задает список сборок, в которых нужно искать доступные плагины при старте приложения. - Раздел
Serilog
(объект) задает настройки логирования. Описание параметров смотрите в документации библиотеки Serilog. - Раздел
plugins
(объект) содержит настройки плагинов.
Выше было сказано, что настройки плагинов хранятся в разделе plugins
. Каждый ключ раздела - это название класса плагина, включая пространство имен.
Значение для каждого ключа - это параметры плагина. Названия параметров смотрите в описаниях плагинов.
В каждом плагине доступно поле Configuration
, определенное в базовом классе PluginBase
. Его значение реализует интерфейс
IConfigurationSection
из пакета Microsoft.Extensions.Configuration.
Через него плагин может получить значения своих настроек.
public class MyPlugin: PluginBase
{
public override void InitPlugin()
{
string myParameter = Configuration["parameterName"];
// ...
}
}
Вы можете передать дополнительные параметры конфигурации через переменные окружения с префиксом THINKINGHOME_
. В качестве иерархического разделителя используйте __
.
Например, чтобы передать параметр plugins:ThinkingHome.Plugins.Mail.MailPlugin:fromMail
укажите значение для переменной окружения THINKINGHOME_plugins__ThinkingHome.Plugins.Mail.MailPlugin__fromMail