При разработке интеграционных решений между .NET-приложениями и платформой 1С:Предприятие программисты часто используют COM-соединение через библиотеку V83Connector. Это мощный инструмент, позволяющий напрямую работать с базой данных 1С из кода на C#. Однако такое взаимодействие таит в себе множество подводных камней, связанных с различием в архитектуре сред выполнения. Один из самых загадочных и труднодебажимых сбоев — это возникновение исключения System.NullReferenceException при попытке получить объект по ссылке.
В этой статье мы подробно разберем, почему возникает эта ошибка, как механизмы платформы .NET маскируют реальные проблемы 1С, каким образом подписки на события могут сломать получение объекта и как быстро локализовать и исправить эту неисправность.
Рассмотрим типичный сценарий. Из внешнего консольного приложения на C# выполняется подключение к информационной базе 1С. Подключение проходит успешно, авторизация выполняется без ошибок. Мы можем без проблем выбирать любые ссылки, читать их реквизиты и даже работать со стандартными перечислениями. Проблемы начинаются тогда, когда мы пытаемся вызвать метод ПолучитьОбъект() (или GetObject()) для конкретной ссылки.
Посмотрим на пример кода, который иллюстрирует эту ситуацию:
// Инициализация COM-объекта и подключение к базе
Type comType = Type.GetTypeFromProgID("V83.COMConnector");
dynamic connector = Activator.CreateInstance(comType);
dynamic connection = connector.Connect("Srvr=\"1CServer\";Ref=\"TradeDB\";Usr=\"Admin\";Pwd=\"Pass\";");
// Получение ссылки на документ "Заказ клиента" по уникальному идентификатору
dynamic uuid = connection.NewObject("УникальныйИдентификатор", "e69e6e8d-17d9-11e8-80cf-00155d0a0504");
dynamic docRef = connection.Документы.ЗаказКлиента.ПолучитьСсылку(uuid);
// Проверка ссылки на пустоту - отрабатывает корректно
if (docRef.Пустая())
{
Console.WriteLine("Ссылка пустая!");
}
else
{
// Попытка получить объект вызывает NullReferenceException!
dynamic docObject = docRef.ПолучитьОбъект();
}
При выполнении строки docRef.ПолучитьОбъект() среда выполнения .NET выбрасывает ошибку: System.NullReferenceException: Object reference not set to an instance of an object. Самое странное здесь то, что для одних типов документов (например, самописных или простых справочников) этот метод отрабатывает безупречно, а для других (чаще всего сложных типовых документов вроде "Заказ клиента" или "Реализация товаров и услуг") — стабильно падает.
Для того чтобы понять причину сбоя, проанализируем механизм взаимодействия C# и 1С через COM. Когда мы объявляем переменную с ключевым словом dynamic, C# использует Dynamic Language Runtime (DLR) для отправки вызовов через интерфейс IDispatch COM-сервера 1С.
Если внутри платформы 1С при выполнении операции происходит критическая ошибка (например, сбой компиляции программного кода), COM-сервер не всегда способен упаковать это внутреннее исключение в структурированную ошибку COM (структуру EXCEPINFO) и передать ее обратно в вызывающий поток .NET. Вместо этого вызов метода API на стороне 1С аварийно завершается с системным кодом ошибки (например, возвращая нулевой указатель NULL или E_POINTER).
Среда выполнения CLR в C#, ожидая получить валидный указатель на COM-объект интерфейса 1С, принимает значение null. При попытке обернуть этот результат в динамический объект и продолжить выполнение программы, DLR закономерно генерирует стандартное исключение NullReferenceException. Таким образом, реальная ошибка ядра 1С оказывается полностью скрыта за стандартным поведением виртуальной машины .NET.
Логичный вопрос, который возникает у разработчика: почему ошибка происходит именно на этапе вызова ПолучитьОбъект(), если мы еще ничего не записываем и не проводим? Кажется, что этот метод должен просто прочитать поля из таблицы СУБД и инициализировать объект в оперативной памяти.
Дело в том, что при вызове метода ПолучитьОбъект() платформа 1С выполняет глубокую инициализацию объекта в памяти, которая включает в себя:
ПередЗаписью, ПриЗаписи, ОбработкаПроведения и другие).Если в процессе компиляции любого из этих модулей в контексте внешнего соединения происходит сбой, инициализация объекта прерывается, метод ПолучитьОбъект() возвращает ошибку, а в C# мы видим заветный NullReferenceException.
Рассмотрим две самые распространенные причины сбоя подписок при внешнем соединении:
Если процедура-обработчик подписки расположена в общем модуле, у которого в свойствах не установлен флаг Внешнее соединение, то при попытке инициализировать объект платформа 1С просто не сможет скомпилировать этот вызов. Контекст COM-соединения не увидит этот модуль, что приведет к фатальной ошибке выполнения ядра платформы.
Это крайне частая ошибка, совершаемая при ручном добавлении подписок сторонними разработчиками или при неаккуратном обновлении конфигурации. Для каждого события платформы строго определен состав параметров обработчика. Например:
ПриЗаписи документа процедура-обработчик в общем модуле должна принимать параметры: (Источник, Отказ).ОбработкаПроведения параметры должны быть следующими: (Источник, Отказ, РежимПроведения).Если разработчик ошибся и указал неверное количество параметров в сигнатуре процедуры общего модуля (или сама подписка была настроена некорректно), то попытка связать событие с обработчиком при создании объекта в памяти вызовет ошибку сигнатуры метода, прерывая весь процесс получения объекта.
Посмотрим на пример некорректного кода в общем модуле 1С:
// ОШИБКА: Пропущено ключевое свойство Экспорт или неверный состав параметров!
Процедура ПриЗаписиДокумента(Источник) // Нет параметра "Отказ"
// Логика обработки...
КонецПроцедуры
Исправленный и правильный вариант должен выглядеть так:
// Верная сигнатура и обязательное наличие ключевого слова Экспорт
Процедура ПриЗаписиДокумента(Источник, Отказ) Экспорт
// Логика обработки...
КонецПроцедуры
Стандартный Журнал регистрации 1С в данном случае часто оказывается бесполезен — в нем фиксируются лишь системные события авторизации и завершения сеанса, поскольку ошибка компиляции в COM-потоке прерывает работу до того, как событие запишется в журнал. Выясним, как локализовать проблему с помощью профессиональных инструментов.
Технологический журнал — это самый надежный инструмент для фиксации любых скрытых сбоев на стороне платформы. Для диагностики нам необходимо создать файл конфигурации ТЖ с именем logcfg.xml.
Если база файловая, этот файл кладется в каталог конфигурации локальной машины (например, C:\Program Files\1cv8\conf\). Если база клиент-серверная — файл размещается на сервере приложений 1С.
Проанализируем пример содержимого файла logcfg.xml для сбора ошибок:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
<log location="C:\1C_Logs\EXCP" history="24">
<event>
<eq property="Name" value="EXCP"/>
</event>
<event>
<eq property="Name" value="ERR"/>
</event>
<property name="All"/>
</log>
</config>
После сохранения этого файла запустите ваше C#-приложение и воспроизведите ошибку. В каталоге C:\1C_Logs\EXCP будут созданы файлы логов. Открыв их, вы увидите точное описание ошибки компиляции платформы 1С, например: "Процедура-обработчик события ... не найдена" или "Переменная не определена" с указанием конкретной строки кода и имени общего модуля.
Для детального анализа вы можете подключиться конфигуратором к сеансу COM-соединения. Разберем этот процесс по шагам:
-debug в строке запуска службы).ПолучитьОбъект(). Запустите C#-программу, чтобы она остановилась на этой точке.Как только C# вызовет ПолучитьОбъект(), конфигуратор 1С перехватит управление и остановит выполнение ровно на той строке общего модуля, которая вызывает сбой компиляции или выполнения. Это позволит вам мгновенно увидеть проблемное место.
Если у вас нет прав на изменение конфигурации 1С (например, база находится на жесткой поддержке или принадлежит сторонней организации, которая отказывается исправлять свои подписки на события), можно использовать альтернативный способ подключения.
Вместо легковесного V83.COMConnector вы можете запустить полноценное клиентское приложение 1С в режиме COM-сервера — V83.Application:
// Использование полноценного приложения вместо легковесного коннектора
Type comType = Type.GetTypeFromProgID("V83.Application");
dynamic connection = Activator.CreateInstance(comType);
// Инициализация соединения (открывает скрытый процесс 1cv8.exe)
connection.Connect("Srvr=\"1CServer\";Ref=\"TradeDB\";Usr=\"Admin\";Pwd=\"Pass\";");
Разберем разницу между этими подходами:
1cv8.exe.Используйте V83.Application только в крайних случаях, когда внести исправления в код конфигурации 1С не представляется возможным.