Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Горячая подмена в .Net приложениях

8 комментариев

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

Суть задачи

Я пишу плагин для Решарпера. Чтобы заниматься отладкой этого плагина, мне нужно запускать дополнительный инстанс Visual Studio, который занимает нехилое кол-во времени при стартапе. И понятное дело что при каждом изменении с моей стороны нужно запускать этот процесс снова, благо Edit & Continue – это миф, и там реально ничего не работает.

Мне это не нравится. Я хочу чтобы было лучше.

В частности, я хочу редактировать плагин прямо при исполнении и чтобы “все работало”. Сразу. Без задержек. И тут-то возникают проблемы.

Как это решается сейчас

Допустим у меня есть некий класс который наследует от BulbItemImpl — не важно что это за класс, суть не важна. Важно, в основном то, что этот класс создают другие мои классы в плагине примерно вот так:

// вот мой класс
class MyItem : BulbItemImpl
{
  ...
}
 
// и вот как я его создаю
var i = new MyItem(...);

Соответственно если я хочу подменить реализацию MyItem, я не смогу больше делать new, т.к. в том случае будет создаваться старый MyItem, а не новый.

Вклиниваем контейнер

Следовательно, я делаю небольшой трик: без особого детерминизма я втыкаю Unity как статический класс с соответствующим набором extension методов:

public static class ContainerExtensions
{
  static UnityContainer container;
  
  static ContainerExtensions()
  {
    container = new UnityContainer();
  }
  
  static TBase Get<TBase,TImpl>(this object o, params object [] args)
  {
    return container.Resolve<TBase>(typeof(TImpl).Name, new InjectionConstructor(args));
  }
  
  static void Set<TBase>(this object o, Type implType)
  {
    container.RegisterType(typeof(TBase), implType, typeof(TImpl).Name);
  }
}

Вот такое вот шаманство. Ничего странного не находите? Как насчет typeof(TImpl).Name? Давайте поясню что тут происходит. У нас много классов наследуют от BulbItemImpl, следовательно нам как бы некошерно просто делать container.RegisterType<BulbItemImpl,MyItem> потому что эта регистрация будет 100 раз перекрыта. Поэтому мы делаем именную регистрацию по имени типа конечного результата. Это будет полезно дальше.

Делаем Impl-прослойку

Итак, у нас есть класс который выглядит, например, вот так:

public sealed class RequireOrEnsureBulbItem : BulbItemImpl
{
    // Fields
    private readonly string block;
    private readonly IMethodDeclaration method;
    private readonly ICSharpContextActionDataProvider provider;
    private readonly string text;
    // Methods
    public RequireOrEnsureBulbItem(ICSharpContextActionDataProvider provider, IMethodDeclaration method, string text, string block);
    protected override Action<ITextControl> ExecuteTransaction(ISolution solution, IProgressIndicator progress);
    // Properties
    public override string Text { get; }
}

Соответственно, мы берем и переименовываем этот класс, добавляя префикс Impl.

Теперь, берем и делаем его копию. Копия эта является прозрачным прокси поверх оригинала. Копия содержит инстанс Impl-типа:

private BulbItemImpl impl;

который инициализируется в конструкторе:

public RequireOrEnsureBulbItem(ICSharpContextActionDataProvider provider,
  IMethodDeclaration method, string text, string block)
{
  impl = this.Get<BulbItemImpl,RequireOrEnsureBulbItemImpl>(
    provider, method, text, block);
}

и используется для всех пробросов вызовов методов (в т.ч. свойств), например

public override string Text { get { return impl.Text; } }

Что дальше?

Мы только что разделили один класс на интерфейс, который загружается единожды, и реализацию, которую можно, в принципе, менять в контейнере. Но как? На самом деле – очень просто. Для начала давайте решим как мы будем знать что можно редактировать а что нет. Лучший способ – пометить это типы (это сгенерированные Impl-типы) каким-нибудь аттрибутом вроде [Editable]. Тогда мы будем всегда знать что можно генерировать а что нет.

Теперь берем, и как я уже описал, открываем редактор на этот Impl-тип. Следует помнить что редактируем мы тип без названия Impl, т.к. только для него у нас есть сорцы. Мы ничего не переименовываем, а просто создаем новый тип (наследующий, в данном примере, от BulbItemImpl) и регистрируем его в контейнере.

this.Set<BulbItemImpl>(GeneratedType);

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

Заключение

То что я описал выше – это и есть “горячая подмена” функционала. С помощью нее я могу редактировать плагин прямо в момент исполнения, проверять API, тестировать новые идеи и при этом не напрягаться с перезапуском отладчика. Понятное дело что отладка динамически сгенерированных сборок у меня не работает (не напрягаюсь я на тему PDB), но это не так критично как получить ответ на простой вопрос “работает оно или нет?”.

Реклама

Written by Dmitri

29 декабря 2010 в 1:03

Опубликовано в .NET

Tagged with

комментариев 8

Subscribe to comments with RSS.

  1. Стоило хотя бы раз упомянуть, что описанная «горячая подмена» — часть стандартного функционала IoC контейнеров.

    В unity повторная регистрация приведет к такому же эффекту.

    container.RegisterInstance<BulbItemImpl>(new GeneratedType());

    Ruslan

    29 декабря 2010 at 1:46

    • Да, это само собой разумеется. Именно этот эффект я и использую. Только в посте речь идет про ситуацию в которой корневые элементы не создаются с помощью IoC-контейнера и тем самым их реализацию не заменить. Эта проблема везде и повсюду, кстати.

      Dmitri

      29 декабря 2010 at 15:00

  2. А как насчёт класса, который ставил бы FileWatch на Output директорию плагина и каждый раз при изменении dll копировал бы её и pdb во временную директорию и загружал бы её и из неё все классы помеченные атрибутом (тем же Editable) и заменял старые регистрации новыми по имени класса. Не надо никаких бубнов с именами и у тебя новая реализация каждый раз после сборки плагина.

    alexeysuvorov

    29 декабря 2010 at 3:35

    • Конкретно в моем сценарии это не работает, т.к. Решарпер, будь он неладен, найдет и старые и новые типы, что приведет к ситуации когда один и тот же ф-л дуплицируется по N раз. Понятное дело что если бы он использовал мой IoC-контейнер (или я его), такие танцы с бубном не понадобились бы.

      Dmitri

      29 декабря 2010 at 14:55

    • Ну это было бы круто если бы система плагинов использовала DI при создании типов. Но она использует в данном случае свой DI, которым я управлять не могу.

      Dmitri

      29 декабря 2010 at 15:01

  3. когда новые подкасты будут?

    Беко

    29 декабря 2010 at 8:09

  4. Любой IoC контейнер тут подойдет…

    xna

    29 декабря 2010 at 10:10


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: