Динамическая загрузка, эксплуатация и выгрузка сборок в .NET

Довольно часто перед разработчиком встаёт вопрос о расширении основного алгоритма однотипными задачами. Например, агрегаторы различных сервисов, которые предоставляют единый интерфейс пользователю, делая запросы сотне-другой поставщиков услуг. Задача стоит таким образом, чтобы основное ядро могло динамически загружать сборки с различными реализациями некоторого интерфейса. Никакой непосильной работы для программиста .NET здесь изначально не предвидится. Если термин «отражение» Вам известен, Вы вероятно уже хотите пройти мимо? Но в этом топике речь пойдёт не про отражение как таковое… я расскажу как это сделать наиболее «чисто». Т.е. с одним нюансом — исследуемые сборки нужно выгрузить после их эксплуатации.


Итак задача: нужно создать консольное приложение ExtensionLoader, которое при старте динамически из своего каталога загружает все библиотеки, в которых был найден класс, реализующий интерфейс IExtension:
interface IExtension
{
    String GetExtensionName();
}

После загрузки найденных библиотек приложение создаёт экземпляр для каждого найденного класса (с IExtension) и выполняет единственный метод GetExtensionName(), результат выводит в консоль.

Звучит как тестовое задание, — на самом деле так оно и было…

Один нюанс в этом задании был очень интересен: Архитектура приложения должна быть построена таким образом, чтобы была возможность выгрузки всех подгружаемых библиотек. Вот здесь и начинается самое интересное. Быстрое «гугление» не дало готовых решений. Анализ возникшей проблемы занял определённое время и я хочу поделится им… надеясь получить конструктивный отклик, да и чтобы не валялось зря…

Начнём с общей библиотеки с описанием интерфейса. Само-собой это будет отдельный проект, чтобы его можно было подключать как к расширению, так и к ядру системы не цепляя за собой все реализации этого интерфейса. Это, вроде как, очевидно. Наша библиотека, по заданию, будет содержать один интерфейс. Он уже описан в задании, поэтому не буду повторять.

Далее реализуем интерфейс IExtension. Тоже очевидно, что это будет отдельный проект, который в нашем примере будет содержать одну реализацию заданного интерфейса.
    public class Extension1 : MarshalByRefObject, IExtension
    {
        public Extension1()
        {
        }

        public string GetExtensionName()
        {
            return "Extension 1 from " + AppDomain.CurrentDomain.FriendlyName;
        }
    }


Пытливый читатель уже заметил некоторые излишества, которых он не ожидал здесь увидеть. А именно наследование от MarshalByRefObject. Фишка здесь в следующем: Если мы не отнаследуемся от MarshalByRefObject, при попытке выполнить метод с помощью отражения, сборка будет загружена в текущий домен приложения из которого осуществляется вызов метода. Следствием этого будет являться невозможность выгрузки сборки, поскольку сборки по отдельности не выгружаются (только полностью всем доменом приложения). Задание будет провалено.

Далее приведу «скучные методы», которые можно не разглядывать. Данные методы реализованы, в качестве примера, в приложении загрузчике. Их назначение сугубо утилитарно решаемой задаче.
        /// <summary>
        /// Возвращает перечисление содержащее плагины.
        /// </summary>
        /// <param name="domain">Домен, в который будут загружатся исследуемые сборки.</param>
        private static IEnumerable<IExtension> EnumerateExtensions(AppDomain domain)
        {
            IEnumerable<string> fileNames = Directory.EnumerateFiles(domain.BaseDirectory, "*.dll");
            if (fileNames != null)
            {
                foreach (string assemblyFileName in fileNames)
                {
                    foreach (string typeName in GetTypes(assemblyFileName, typeof(IExtension), domain))
                    {
                        System.Runtime.Remoting.ObjectHandle handle;
                        try
                        {
                            handle = domain.CreateInstanceFrom(assemblyFileName, typeName);
                        }
                        catch (MissingMethodException)
                        {
                            continue;
                        }
                        object obj = handle.Unwrap();
                        IExtension extension = (IExtension)obj;
                        yield return extension;
                    }
                }
            }

        }

        /// <summary>
        /// Возвращает перечисление имён классов, которые реализуют заданный интерфейс.
        /// Сборка загружаются в указанный домен.
        /// </summary>
        /// <param name="assemblyFileName">Имя файла анализируемой сборки</param>
        /// <param name="interfaceFilter">Искомый интерфейс</param>
        /// <param name="domain">Домен для загрузки сборки.</param>
        /// <returns>Перечисление полных имён классов.</returns>
        private static IEnumerable<string> GetTypes(string assemblyFileName, Type interfaceFilter, AppDomain domain)
        {
            Assembly asm = domain.Load(AssemblyName.GetAssemblyName(assemblyFileName));
            Type[] types = asm.GetTypes();
            foreach (Type type in types)
            {
                if (type.GetInterface(interfaceFilter.Name) != null)
                {
                    yield return type.FullName;
                }
            }
        }


