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

в 15:18, , рубрики: .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();
        }

Автор: AndyGrom

Поделиться

* - обязательные к заполнению поля