gitverse new year логотип

extensibility

Форк
0
Форк от sidristij/extensibility

README.md

Extensibility

Библиотека, позволяющая расширять ASPNET приложения условно внешним функционалом.

Возможности:

  1. Хост может загружать плагины из папки;
  2. У хоста и плагина свои, изолированные DI контейнеры. Связь между ними идёт через JointConfiguration. Плагин запрашивает у хоста реализации необходимых интерфейсов и отдаёт свои реализации других общих интерфейсов через реализацию метода RegisterCrossBoundary;
  3. Если и хост и плагин имеют общий nuget пакет с общими типами интерфейсов, то плагин может предоставить хосту безопасную реализацию этого типа;
  4. При регистрации нового плагина произойдёт перекрытие реализации интерфейса и те, кто её запросят получат новую реализацию;
  5. Классы хоста затягивают типы плагина через DI хоста, т.е. прозрачно, через параметры конструктора;
  6. Классы плагина затягивают типы хоста через DI хоста, т.е. прозрачно, через параметры конструктора;
  7. "представители" классов плагина в хосте -- это обёртки над экземплярами типов, которые логгируют исключения и трассируют вызовы в уровне логгирования TRACE;
  8. При смене пути плагинов на другой путь все плагины перезагружаются в нового места на "горячую";
  9. При перезаписи папки конкретного плагина тот будет перезагружен с диска;
  10. При корректной реализации (несохранении ссылок на объекты плагина в хосте) плагин старой версии будет отгружен из памяти. Иначе будут сосуществовать обе версии плагина. Даже если они будут основаны на библиотеках различных версий;
  11. Если необходим частичный импорт интерфейса, на стороне плагина создаётся дубль-интерфейс с описанием тольео необходимых методов и осуществляется duck-typed импорт.

Подключение папки с компилируемой библиотекой в контейнер

1. Публикация папки

Предоставить сетевой доступ к папке с собранным плагином /Debug/ и дать необходимое сетевое имя. Нарпимер,

plugin

2. Подключение к папке

Зайдите по SSH в контейнер и запустите команду монтирования папки:

mount -t cifs -o username=<user_name> //<host-ip>/plugin /mnt/ Password for <user_name>@//<host-ip>/tmp: **************

Документация

На основе чего работает

Функционал плагинов основан на функционале .NET Core

AssemblyLoadContext
, которые были внесены в ядро на замену
AppDomains
.

AssemblyLoadContext
может быть создан с
isCollectible=true или =false
.
true
означает, что сборки, загружаемые в
AssemblyLoadContext
могут быть отгружены в любой момент. При этом GC отгрузит сборки тогда и только тогда, когда последняя ссылка на объект типа сборки, загруженной в такой контекст потеряет последнюю ссылку на себя. Т.е. условия для выгрузки сборок из памяти два. Первое условие -- была запрошена выгрузка контекста
AssemblyLoadContext.Unload()
. Второе условие -- более никто не ссылается на объекты типов этого контекста.

Общий аглоритм работы

Инициализация

На стороне сервиса

Для того чтобы создать точку загрузки плагинов, при настройке

HostBuilder
достаточно добавить соотвесттвующий сервис вызовом методом расширения
AddPluginsFromFolder
типа
AddPluginsFromFolderEx
из пакета
DevTools.Extensibility.PluginHostSide
:

