Почему событие .NET COM, выбрасывающее объект, не соответствует целевому типу при вызове?

TL;DR: событие COM, которое работает в большинстве случаев, вызывает System.Reflection.TargetException, когда

  1. COM-сервер развертывается с использованием бесплатной регистрации SxS.
  2. И событие вызывается узлом службы WCF.

Мы используем сложную архитектуру для модернизации набора корпоративных приложений, которым более 30 лет. Мы уже много лет используем WCF для запуска событий COM в нескольких наших приложениях. Это позволяет нам получать доступ к данным и бизнес-процессам в наших устаревших приложениях и предоставлять эти данные любому современному приложению, которое использует клиент WCF. Мы достигаем этого путем создания COM-серверов, предоставляющих приложениям события для подписки, и единого COM-метода, который при вызове создает узел службы WCF, запускающий эти события при получении запросов сообщений. Недавно мы обновили одно из наших устаревших приложений — старое настольное приложение для Windows, которое обеспечивает основной пользовательский интерфейс для ядра нашей системы, а также обеспечивает основную часть бизнес-операций для нашей системы. Его можно запустить в автоматическом режиме, что позволяет использовать его аналогично примитивному сервису NT. Он может выполнять фоновые операции, а также отправлять ему служебные запросы. Недавно мы создали COM-сервер .NET, который может предоставлять некоторые современные окна WPF для этого устаревшего приложения. Это наша первая попытка открыть окна WPF из нашего устаревшего приложения на основе GDI. Я упоминаю об этом, потому что COM-сервер может вызывать события при запуске из компонентов WPF, поставляемых сервером. В качестве альтернативы этот же сервер COM может поддерживать узел службы WCF, который вызывает те же события COM, что и пользовательский интерфейс WPF, для поддержки режима без вывода сообщений устаревшего приложения, когда приложение работает без пользовательского интерфейса. Все это работает и без проблем развернуто в продакшене.

Однако недавно я начал изучать COM без регистрации для этого конкретного COM-сервера. В какой-то степени я был на удивление успешным. Когда пользовательский интерфейс запущен, все компоненты WPF работают нормально, и эти события COM успешно обрабатываются устаревшим приложением. Когда приложение работает в автоматическом режиме, клиентское приложение успешно создает и настраивает сервер COM, а также успешно привязывается к событиям COM. COM-сервер успешно инициализирует узел службы WCF. Но когда узел получает сообщение и пытается вызвать одно из этих событий, это исключение выдается до того, как событие будет вызвано. Клиент никогда не уведомляется о событии. Просто чтобы уточнить, конкретное событие, на котором я здесь сосредоточиваюсь, успешно вызывается и обрабатывается, когда его запускают компоненты WPF COM-сервера, но не когда его запускает узел службы WCF COM.

Объект не соответствует типу цели.
(System.Reflection.TargetException)

Источник: mscorlib Целевой сайт: InvokeDispMethod Тип объявления: System.RuntimeType

Трассировки стека: ----------------------------------------------- ---- at System.RuntimeType.InvokeDispMethod (имя строки, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, культура Int32, String[] namedParameters) at System.RuntimeType.InvokeMember (имя строки, BindingFlags bindingFlags , Связыватель связывателя, Цель объекта, Object[] createdArgs, модификаторы ParameterModifier[], культура CultureInfo, String[] namedParams) в System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Int32[] aWrapperTypes, MessageData& msgData) в MyApp.IMyCOMServer.RetrieveProcesses()
в MyApp.MyCOMServer.OnRetrieveProcesses() в MyApp.MyService.d__7.MoveNext()

Одним из важных различий между компонентами WPF и компонентами WCF является многопоточность. Устаревшее приложение представляет собой однопотоковое апартаментное приложение, поэтому все события WPF передаются и обрабатываются потоком пользовательского интерфейса приложения. Здесь я ожидал бы провала, потому что это была непростая задача, и это очень новая архитектура для нас. Но когда приложение работает без пользовательского интерфейса, служба WCF обрабатывает каждое сообщение в новом потоке. Устаревшее приложение написано на языке Visual FoxPro, который поддерживает серверы автоматизации COM и решает за нас весь беспорядок, связанный с событиями COM и проблемами потоковой передачи. Мы широко используем события COM в наших приложениях VFP уже более 20 лет и имеем огромный опыт в этой области, поэтому я ожидаю, что именно здесь возникнут наименьшие проблемы. Потоки никогда раньше не были проблемой для хостов наших сервисов WCF, и вот я здесь. Это важная часть интерфейса событий COM.

