Решение конфликтов зависимостей в .NET 4.8 на примере плагина для Autodesk Revit: изоляция через Cross Domain Interaction
Предисловие
Меня зовут Худошин Илья, я занимаюсь разработкой десктопных, серверных и веб‑приложений. Хотя в последние годы я почти не пишу под .NET Framework, мне попалась на размышление одна типичная проблема из мира плагинов для Autodesk Revit. Идея её решения оказалась достаточно интересной, чтобы реализовать прототип и разобрать подход подробно.
Речь пойдёт о конфликте зависимостей в плагинах для Revit, работающих на .NET Framework 4.8, и о том, как с помощью отдельного домена выполнения (AppDomain) изолировать часть логики плагина и его библиотеки, минимизировав риск "битвы" версий и непредсказуемых падений.
---
Контекст: Revit, .NET и архитектура плагинов
Autodesk Revit предназначен для BIM‑моделирования различных разделов проекта. Один из самых распространённых способов автоматизировать рутинные задачи в Revit - писать плагины на платформе .NET.
До версии Revit 2024 включительно средой выполнения для плагинов является .NET Framework 4.8. Начиная с Revit 2025, Autodesk переехал на .NET 8 (наследник .NET Core), что радикально меняет подход к структуре проектов, csproj, подбору NuGet‑пакетов и совместимости библиотек. Однако в этой статье мы намеренно остаёмся в "старом мире" - Revit 2024 и ниже, где всё крутится вокруг .NET Framework 4.8.
Плагины подключаются в Revit через манифесты формата `.addin`. При запуске программы Revit сканирует предопределённые каталоги, находит такие манифесты и на их основе загружает сборки, реализующие интерфейсы `IExternalApplication`, `IExternalCommand` и т.п.
Ключевой момент: при старте `Revit.exe` загружается CLR .NET Framework 4.8 и инициализируется базовый `DefaultDomain`. Уже в этом домене Revit начинает подтягивать свои собственные сборки и их зависимости, а затем - плагины сторонних разработчиков. Никакой отдельной инициализации рuntime для каждого плагина не происходит: все сборки оказываются в общем домене.
---
В чём суть проблемы: конфликты зависимостей в одном домене
Так как все плагины и сам Revit работают в одном `DefaultDomain`, то:
- все они разделяют один общий набор загруженных сборок;
- любые совпадающие по имени библиотеки, но с разными версиями, начинают бороться за право быть загруженными первыми;
- Revit подгружает свои зависимости заранее, и именно их версии часто побеждают, навязывая свои ограничения всем плагинам.
Ситуация осложняется тем, что на одном рабочем месте может быть установлено несколько плагинов разных разработчиков. Они нередко используют одни и те же популярные пакеты: логгеры, DI‑контейнеры, WPF‑UI‑фреймворки, вспомогательные библиотеки. Одно и то же имя сборки с разными версиями - и вот вы уже имеете типичный конфликт:
- ваш плагин ожидает одну версию библиотеки;
- другой плагин или сам Revit уже подгрузили другую;
- CLR использует первую загруженную версию, а остальной код вынужден "как-то ужиться" с ней.
Простой пример - логгер Serilog. Если в проекте плагина добавить ряд зависимостей и их транспарентные зависимости подтянут конкретные версии Serilog, а в другом плагине или в экосистеме Revit будет другая версия, высока вероятность конфликта при инициализации логгера. Аналогичная история с WPF UI‑китами: темы, контролы, вспомогательные компоненты нередко несовместимы между версиями, но CLR об этом не предупреждает заранее.
---
Почему это опасно и сложно диагностируется
Сложность таких конфликтов в том, что они:
- проявляются только в рантайме, причём не всегда при старте плагина;
- могут "выстрелить" в любой момент - при вызове конкретного метода, обращении к определённому свойству, создании конкретного типа;
- зачастую воспроизводятся только при определённых сочетаниях установленных плагинов и конкретных версий Revit.
Результат - неочевидные исключения, падения плагина, а иногда и критические сбои, влияющие на работу Revit в целом. Это ведёт к дополнительным трудозатратам: приходится тратить время не на развитие функционала, а на разбор стека вызовов, эксперименты с версиями библиотек и попытки воспроизведения ошибки на машине пользователя.
В больших корпоративных решениях, где установлено множество плагинов, подобные конфликты превращаются в постоянную головную боль, а отловить их на стадии тестирования получается далеко не всегда.
---
Очевидные, но неудобные пути решения
В теории вариантов борьбы с конфликтами немало:
- ручное редактирование IL‑кода, переименование сборок, "склеивание" нескольких версий;
- создание собственных форков популярных библиотек с уникальными именами сборок;
- отказ от сторонних плагинов и попытка "жить в вакууме", жёстко контролируя окружение;
- агрессивное использование `bindingRedirect` в конфиге приложения (что не всегда доступно для плагинов).
Каждый из этих подходов имеет право на существование, но обладает серьёзными минусами: ухудшение сопровождаемости кода, рост сложности деплоя, необходимость следить за форками, ручной труд и большой риск ошибок.
Я в разное время пробовал разные варианты, и у каждого действительно есть свои плюсы и минусы. Однако в этой статье я намеренно не углубляюсь в сравнение всех стратегий. Вместо этого фокусируюсь на подходе, который считаю одним из наиболее безопасных и предсказуемых: изоляция зависимостей с помощью Cross Domain взаимодействия.
---
Идея: изолировать плагин в отдельный домен выполнения
Цель подхода - вытащить "опасные" части плагина (логика, много зависящая от внешних библиотек, WPF‑UI, логгеры, DI, сторонние пакеты) из общего `DefaultDomain` в отдельный AppDomain.
Мы получаем:
- собственный, изолированный набор загружаемых сборок;
- возможность использовать любые версии зависимостей, не оглядываясь на Revit и другие плагины;
- ограниченную и контролируемую точку контакта между доменами.
По сути, Revit и минимальный "стартер" плагина живут в `DefaultDomain`, а "ядро" плагина и модули с богатыми зависимостями - в другом домене. Между ними организуется чёткий протокол взаимодействия.
---
Нефункциональные требования к решению
Прежде чем строить архитектуру такого решения, полезно чётко сформулировать требования. В рамках данного подхода я для себя зафиксировал следующее:
1. Изолировать конфликтующие зависимости от DefaultDomain, предоставляемого `Revit.exe`.
2. Не ограничивать себя выбором пакетов под .NET Framework 4.8:
- свободно использовать разные WPF UI‑киты;
- подключать любые библиотеки логирования;
- использовать удобный контейнер зависимостей, не боясь его версии.
3. Обеспечить модульность: новые функциональные блоки плагина должны подключаться без значимых архитектурных изменений.
4. Создать уровень абстракции над реализацией, которая фактически исполняется в другом домене:
- в DefaultDomain - только контракты и минимальная логика;
- в отдельном домене - тяжёлая реализация.
5. Сохранить возможность взаимодействия с Revit из другого домена:
- как в блокирующем UI сценарии (например, модальные окна),
- так и в неблокирующем режиме, не мешая основной работе пользователя.
6. Предоставить удобный способ управления зависимостями и их получения:
- для стартового кода плагина;
- для ядра системы;
- для отдельных модулей.
Все эти требования будут учтены в архитектуре и дальнейшем примере реализации.
---
Архитектура: ядро, домен‑менеджер и модули
Логически систему можно разделить на несколько слоёв:
1. Стартер плагина в DefaultDomain
- минимальный код, который Revit вызывает через `IExternalApplication` (и, при необходимости, `IExternalCommand`);
- отвечает за инициализацию домен‑менеджера и регистрацию модулей;
- не тянет тяжёлые зависимости.
2. Домен‑менеджер (Domain Manager)
- отвечает за создание и конфигурацию нового AppDomain;
- загружает в него ядро плагина и модули;
- реализует прокси‑взаимодействие между доменами (вызовы методов, передача данных).
3. Ядро плагина
- основной каркас приложения: DI‑контейнер, логгирование, базовые сервисы;
- располагается в изолированном домене, использует любые необходимые зависимости.
4. Модули (например, `ITestSampleModule`)
- независимые функциональные блоки;
- реализуют определённый контракт (интерфейс), известный стартеру и домен‑менеджеру;
- подключаются к ядру динамически.
5. Контракты взаимодействия
- общий набор интерфейсов и DTO, доступных обоим доменам;
- должны быть спроектированы так, чтобы свести к минимуму утечку деталей реализации.
Такое разделение позволяет изолировать всё потенциально конфликтное в отдельный AppDomain, а в DefaultDomain оставить только "обвязку" и контракты.
---
Реализация домен‑менеджера и ядра
В практической реализации домен‑менеджер:
- создаёт новый AppDomain с нужной конфигурацией (пути поиска сборок, настройки загрузки);
- инициализирует в нём стартовый объект ядра (например, класс Bootstrapper или Kernel);
- предоставляет вызывающему коду методы наподобие:
- загрузить модуль по имени/пути;
- выполнить команду модуля;
- передать данные и получить результат.
Само ядро в изолированном домене может:
- поднять контейнер зависимостей;
- настроить логирование (Serilog, NLog или любой другой);
- инициализировать WPF‑подсистему (если требуется UI);
- зарегистрировать модули и их зависимости.
Домен‑менеджер в DefaultDomain не знает деталей реализации - только общие интерфейсы. Это даёт гибкость: сменить DI‑контейнер или логгер в ядре можно без изменения стартового кода плагина.
---
Модуль `ITestSampleModule` как пример
Отдельный модуль вроде `ITestSampleModule` служит демонстрацией того, как можно строить независимые части функционала:
- модуль реализует интерфейс, описывающий его API: методы запуска, остановки, взаимодействия с Revit;
- физически собирается в отдельную сборку, которая загружается в изолированный домен;
- использует любые зависимости, не заботясь о версиях в DefaultDomain.
Через домен‑менеджер стартер в Revit может:
- запросить у ядра список доступных модулей;
- активировать нужный модуль по имени;
- вызвать в модуле определённую команду или обработчик.
Такое построение позволяет масштабировать плагин без страха, что каждый новый модуль привнесёт несовместимые версии библиотек или нарушит работу других частей системы.
---
Пример взаимодействия между доменами
Типичная схема вызова выглядит так:
1. Пользователь запускает Revit.
2. Revit по манифесту `.addin` инициализирует стартер плагина в `DefaultDomain`.
3. Стартер создаёт домен‑менеджер и инициирует создание изолированного AppDomain.
4. В новом домене запускается ядро плагина, которое регистрирует модули.
5. При необходимости стартер вызывает у домен‑менеджера определённые методы (например, запуск модуля), передавая аргументы через контракты.
6. Модуль выполняет работу и возвращает результат обратно в `DefaultDomain`.
Важно, что при этом:
- обмен данными идёт через хорошо определённые интерфейсы и сериализуемые структуры;
- пересекается только минимальный набор типов;
- точно известно, что внутри изолированного домена загружены "свои" версии сборок.
---
Взаимодействие с Revit: блокирующий и неблокирующий режимы
Отдельный аспект - работа с Revit API из другого домена. Здесь важно соблюдать несколько правил:
1. Блокирующий сценарий (модальные операции)
- используется, когда нужно показать модальное окно, запросить у пользователя действие, начало/окончание транзакций и т.п.;
- логика может инициироваться из отдельного домена, но фактический вызов Revit API лучше проксировать в `DefaultDomain`, чтобы строго контролировать поток и контекст.
2. Неблокирующий сценарий (фоновая обработка)
- когда требуется выполнить длительную операцию, не блокируя интерфейс;
- вычисления, парсинг, подготовка данных выполняются в изолированном домене;
- UI‑обновления и взаимодействие с документом выполняются "мостом" в основном домене.
Такое разделение помогает не ломать модель потоков, ожидаемую Revit API, и одновременно сохранять изоляцию зависимостей.
---
Подключение модулей и домен‑менеджера к стартеру плагина
Со стороны стартера схема выглядит упрощённо:
- при `OnStartup` плагина создаётся домен‑менеджер;
- домен‑менеджер поднимает ядро, загружает модули и сообщает стартеру, какие функции доступны;
- в зависимости от сценария работы (кнопки на панели, команды, события) стартер вызывает соответствующие методы домен‑менеджера.
С точки зрения Revit весь этот механизм "прозрачен": снаружи это просто плагин, состоящий из одной или нескольких команд. Но внутри он устроен как отдельное мини‑приложение со своей средой выполнения.
---
Точки роста и развитие архитектуры
Представленный подход даёт основу, но его можно развивать дальше:
1. Горячая перезагрузка модулей
- возможность выгружать и перезагружать модули без перезапуска Revit;
- полезно при разработке и отладке.
2. Версионирование модулей
- разные версии одного и того же модуля могут coexist в разных доменах;
- облегчает поэтапный переход между версиями.
3. Плагины внутри плагина
- поверх ядра можно реализовать свою систему плагинов, где сторонние разработчики подключают модули, не влияя на базовое ядро.
4. Гибкая конфигурация загрузки
- конфиг‑файл или интерфейс в Revit для включения/отключения модулей;
- выбор зависимостей и поведения без перекомпиляции.
5. Расширенный протокол взаимодействия
- очереди задач, асинхронная коммуникация между доменами;
- централизованная обработка ошибок с логированием и уведомлениями.
---
Практические рекомендации
Чтобы такой подход оказался полезен на практике, а не остался теоретической конструкцией, стоит придерживаться нескольких принципов:
- Минимизировать контракт между доменами
Чем меньше типов нужно шарить между доменами, тем проще поддержка и меньше риск ошибок сериализации.
- Жёстко разделять ответственность
В DefaultDomain - только то, что необходимо Revit (инициализация, контакты с API, UI‑мост). В изолированном домене - логика, данные, тяжёлые зависимости.
- Использовать понятную схему логирования
Логи из разных доменов должны быть легко различимы - хотя бы по префиксами или полям контекста. Это значительно упрощает отладку.
- Прорабатывать сценарии ошибок
При падении модуля в изолированном домене стартер не должен "заваливать" Revit. Лучше вернуть контролируемое сообщение, предложить пользователю перезапустить модуль или сообщить о проблеме.
- Планировать переход на .NET 8
Несмотря на ориентацию на .NET 4.8, стоит сразу проектировать архитектуру так, чтобы при переходе на Revit 2025 и .NET 8 иерархия слоёв и контрактов сохранилась, а механику изоляции можно было адаптировать под новые механизмы (например, загрузчики сборок, контейнеры, отдельные процессы).
---
Заключение
Конфликты зависимостей в плагинах для Autodesk Revit на .NET Framework 4.8 - не уникальная проблема, но в условиях общего `DefaultDomain` и большого количества сторонних решений они становятся особенно болезненными.
Изоляция "опасных" зависимостей в отдельный домен выполнения и построение Cross Domain взаимодействия позволяют:
- защитить ваш плагин от версионных конфликтов с другими сборками;
- свободно выбирать WPF UI‑киты, логгеры и DI‑контейнеры;
- строить модульную архитектуру, удобную для расширения;
- контролировать взаимодействие с Revit, не нарушая ожиданий его API.
В рамках описанного подхода мы формулируем нефункциональные требования, проектируем архитектуру с ядром, домен‑менеджером и модулями, реализуем пример модуля (`ITestSampleModule`) и продумываем взаимодействие между доменами. Такой подход не отменяет других возможных решений, но даёт разработчику плагинов под Revit устойчивый и предсказуемый инструмент борьбы с конфликтами зависимостей, особенно в сложных и развивающихся проектах.