Далее два тоже простых метода, но более важных в контексте статьи. Методы могут показаться странными за счёт того, что в них всего по одному вызову, но это всего лишь последствия упразднения ненужного для данной статьи функционала.
        /// <summary>
        /// Создаёт новый домен.
        /// </summary>
        /// <param name="path">Папка для поиска загружаемых сборок.</param>
        /// <returns>Новый домен приложения.</returns>
        static AppDomain CreateDomain(string path)
        {
            AppDomainSetup setup = new AppDomainSetup();
            setup.ApplicationBase = path;
            return AppDomain.CreateDomain("Temporary domain", null, setup);
        }

        /// <summary>
        /// Выгружает домен приложения.
        /// </summary>
        /// <param name="domain">Домен подлежащий выгрузке.</param>
        static void UnloadDomain(AppDomain domain)
        {
            AppDomain.Unload(domain);            
        }


Ну, и на конец, метод Main… который всё это «пользует»
        static void Main(string[] args)
        {
            // Создаём домен приложения, в котором будут выполняться расширения.
            AppDomain domain = CreateDomain(Directory.GetCurrentDirectory());
            try
            {
                // Получаем список экземпляров расширений.
                IEnumerable<IExtension> extensions = EnumerateExtensions(domain);
                foreach (IExtension extension in extensions)
                    // Выполняем метод расширения. Выполнение происходит в другом домене.
                    Console.WriteLine(extension.GetExtensionName());

                // Выгрузка домена приведёт к ожидаемому результату. Ни одна сборка не зависнет.
                UnloadDomain(domain);                
            }
            finally
            {
                domain = null;
                GC.Collect(2);
            }

            Console.ReadKey();
        }
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 14
  • +7
    Вот уже раза три сталкивался с этим велосипедом…
    Коллеги всегда настаивали мол давай напишем «правильно» — то есть через AppDomain.
    И тут начинают вылезать подводные камни пачками:

    1. Код выполняемый в созданном домене имеет урезанные права (Code Access Security, что там в .Net 4.0 поменяли — не вкурсе).
    Как правило при создании домена приходится настраивать ему права в Full trust.

    2. У вас объект унаследован от MarshalByRefObject. А как GC созданного домена узнает, что объект еще нельзя собирать? Гуглим по InitialeLifeTimeService…

    3. А значение GetExtensionName вернется по значению или по ссылке? Так как строка не MarshalByRef, то она будет сериализовываться при передаче между доменами со всеми вытекающими…

    4. В 90% случаев время жизни плагина (extension) совпадает со временем жизни всего приложения — соответсвенно Unload нам никогда не потребуется.

    Вобщем, если вы не пишете что-то типа IIS, то используйте MEF или что-то подобное для этих целей.
    • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          1. Ничто не даётся даром.
          2 и 3. Данное решение не «гуглится», а bing`уется в msdn. Я так понимаю, что если проникнутся забытой технологией remouting, то можно получить ответы на Ваши вопросы. К тому же станут ясны многие «тонкие» моменты.
          4. Сожалею, но таково ТЗ. Условия эксплуатации алгоритма неизвестны. Но возможность выгрузки сборок — основное условие.

          Насчёт MEF — спасибо. Обогатился почитав статьи на хабре, возьму на заметку.
          • 0
            Забавно, свою первую (и пока крайнюю) статью я тоже посветил теме велосипеда с расширениями в .Net. Хоть и писалось все еще в эпоху 3.5, но все равно на Хабре законно дали понять что это таки велосипед. В общем, ждите еще много интересного и нового в комментариях. Полезные комментарии — это одно из главных достоинств Хабра.
        • +1
          Поздравляю, вы написали свой MEF
          • 0
            MEF не умеет выгружать сборки
            • +1
              У дотнета вообще очень плохо с выгрузкой. Вы уже решили эту проблему аппдоменом.
          • +1
            Хм неплохо :) Но я бы вынес код загрузки и работы с интерфейсом в тот же домен где реализация интерфейса живет. Так решается очень много проблем со временем жизни объектов и междоменным взаимодействием, да и разработчику плагина не надо парится с MarshalByRef.
            • +2
              Стоп, стоп. Сборка с плагином на самом деле загрузиться в ваш основной домен вот здесь Type[] types = asm.GetTypes(). Я тоже когда-то таким игрался, единственный путь которым мне удалось обойти, это грузить в новый домен свою сборку с классом хелпером, который уже подгружал сборки с плагинами и искал уже там типи и при необходимости их создавал.
              Да это велосипед, но хотел сделать безопасную среду для исполнения плагинов. Моя статья на эту тему
              • 0
                Да, совершенно верно. Любая работа со сборкой должны быть в отдельном аппдомене, если эту сборку хочется потом выгрузить.
              • 0
                Habrahabr — мы придумали 10001 способ реализации MEF.
                • 0
                  Хотелось бы еще теста насколько просела производительность вызовов при использовании домена. Когда-то на rsdn.ru писали что 20х
                  • 0
                    Автор может справедливо счесть это брюзжанием, но мне почему-то в глаза бросилось
                    if (fileNames != null)
                    На самом деле эту проверку можно не выполнять, так как EnumerateFiles либо выбросит исключение, либо вернет таки набор с именами файлов (может даже пустой) в виде итератора
                    new FileSystemEnumerableIterator(path, originalUserPath, searchPattern, searchOption, new StringResultHandler(includeFiles, includeDirs));
                    (подсмотрено Reflector'ом)

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