#region COM Event Delegates 
[ComVisible(false)]
public delegate ProcessList RetrieveProcessesDelegate();
#endregion

[ComVisible(true)]
[Guid(ComDefinitions.EventsInterfaceId)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IMyCOMEvents
{
  [DispId(30)]
  ProcessList RetrieveProcesses();
}

Вот соответствующая часть интерфейса COM-сервера. COM-клиент создает экземпляр COM-сервера, затем подписывается на события, предоставляемые IMyCOMEvents, определенными выше, и, наконец, вызывает StartWcfService, определенный ниже. Затем клиент бездействует, ожидая событий для обработки.

[ComVisible(true)]
[Guid(ComDefinitions.InterfaceId)]
public interface IMyCOMServer
{
  [Description("Initializes and starts the WCF service host ")]
  void StartWcfService();
}

Вот соответствующая часть COM-сервера. Когда возникает исключение, событие RetrieveProcesses не является нулевым — кажется, что COM-клиент успешно подписался. ProcessList — это POCO-подобный COM-сервер, который, как ожидается, будет создан и возвращен COM-клиентом, хотя я также пытался реорганизовать определение события, чтобы клиент возвращал System.String, но выбрасывалось то же исключение. ProcessList также поддерживает сериализацию с использованием System.Runtime.Serialization.DataContractSerializer, что имеет отношение к службе WCF, когда я доберусь до этого ниже.

[ComVisible(true)]
[Guid(ComDefinitions.ClassId)]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(IMyCOMEvents))]
[ProgId(ComDefinitions.ProgId)]
public class MyCOMServer : IMyCOMServer
{
    private ServiceHost _svc;
    public event RetrieveProcessesDelegate RetrieveProcesses;

    public ProcessList OnRetrieveProcesses()
    {
      return RetrieveProcesses?.Invoke();
    }

    public void StartWcfService()
    {
        if (_svc != null)
        {
          // Service is already running;
          return;
        }

        _svc = new ServiceHost(new MyService(this));
        var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None)
          {
            CloseTimeout = TimeSpan.FromMinutes(1d),
            OpenTimeout = TimeSpan.FromMinutes(1d),
            ReceiveTimeout = TimeSpan.FromHours(5d),
            SendTimeout = TimeSpan.FromHours(5d),
            TransferMode = TransferMode.Streamed,
            HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
            MaxBufferPoolSize = 524288L,
            MaxBufferSize = 2147483647,
            MaxConnections = 5,
            MaxReceivedMessageSize = 2147483647L,
            ReaderQuotas =
                   {
                     MaxDepth = 32,
                     MaxStringContentLength = 10000000,
                     MaxArrayLength = 16384,
                     MaxBytesPerRead = 16384,
                     MaxNameTableCharCount = 100000
                  }
          };

        var endpoint = _svc.AddServiceEndpoint(typeof(IMyService), binding, MessageDefinitions.MyUrl);
        _svc.Open();
      }
      catch (Exception ex)
      {
        // Write to event log
        EventWriter.Write(ex);
        throw new COMException(ex.Message, ex);
    }
}

internal struct ComDefinitions
{
  public const string TypeLibId = "0ABC52FB-9B16-4E61-A869-8244650EC62C";
  public const string ClassId = "CB3CCF16-68F4-4C50-8047-2A2FBE379B43";
  public const string InterfaceId = "032A0A65-056C-4038-A121-7CA21D34A4D1";
  public const string EventsInterfaceId = "B0294518-9A1A-425B-B6EE-87BDA09CA0C0";
  public const string ProgId = "MyCOMServer.ProgId";
}

Вот соответствующая часть экземпляра службы WCF. Служба возвращает задачу в первую очередь для поддержки наших клиентов WCF, которые полностью асинхронны.

