Архитектура плагинов C # с разделением интерфейсов между плагинами - PullRequest
16 голосов
/ 06 мая 2009

Я поделил свою проблему на короткую и длинную версию для людей, у которых мало времени.

Короткая версия:

Мне нужна архитектура для системы с плагинами провайдера и потребителя. Поставщики должны внедрить интерфейс 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 или текущем домене приложений: Обнаружена сборка: Datamex.Projects.Polaris.Testing.Providers, версия = 1.0.0.0, культура = нейтральная, PublicKeyToken = 2813de212e2efcd3 Обнаружена сборка: Datamex.Projects.Polaris.Testing.Consumers, версия = 1.0.0.0, культура = нейтральная, PublicKeyToken = ea5901de8cdcb258

Поскольку Потребитель использует Поставщика, от Потребителя была сделана ссылка на Поставщика. Теперь событие AssemblyResolve сработало снова, заявив, что ему нужен следующий файл: AssemblyName = Datamex.Projects.Polaris.Testing.Providers, версия = 1.0.0.0, культура = нейтральная, PublicKeyToken = 2813de212e2efcd3

Мои вопросы: Почему это? Этот файл уже загружен правильно? Почему приведение из IProvider к какому-либо интерфейсу, который, как я знаю, реализуется, невозможно? Возможно, это связано с тем, что исполняющая программа сама не знает этого интерфейса, но не может ли она быть загружена динамически?

Моя конечная цель: Потребительские плагины спрашивают у ServiceProcessor, какие у него есть провайдеры, которые реализуют интерфейс x. Провайдеры могут быть приведены к этому интерфейсу x без выполнения сборки, зная об интерфейсе x.

Кто-нибудь, кто может помочь?

Заранее спасибо, Erik

Ответы [ 5 ]

15 голосов
/ 06 мая 2009

Я просто попытался воссоздать ваше решение как можно лучше, и у меня нет таких проблем. (Внимание! За этим следует множество примеров кода ...)

Первый проект - это приложение, которое содержит один класс:

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 и вызывает Initialize для них. Внутри инициализации потребитель может запросить поставщиков из загрузчика, и в случае этого кода создается TestProvider и возвращается. Все это работает для меня без какого-либо сложного контроля загрузки сборок.

4 голосов
/ 06 мая 2009

Он все еще находится в разработке, но звучит как идеальный вариант использования MEF (для включения в .Net 4) и используется внутри VS2010.

MEF представляет простое решение для проблема расширяемости во время выполнения. До тех пор Теперь любое приложение, которое хотел поддерживать модель плагина, необходимую для создать свою собственную инфраструктуру из царапина. Эти плагины часто бывают для конкретного приложения и не может быть повторно используется через несколько Реализации.

Предварительные просмотры уже доступны на http://www.codeplex.com/MEF

Блог 1011 * блока Глен также может быть полезен.

2 голосов
/ 06 мая 2009

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

Плагины в C # Basic Tutorial:

http://www.codeproject.com/KB/cs/pluginsincsharp.aspx

Последующая статья с включенной универсальной библиотекой диспетчера подключаемых модулей:

http://www.codeproject.com/KB/cs/ExtensionManagerLibrary.aspx

0 голосов
/ 06 мая 2009

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

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

0 голосов
/ 06 мая 2009

Если вы задаетесь вопросом, как две несвязанные сборки могут использовать один и тот же интерфейс, ответ «вы не можете». Решение состоит в том, чтобы включить интерфейс во все сборки, возможно, в dll, на который могут ссылаться создатели плагинов, и в вашей загрузочной сборке.

...