Архитектура плагина С# с интерфейсами разделяет между плагинами
Я разделил свою проблему на короткую и длинную версию для людей, у которых мало времени.
Краткая версия:
Мне нужна архитектура для системы с плагинами поставщиков и потребителей.
Поставщики должны внедрять межсетевой IProvider, и потребители должны внедрить IConsumer.
Исполнительному приложению следует знать только IProvider и IConsumer.
Потребительская реализация может запросить исполняющую сборку (с помощью ServiceProcessor), которые провайдеры реализуют InterfaceX и получают список обратно.
Эти объекты IProvider должны быть переданы в InterfaceX (у потребителя), чтобы иметь возможность подключить пользователя к некоторым событиям, которые определяет InterfaceX. Это не удастся, потому что исполняющая сборка каким-то образом не знает этого типа InterfaceX (сбой при отказе). Решение заключалось бы в том, чтобы включить InterfaceX в некоторую сборку, которая включает как плагины, так и исполняемую ссылку на сборку, но это должно означать перекомпиляцию для каждой новой пары поставщиков/потребителей и крайне нежелательно.
Любые предложения?
Длинная версия:
Я разрабатываю некий общий сервис, который будет использовать плагины для достижения более высокого уровня повторного использования. Услуга состоит из какой-либо реализации шаблона Observer с использованием Провайдеров и Потребителей. Как поставщики, так и потребители должны быть плагинами для основного приложения. Позвольте мне сначала объяснить, как работает сервис, перечисляя проекты, которые у меня есть в моем решении.
Проект A: проект службы Windows для размещения всех плагинов и основных функций. Проект TestGUI Windows Forms используется для упрощения отладки. Экземпляр класса ServiceProcessor из Project B выполняет связанные с плагинами вещи. Вложенные папки "Потребители" и "Поставщики" этого проекта содержат вложенные папки, в которых каждая подпапка содержит соответственно пользовательский или провайдер-плагин.
Проект B: библиотека классов, содержащая класс ServiceProcessor (который выполняет загрузку и отправку плагинов между плагинами и т.д.), IConsumer и IProvider.
Проект C: библиотека классов, связанная с проектом B, состоящая из TestConsumer (реализация IConsumer) и TestProvider (реализация IProvider). Дополнительный интерфейс (ITest, сам полученный из IProvider) реализуется TestProvider.
Цель состоит в том, что плагин Consumer может запросить ServiceProcessor, у которого есть Провайдеры (реализующие хотя бы IProvider)). Возвращенные объекты IProvider должны быть отправлены на другой интерфейс, который он реализует (ITest) в реализации IConsumer, чтобы потребитель мог привязать обработчики событий к событиям ITest.
Когда запускается проект A, загружаются подпапки, содержащие плагины потребителя и поставщика. Ниже приведены некоторые проблемы, с которыми я столкнулся до сих пор, и попытался решить.
Интерфейс ITest использовался для реализации в Project C, поскольку это относится только к методам и событиям, которые знают TestProvider и TestConsumer. Общая идея состоит в том, чтобы сохранить простой проект и не знать, что делают плагины друг с другом.
С ITest в проекте C есть и код в методе Initialize TestConsumer, который передает IProvider в ITest (это не может не произойти в одной библиотеке классов, когда объект, реализующий ITest, известен как объект IConsumer), неверное литье возникла ошибка. Эта ошибка может быть решена путем размещения интерфейса ITest в проекте B, на который ссылается также проект A. Это очень нежелательно, поскольку нам нужно перекомпилировать проект A при создании нового интерфейса.
Я пытался помещать ITest в единую библиотеку классов, на которую ссылается только проект C, поскольку только провайдер и потребитель должны знать об этом интерфейсе, но безуспешно: при загрузке плагина в CLR указано, что указанный проект не может быть найденным. Это можно решить, подключившись к событию AssemblyResolve текущего AppDomain, но так или иначе это кажется нежелательным. ITest снова вернулся к проекту B.
Я попытался разделить проект C на отдельный проект для потребителя и поставщика, и оба загружают сборки, которые сами работают хорошо: обе сборки находятся в коллекции Assemblies или в текущем AppDomain:
Сборка найдена: Datamex.Projects.Polaris.Testing.Providers, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = 2813de212e2efcd3
Сборка найдена: Datamex.Projects.Polaris.Testing.Consumers, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = ea5901de8cdcb258
Поскольку потребитель использует Поставщика, ссылка была сделана от Потребителя к Поставщику. Теперь событие AssemblyResolve возобновилось, указав, что ему нужен следующий файл:
AssemblyName = Datamex.Projects.Polaris.Testing.Providers, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = 2813de212e2efcd3
Мои вопросы:
Почему это? Этот файл уже загружен правильно?
Почему бросок от IProvider к некоторому интерфейсу, который я знаю, он реализует невозможно? Вероятно, это связано с тем, что сама исполняющая программа не знает этого интерфейса, но не может ли это быть загружено динамически?
Моя конечная цель:
Потребительские плагины запрашивают у ServiceProcessor, какие у него есть Поставщики, которые реализуют интерфейс x. Поставщики могут быть отправлены на этот интерфейс x, не выполняя сборку, которая знает интерфейс x.
Кто-нибудь, кто может помочь?
Спасибо заранее,
Erik
Ответы
Ответ 1
Я просто пытался воссоздать ваше решение как можно лучше, и у меня нет таких проблем. (Предупреждение, много примеров кода следуют....)
Первый проект - это приложение, оно содержит один класс:
public class PluginLoader : ILoader
{
private List<Type> _providers = new List<Type>();
public PluginLoader()
{
LoadProviders();
LoadConsumers();
}
public IProvider RequestProvider(Type providerType)
{
foreach(Type t in _providers)
{
if (t.GetInterfaces().Contains(providerType))
{
return (IProvider)Activator.CreateInstance(t);
}
}
return null;
}
private void LoadProviders()
{
DirectoryInfo di = new DirectoryInfo(PluginSearchPath);
FileInfo[] assemblies = di.GetFiles("*.dll");
foreach (FileInfo assembly in assemblies)
{
Assembly a = Assembly.LoadFrom(assembly.FullName);
foreach (Type type in a.GetTypes())
{
if (type.GetInterfaces().Contains(typeof(IProvider)))
{
_providers.Add(type);
}
}
}
}
private void LoadConsumers()
{
DirectoryInfo di = new DirectoryInfo(PluginSearchPath);
FileInfo[] assemblies = di.GetFiles("*.dll");
foreach (FileInfo assembly in assemblies)
{
Assembly a = Assembly.LoadFrom(assembly.FullName);
foreach (Type type in a.GetTypes())
{
if (type.GetInterfaces().Contains(typeof(IConsumer)))
{
IConsumer consumer = (IConsumer)Activator.CreateInstance(type);
consumer.Initialize(this);
}
}
}
}
Очевидно, что это можно убрать чрезвычайно.
Следующий проект - это общая библиотека, которая содержит следующие три интерфейса:
public interface ILoader
{
IProvider RequestProvider(Type providerType);
}
public interface IConsumer
{
void Initialize(ILoader loader);
}
public interface IProvider
{
}
Наконец, есть проект плагина с этими классами:
public interface ITest : IProvider
{
}
public class TestConsumer : IConsumer
{
public void Initialize(ILoader loader)
{
ITest tester = (ITest)loader.RequestProvider(typeof (ITest));
}
}
public class TestProvider : ITest
{
}
Как приложение, так и проекты плагина ссылаются на общий проект, а плагин dll копируется в каталог поиска для приложения - но они не ссылаются друг на друга.
Когда модуль PluginLoader создан, он обнаруживает, что все IProviders затем создают все IConsumers и называет Инициализировать их. Внутри инициализации потребитель может запросить поставщиков у загрузчика, а в случае этого кода будет создан и возвращен TestProvider. Все это работает для меня без какого-либо контроля над загрузкой сборок.
Ответ 2
Он все еще находится в разработке, но звучит как идеальная утилита для MEF (для включения в .Net 4) и используется внутри VS2010.
MEF представляет собой простое решение для проблема расширяемости среды выполнения. До теперь любое приложение, которое поддержка модели плагина, необходимой для создать свою собственную инфраструктуру из царапина. Эти плагины часто бывали специфичный для приложения и не может быть повторно использовать несколько реализации.
Предварительный просмотр уже доступен на http://www.codeplex.com/MEF
blog блока Glen также может быть полезным.
Ответ 3
Вы можете найти мои статьи полезными, чтобы увидеть рабочий пример структуры плагина и как эти проблемы решаются путем создания общей сборки, содержащей интерфейсы:
Плагины в базовом учебном пособии С#:
http://www.codeproject.com/KB/cs/pluginsincsharp.aspx
Последующая статья с библиотекой диспетчера подключаемых модулей с поддержкой Generics:
http://www.codeproject.com/KB/cs/ExtensionManagerLibrary.aspx
Ответ 4
Если вы сомневаетесь, как две несвязанные сборки имеют один и тот же интерфейс, ответ "вы не можете". Решение состоит в том, чтобы включить интерфейс во все сборки, возможно, в dll, которые могут ссылаться разработчиками плагинов, и в вашей сборке.
Ответ 5
Я сделал что-то похожее на то, что вы пытаетесь сделать, и пока у меня были сборки в месте, где загрузчик автоматически смотрелся, я не сталкивался с какими-либо проблемами.
Вы пытались поместить все свои сборки в подкаталог под которым находится exe? Я не могу вспомнить данные сейчас, но есть список шагов, которые описаны в точности, где и в каком порядке загрузчик ищет сборки/типы.