[ComVisible(false)]
[ServiceContract(Namespace = MessageDefinitions.Namespace)]
public interface IMyService
{
  [OperationContract(AsyncPattern = true)]
  [FaultContract(typeof(ServiceFaultInfo))]
  Task<ProcessList> RetrieveProcessesAsync();
}

[ComVisible(false)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
public class MyService : IMyService
{
  private readonly MyCOMServer _comServer;

  public MyService(MyCOMServer comServer)
  {
    _comServer = comServer;
  }

  public Task<ProcessList> RetrieveProcessesAsync()
  {
    try
    {
      var result = _comServer.OnRetrieveProcesses() ?? new ProcessList();
      return Task.FromResult(result);
    }
    catch (Exception ex)
    {
      // Log exception
      EventWriter.Write(ex);
      var reason = new FaultReason(ex.Message);
      var code = new FaultCode(_vfpFaultCode, MessageDefinitions.Namespace);
      throw new FaultException<ServiceFaultInfo>(new ServiceFaultInfo(ex), reason, code);
    }
  }
}

Вот соответствующий код COM-клиента, написанный в Visual FoxPro.

LOCAL loComInstance, loComEventHandler
loComInstance = CREATEOBJECT("MyCOMServer.ProgId")
loComEventHandler = CREATEOBJECT("ComEventHandler")
IF NOT EVENTHANDLER(loComInstance, loComEventHandler)
  ERROR 1429, "Unable to subscribe to MyCOMServer.ProgId events"
ENDIF
DO WHILE .T.
  READ EVENTS
ENDDO

DEFINE CLASS ComEventHandler AS Session OLEPUBLIC
  IMPLEMENTS IMyCOMEvents IN MyCOMServer.tlb

  PROCEDURE IMyCOMEvents_RetrieveProcesses() AS "MyCOMServer.ProcessListProgId"
    LOCAL loList
    loList = CREATEOBJECT("MyCOMServer.ProcessListProgId")
    * … Populate list
    RETURN loList
  ENDPROC
ENDDEFINE

person RMart    schedule 21.04.2021    source источник
comment
Я сделал немного с использованием манифестов, чтобы избежать регистрации COM. Я бы предположил, что, поскольку это проблема с типом, возможно, это связано с интерфейсом. Есть ли в вашем манифесте интерфейс IComEvents, к которому может получить доступ вызывающая сторона? Если вы вручную поместите интерфейс с его прокси-заглушкой и всей соответствующей информацией в HKCR\Interface, сможете ли вы решить проблему? Если вы собираетесь пересекать апартаменты/потоки, то он должен знать, как маршалировать интерфейсы, которые вы используете. Еще хуже, если в вашем интерфейсе есть типы данных, не совместимые с автоматизацией. Я видел эту проблему в проектах C++.   -  person Joseph Willcoxson    schedule 21.04.2021
comment
@JosephWillcoxson Манифест приложения COM-клиента ссылается только на AssemblyIdentity COM-сервера .NET. Манифест COM-сервера определяет только clrClass и не ссылается на интерфейс событий. Обратите внимание, что COM-клиент ссылается на MyComServer.tlb при определении обработчика событий COM. Я могу обновить вопрос с определениями манифеста, если вы считаете это важным, но, учитывая, что при некоторых обстоятельствах события обрабатываются правильно, я не подозревал конфигурацию SxS, но вы поднимаете несколько хороших вопросов. Я исследую.   -  person RMart    schedule 22.04.2021
comment
О TargetException: docs.microsoft.com /en-us/dotnet/api/   -  person Theobald Du    schedule 22.04.2021
comment
Бесплатную регистрацию часто трудно проверить, так как если в реестре есть какие-то остатки, это может свести вас с ума. Итак, первое, что нужно сделать, это действительно протестировать с использованием чистых настроек. В любом случае, потоки могут быть здесь проблемой, вы проверили это: stackoverflow.com/questions/23341197/, в противном случае трудно сказать, нет ли полного воспроизводящегося проекта.   -  person Simon Mourier    schedule 22.04.2021
comment
@SimonMourier Мой вопрос - это просто дубликат того, на который вы ссылались. Мне удалось решить проблему, включив в манифест правильные определения интерфейса. Спасибо!   -  person RMart    schedule 23.04.2021