static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseUnityServiceProvider()
.ConfigureServices((hostContext, services) =>
{
services
.AddLogging()
.AddPluginsFromFolder(hostContext);
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());

Этот вызов делает вообще всю инициализацию и дальнейшая настройка производится от IOptionMonitor

На стороне плагина

Инициализация на стороне плагина доступна двумя путями.

DevTools.Extensibility.EasyPlugin

Первый путь -- базироваться на использовании

Extensibility.SimpleExtension
, в котором уже есть настроенный DI контейнер. Прощу заметить, что т.к. DI контейнеры плагина и хоста изолированы, то нет нужды беспокоиться, что контенер не той версии или не той библиотеки, что испльзуется в хосте (сервисе).

Далее создаётся файл, который будет содержать класс связывания плагина и хоста:

[ExtensionVersion("1.1.0.0")]
[ExtensionName("EasyPlugin")]
public class EasyServiceExtension : UnityBasedExtensionBase
{
protected override void InjectAdditionalDependencies(UnityContainer containerService)
{
// host awaits for two types from plugins. We can propose both
containerService.RegisterType<ISomeAdapter, CardsAdapter>();
containerService.RegisterType<IAnotherAdapter, AnotherCardsAdapter>();
containerService.RegisterSingleton<ILoggerFactory, LoggerFactory>();
containerService.RegisterSingleton(typeof(ILogger<>), typeof(Logger<>));
}
protected override void RegisterCrossBoundary(IConfiguration configuration, IJointsConfiguration joints)
{
// request to publish local types to host
joints.ExportToHost<ISomeAdapter>();
joints.ExportToHost<IAnotherAdapter>();
// request to take host type instance from host
joints.ImportFromHost<IOptions<SomeAdapterOptions>>();
}
protected override void ConfigureServices(IConfiguration configuration, ExtensionHostedServices hostedServices)
{
}
}

Далее переопределив метод

InjectAdditionalDependencies
плагин инициализирует свой контейнер. У плагина может быть сколь угодно сложная внутренняя структура и все типы этой структуры могут быть помещены в DI плагина. DI хоста при этом о них не узнает.

Переопределение метода

RegisterCrossBoundary
задаёт три списка типов:

  1. ImportFromHost
    . На импорт из хоста в плагин -- используется для размещения объектов хоста в DI плагина.
  2. ExportToHost
    . Список экспорта объектов из плагина в хост или другие плагины. Он опрадаляет список объектов плагина, доступных для использования вовне: как в хосте, так и в других плагинах.
  3. DuckTypedImportFromHost
    . Список импорта через утиную типизацию. Доступен для частичного импорта интерфейсов хоста (см. отдельный раздел).
Вызовы хост <-> плагин

Для того чтобы хост вызвал плагин по некоторому интерфейсу, должны произойти три события:

Должна быть библиотека, которая загружается И в хост И в плагин. Соотетственно, любые зависимости этой библиотеки должны быть на одной версии как в хосте, так и в плагине. Например,

DevTools.Core
. Поэтому в идеале библиотека не должна тянуть зависимости. Она должна содержать контракт взаимодействия и всё. Например, в демо-проекте определён проект
HostSide\CardsAdapterPlugin.Abstracts.csproj
, который содержит интерфейс:

public interface ISomeAdapter
{
string Adapt(string source);
}

И настройки плагина:

public class SomeAdapterOptions
{
public string SomeString { get; set; }
}

Теперь хост может определить настройки как читаемые из IOptionMonitor либо вообще задать статичные, а плагин -- получить их.

Второй шаг -- в проекте плагина реализовать интерфейс

IVisirCardsAdapter
:

public class CardsAdapter : ISomeAdapter
{
private readonly IOptions<SomeAdapterOptions> _opts;
private readonly string _unityVersion;
private readonly ILogger _logger;
public CardsAdapter(
IOptions<SomeAdapterOptions> opts,
ILoggerService loggerService)
{
_opts = opts;
_unityVersion = typeof(Unity.UnityContainer).Assembly.GetName().Version?.ToString();
_logger = loggerService.GetLogger(nameof(CardsAdapter));
}
public string Adapt(string source)
{
var msg = $"CardsAdapter: {_opts.Value.SomeString} -> {source}, Unity ver. = {_unityVersion}";
_logger.LogInfo(msg);
return msg;
}
}

Заметьте, класс описывается как любой класс, автоинициализируемый через DI. Т.к. подсистема связывания получала типы

IOptions\<SomeAdapterOptions\>
и
ILoggerService
посредством импорта из хоста, их экземпляры будут получены корректно.

Третий шаг -- использования в хосте также происходит стандартным образом:

[ApiController]
public class DataController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly ISomeAdapter _plugin;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
ISomeAdapter plugin)
{
_logger = logger;
_plugin = plugin;
}
public WeatherForecast Get()
{
return new WeatherForecast
{
Summary = _plugin.Adapt("{{HOST SIDE}}")
};
}
}

Импорт утиной типизацией

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

Для этого необходимо объявить интерфейс, который содержит часть методов другого интерфейса:

[PluginBridgeTo(typeof(IOptions<SomeAdapterOptions>))] // либо
[PluginBridgeTo("CardsAdapterPlugin.Abstracts.SomeAdapterOptions"))] // либо
public interface IDuckTypedOptionGetter
{
/// <summary>
/// Mapping to <see cref="IOptions{SomeAdapterOptions}.SomeString" /> method of <see cref="IOptions{SomeAdapterOptions}" />
/// </summary>
public SomeAdapterOptions Value { get; }
}

И при помощи атрибута

PluginBridgeTo
указать какой тип копируется, указав либо тип либо
FullName
типа. После чего хост при помощи Castle.DynamicProxy создаст мост до экземпляра типа, указанного в
PluginBridgeTo
и плагин сможет вызывать в нём перечисленные методы.

public class CardsAdapter : ISomeAdapter
{
private readonly IDuckTypedOptionGetter _greeting;
private readonly WrappedLogger _logger;
public TrawlToVisirCardsAdapter(
IDuckTypedOptionGetter greeting,
ILoggerService loggerService)
{
_greeting = greeting;
_logger = loggerService.GetLogger(nameof(CardsAdapter)).Wrap();
}
public string Adapt(string source)
{
return $"src: {source}, ducktyped import: {_greeting.Value.SomeString};
}
}

Описание

Библиотека для возможности динамического расширения в т.ч. упакованного в Docker container приложения

Языки

C#

Сообщить о нарушении

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.