3

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

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

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

public interface IPlugin
{
    void Loaded();
}

Вот такой класс придумал в этой библиотеке. Теперь я могу спокойно ее подключать к плагину и наследоваться.

public class Test : IPlugin
{
    #region Implementation of IPlugin

    public void Loaded() => Console.WriteLine( "Hello World" );

    #endregion
}

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

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

Вот код которым я пытаюсь что-то наколдовать:

private static void Main ( string[] args )
    {
        var pluginsPath = $"{Directory.GetCurrentDirectory()}\\Plugins";
        var files = Directory.GetFiles( pluginsPath , "*.dll" );

        foreach ( var file in files )
        {
            Console.WriteLine( $"Trying load: {file}" );
            Assembly assembly;

            try { assembly = Assembly.Load( file ); }
            catch ( Exception ex )
            {
                Console.WriteLine( ex.Message );
                continue;
            }

            var types =
                assembly.GetTypes().Where( type => type.IsClass && type.GetInterface( nameof( IPlugin ) ) != null );

            foreach ( var type in types )
            {
                var plugin = Activator.CreateInstance( type ) as IPlugin;

                if ( plugin == null )
                {
                    Console.WriteLine( "Null" );
                    continue;
                }

                plugin.Loaded();
            }
        }
    }

Падает еще на первом try с сообщением:

Необработанное исключение типа "System.IO.FileLoadException" в mscorlib.dll Дополнительные сведения: Не удалось загрузить файл или сборку "C:\Users\anweledig\Documents\Visual Studio 2015\Projects\Anweledig\ConsoleApplication\bin\Debug\Plugins\TestPlugin.dll" либо одну из их зависимостей. Данное имя сборки или база кода недействительны. (Исключение из HRESULT: 0x80131047)

anweledig
  • 835
  • 1
  • 8
  • 24
  • 2
    Не изобретайте велосипед, используйте MEF. – VladD Jun 04 '15 at 08:47
  • Если хотите всё же грузить всё руками, покажите код и Fusion Log из exception'а. – VladD Jun 04 '15 at 08:48
  • @VladD человек же делает плагины в целях изучения рефлекшена. а не учит рефлекшн в целях написания плагина. mef не поможет :) –  Jun 04 '15 at 08:48
  • @PashaPash: Тогда Fusion Log нужен. Может, плагин лежит в непонятном месте. И рефлексия не поможет, если плагинная система правильная (ну кроме первоначальной загрузки какого-нибудь IPlugin). – VladD Jun 04 '15 at 08:50
  • public abstract void Unload(); — это у вас не получится без грязных трюков. Выгрузить загруженную сборку нельзя (разве что только с AppDomain'ом). – VladD Jun 04 '15 at 08:51
  • 1
    И почему абстрактный класс, а не интерфейс? Если вы пришли из C++, это не одно и то же. – VladD Jun 04 '15 at 08:53
  • @VladD Unload это как обработчик события задумывалось, нужно было написать OnUnload, смысл изначально даже заключался не в том чтоб выгрузить сборку, а чтоб выгрузить плагин из листа. По поводу абстрактного класса, у меня там была сначала некоторая реализация, я в принципе вижу разницу и плюсы интерфейсов, но суть это никак не поменяет, не загружается это дело. Сейчас попробую добавить больше информации. – anweledig Jun 04 '15 at 09:08

1 Answers1

3

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

Проблема может быть только в случае, если библиотека лежит не в той же папке, в которой лежит ваше приложение, и вы при этом загружаете ее через Assembly.LoadFrom. Чтобы это обойти, вам нужно указать рантайму что зависимости надо искать в папке плагина, а не в папке вашего приложения. Тогда можно будет использовать обычный Assembly.Load и заодно заработает станадартный механизм загрузки сборок.

Для этого можно использовать создание аппдомена с указанием AppDomainSetup.ApplicationBase

Т.е. схема примерно такая:

У вас есть базовая dll для плагинов - MyApp.SDK, в ней базовый класс (а лучше - интерфейс):

public abstract class Plugin
{
    public abstract void Loaded();
    public abstract void Unload();
}

в ней же код загрузки и вызова плагина (для простоты)

public static class PluginInvoker
{
    public static void InvokePlugin()
    {
        var pluginAssembly = Assembly.Load("SomePlugin");
        var pluginType = pluginAssembly.GetTypes().Where(t => typeof(Plugin).IsAssignableFrom(t)).Single();
        Plugin plugin = (Plugin)pluginType.GetConstructor(new Type[0]).Invoke(null);
        plugin.Loaded();
    }
}

есть реализация этого плагина в SomePlugin.dll:

public class Test: MyApp.SDK.Plugin
{
    public override void Loaded()
    {
        Console.WriteLine("Loaded");
    }

    public override void Unload()
    {
        Console.WriteLine("Unloaded");
    }
}

в SomePlugin есть референс на MyApp.SDK.

И есть главное приложение, в котором есть ссылка на MyApp.SDK, но нет ссылки на SomePlugin:

static void Main(string[] args)
{
    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationBase = @"C:\Projects\MyApp\SomePlugin\bin\Debug\";

    AppDomain pluginDomain = AppDomain.CreateDomain("plugin", null, setup);
    pluginDomain.DoCallBack(PluginInvoker.InvokePlugin);
}

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

Если плагины подгружаются из подпапки приложения (например, из Plugins), то в вместо AppDomainSetup.ApplicationBase можно использовать AppDomainSetup.PrivateBinPath.

Если же при этом плагины вообще не планируется выгружать, и все плагины лежат в одной папке (без подпапок на каждый из плагинов) - то можно обойтись без отдельного AppDomain, просто дописав путь к папке с плагинами в секцию app.config/<probing>.

код примера на github: https://github.com/PashaPash/PluginLoadSample

  • Спасибо большое. Буду разбираться. – anweledig Jun 04 '15 at 09:21
  • 2
    @anweledig держите рабочий пример: https://github.com/PashaPash/PluginLoadSample –  Jun 04 '15 at 09:31
  • @PashaPash: А почему не старый добрый AssmeblyResolve? Всё же работа через границу домена — не самая быстрая штука. – VladD Jun 04 '15 at 09:59
  • @VladD AssemblyResolve потянет за собой LoadFrom - а это потенциально приключения с контекстом загрузки, ифы на системные сборки и прочие костыли. Ну и автор все равно захочет плагины выгружать рано или подзно, так что домен нужен будет. –  Jun 04 '15 at 10:05
  • @PashaPash: Вы уверены насчёт системных сборок? Майкрософтовский официальный пример не делает if'ы. (И вообще, рекомендует app.config.) – VladD Jun 04 '15 at 11:16
  • @PashaPash: Так что наверное для плагинов проще всего app.config/<probing> – VladD Jun 04 '15 at 11:18
  • @VladD да, или PrivateBinPath - но только если плагины лежат в подпапке приложения. В общем случае - ApplicationBase. Когда я писал ответ - дополнения с GetCurrentDirectory еще не было, да и в семпле у меня загрузка из внешней папки. –  Jun 04 '15 at 11:36
  • @VladD дополнил –  Jun 04 '15 at 11:42