Разрешение зависимости области видимости для объявляющей сборки

#c# #plugins #dependency-injection #autofac

#c# #Плагины #внедрение зависимости #autofac

Вопрос:

Всем доброго утра.

Краткая версия; все конкретные реализации из всех сборок, которые реализуют мой обычно определенный интерфейс IMenuItem, вводятся во все конструкторы, которые требуют IEnumerable<IMenuItem> .

Я создаю небольшое ядро приложения Windows TrayIcon, которое допускает плагины. Ядро обнаруживает все плагины в каталоге bin и создает мой контейнер Autofac.

У меня есть Core.Interfaces проект, который объявляет IMenuItem интерфейс

Каждый плагин определен в своей собственной сборке, и внутри этого плагина может быть много функций; каждая функция будет объявлять свои пункты меню.

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

Чего я хочу достичь, так это иметь общий IMenuItem интерфейс, но когда конструктор в плагине A запрашивает IEnumerable<IMenuItem> , ему должны передаваться только конкретные элементы, которые мы обнаружили в его собственной сборке.

Просто чтобы сказать, что если я объявляю интерфейс IMenuItem в каждой сборке, то все это работает просто отлично, предположительно потому, что регистрации затем помещаются в пространство имен интерфейса.

Я изо всех сил пытаюсь подобрать правильную терминологию для этого Google, но, думаю, я понимаю, что это проблема регистрации; возможно, та, которую я могу решить только во время разрешения?

Ответ №1:

Из коробки нет ничего, что делало бы это. Вам придется написать пользовательский код.

Большая часть пользовательского кода будет зависеть от того, как вы разрешаете набор плагинов. Вот несколько вариантов решения проблемы, хотя, если ни один из них не подходит для вашего текущего дизайна, возможно, вы можете использовать их, чтобы подстегнуть свое воображение для дополнительных идей.

Отказ от ответственности: Я не запускаю все это через компилятор. Это будут частичные примеры, и я могу что-то опечатать. Некоторые из них могут быть псевдокодом. YMMV.

Вариант 1: Зарегистрируйте каждый плагин в отдельной области действия

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

 var builder = new ContainerBuilder();

// register common stuff that all plugins use
builder.Register<SomethingCommon>().As<ICommonService>();
var container = builder.Build();

// iterate over the assemblies and create scopes per plugin
var pluginScopes = new List<ILifetimeScope>();
foreach(var assembly in GetThePluginAssemblies())
{
  var scope = container.BeginLifetimeScope(b =>
  {
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IPlugin))
     .AsImplementedInterfaces();
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IMenuItem))
     .AsImplementedInterfaces();
  });
  pluginScopes.Add(scope);
}
  

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

 var plugins = pluginScopes.SelectMany(s => s.Resolve<IEnumerable<IPlugin>>());
  

думаю, что именно так работает SelectMany, я всегда путаюсь. Суть в том, что вы получите сжатый список всех плагинов во всех областях.)

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

 var builder = new ContainerBuilder();

// register common stuff that all plugins use
builder.Register<SomethingCommon>().As<ICommonService>();
var container = builder.Build();

// You probably won't want to resolve things "as a tenant" from the container,
// so you'd only use the Multitenant.ApplicationContainer (for global/common stuff)
// or individual tenant scopes directly. The tenant ID strategy, ostensibly, won't
// be used, so just make a dummy one that always returns false or something.
var multiPluginContainer = new MultitenantContainer(container, SomeTenantIdentificationStrategy);

// iterate over the assemblies and create tenants per plugin
// where the tenant ID is something like the assembly name
foreach(var assembly in GetThePluginAssemblies())
{
  multiPluginContainer.ConfigureTenant(assembly.FullName, b =>
  {
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IPlugin))
     .AsImplementedInterfaces();
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IMenuItem))
     .AsImplementedInterfaces();
  });
}
  

Затем вы могли бы получить список плагинов («арендаторов») и разрешить.

 var plugins = multiPluginContainer
  .GetTenants()
  .SelectMany(k =>
    multiPluginContainer.GetTenantScope(k).Resolve<IEnumerable<IPlugin>>());
  

Вариант 2: Использовать метаданные для пометки элементов

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

Во-первых, вы могли бы зарегистрировать все пункты меню и пометить их метаданными.

 var builder = new ContainerBuilder();
// register a bunch of stuff and...
foreach(var assembly in GetThePluginAssemblies())
{
  builder.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IPlugin))
     .AsImplementedInterfaces();
  builder.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IMenuItem))
     .WithMetadata("assembly", assembly.FullName)
     .AsImplementedInterfaces();
}
  

Хорошо, теперь у вас есть все IMenuItem записи, помеченные именем сборки. Создайте модуль, который автоматически присоединяет разрешенный параметр к каждому, IPlugin таким образом, что любой IEnumerable<IMenuItem> будет выполняться вашим параметром. Это в значительной степени основано на примере модуля log4net из документации.

 public class MenuItemModule : Autofac.Module
{
  private static void OnComponentPreparing(object sender, PreparingEventArgs e)
  {
    e.Parameters = e.Parameters.Union(
      new[]
      {
        new ResolvedParameter(
            // Only provide values for IEnumerable<IMenuItem> requested
            // by IPlugin implementations
            (pi, ctx) =>
               pi.ParameterType == typeof(IEnumerable<IMenuItem>) amp;amp;
               pi.Member.DeclaringType.GetInterfaces().Any(i => i == typeof(IPlugin)),
            // Resolve the appropriately tagged menu items
            // IEnumerable<T> - get all the menu items
            // Meta<T> - you want to look at the metadata
            // Lazy<T> - don't actually construct them until you want them
            // meta.Value = Lazy<T>
            // meta.Value.Value resolves the IMenuItem
            (pi, ctx) => {
              var asmName = pi.Member.DeclaringType.Assembly.FullName;
              return ctx.Resolve<IEnumerable<Meta<Lazy<IMenuItem>>>>()
                 .Where(meta => meta.Metadata["assembly"] == asmName)
                 .Select(meta => meta.Value.Value);
            }
        ),
      });
  }

  protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
  {
    registration.Preparing  = OnComponentPreparing;
  }
}
  

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

 builder.RegisterModule<MenuItemModule>();
  

Есть и другие варианты.

Вы могли бы представить другие перестановки для этого. Отдельный контейнер для каждого плагина (что неплохая идея — хорошая изоляция плагинов). Отдельный AppDomain для каждого плагина (еще лучшая изоляция, но работа по маршалированию данных). Базовая IPlugin реализация, которая имеет логику фильтрации в этом, а не в ResolvedParameter . Атрибуты фильтра метаданных в реализациях плагинов для выполнения фильтрации.

Надеюсь, это поможет вам разблокировать.