Как стать автором
Обновить

Пример системы плагинов

Время на прочтение4 мин
Количество просмотров2K
Придумал я себе такую задачу, как разработать к программе систему плагинов (add-in-ов). Немного поковыряв AddIn которые были предложены начиная с .NET 3.5, понял что они мне не подходят. Во-первых это не удобное расположение каталогов, много лишних. Во-вторых не возможно внедриться в какую нибудь часть процесса и сделать по своему, т.е. слабая расширяемость.
И решил изобрести велосипед...

В основу я поставил изолированность от главного приложения и возможность песочницы. Это все не сложно сделать создав новый AppDomain и сконфигурировав его на InternetZone. Но при разработке я столкнулся с проблемой при загрузке сборки с плагином в домен. Проблема заключалась в ошибке со связями. Для загрузки я использую AppDomain.Load(). Как позже оказалось этот метод мне не подходит. По-этому пришлось сделать не большой костыль, о котором я и расскажу.

Начнем с создания нового проекта и консольного приложения (я назвал его PluginHost) и добавим еще один проект библиотеку классов (назовем её PluginFramework). В библиотеку добавим класс:
  1. public abstract class Plugin: MarshalByRefObject
  2. {
  3.   public virtual void Initialize()
  4.   {
  5.   }
  6. }
* This source code was highlighted with Source Code Highlighter.

И еще один класс, без которого было сложно реализовать необходимую мне логику:
  1. public class AssemblyHelper: MarshalByRefObject
  2. {
  3.   private AppDomain _currentDomain;
  4.   public AssemblyHelper()
  5.   {
  6.    _currentDomain = AppDomain.CurrentDomain;
  7.    _currentDomain.AssemblyResolve += new ResolveEventHandler(_currentDomain_AssemblyResolve);
  8.   }
  9.  
  10.   Assembly _currentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
  11.   {
  12.    string[] nameSplit = e.Name.Split(',');
  13.    string path = Path.Combine(SearchFolder, nameSplit[0] + ".dll");
  14.    Assembly loadedAssembly;
  15.    try
  16.    {
  17.      loadedAssembly = Assembly.LoadFile(path);
  18.    }
  19.    catch (Exception exc)
  20.    {
  21.      Exception exp = exc;
  22.      throw;
  23.    }
  24.    if (loadedAssembly != null)
  25.    {
  26.      return loadedAssembly;
  27.    }
  28.    else
  29.    {
  30.      return null;
  31.    }
  32.   }
  33.  
  34.   public string SearchFolder { get; set; }
  35. }
* This source code was highlighted with Source Code Highlighter.

Базовый каркас готов. Не большое отступление чего же я собственно хотел добиться:
1.Еще раз изолированность от основного приложения
2.В любой момент загрузка и выгрузка плагина. Если к примеру плагин упал, мы его просто выгрузим и заново загрузим

Что же у нас в Main:
Для начала я загружая сборку PluginFramework в режиме только для рефлексии и получаю Type нашего базового класса для всех плагинов:
  1. Assembly frameworkReflect = Assembly.ReflectionOnlyLoadFrom(Path.Combine(Environment.CurrentDirectory, "PluginFramework.dll"));
  2. Type basePluginType = frameworkReflect.GetType("MyPluginSample.Framework.Plugin");
* This source code was highlighted with Source Code Highlighter.

Потом поиск по всем имеющимся папкам
  1. string pathPlugins = Path.Combine(Environment.CurrentDirectory, "Plugins");
  2. DirectoryInfo directoryPlugins = new DirectoryInfo(pathPlugins);
  3. foreach (var dir in directoryPlugins.GetDirectories())
  4. {
  5.   FileInfo dllFile = dir.GetFiles(dir.Name + ".dll").First();
* This source code was highlighted with Source Code Highlighter.

И опять я загружаю сборку в режиме только для рефлексии, но уже с плагином и ищу класс который наследуется от базового класса Plugin. Что бы мы здесь могли загрузить сборку с плагином и сравним с базовым типом нам и пришлось в начале PluginFramework тоже загрузить только для рефлексии.
  1. Assembly reflectionAsm = Assembly.ReflectionOnlyLoadFrom(dllFile.FullName);
  2. Type typePlugin = reflectionAsm.GetTypes().First(t => t.IsSubclassOf(basePluginType));
* This source code was highlighted with Source Code Highlighter.

Теперь можно приступить к созданию нового домена, загрузить в него все необходимое и запустить наш плагин на выполнение
  1. AppDomain pluginDomain = AppDomain.CreateDomain(dir.Name + " plugin");
  2. AssemblyHelper helper = (AssemblyHelper)pluginDomain.CreateInstanceAndUnwrap("PluginFramework", "MyPluginSample.Framework.AssemblyHelper");
  3. helper.SearchFolder = dir.FullName;
  4. Plugin plugin = (Plugin)pluginDomain.CreateInstanceAndUnwrap(reflectionAsm.FullName, typePlugin.FullName);
  5. plugin.Initialize();
* This source code was highlighted with Source Code Highlighter.

Вот и все!
И напишем тестовый плагин для проверки
  1. public class MyPlugin1: Plugin
  2. {
  3.   public override void Initialize()
  4.   {
  5.    Console.WriteLine("Executing in Plugin 1. Domain Id: {0}", AppDomain.CurrentDomain.Id);
  6.   }
  7. }
* This source code was highlighted with Source Code Highlighter.

Структура каталогов следующая


Ссылка на исходники PluginSystem.zip

P.S. Сборки для рефлексии я загружаю, потому что если просто загрузить сборку в новый домен, мы не сможем пройтись по ней из дочернего домена и найти нужный нам класс. CLR будет пытаться загрузить эту сборку в дочерний домен
Теги:
Хабы:
Всего голосов 6: ↑4 и ↓2+2
Комментарии23

Публикации

Ближайшие события