Skip to content

Invoking method of .NET 5 COM object (inproc server) from .NET 5 client app using the 'dynamic' keyword throws 'NotSupportedException' #47329

Closed
@lauxjpn

Description

@lauxjpn

Dynamic support for COM objects has been added in .NET 5 (see #12587).
However, I am unable call methods of a .NET 5 COM class (inproc server) from a .NET 5 client app using the dynamic keyword.

I used the COM Server Demo sample as a base and altered it in a way that I would expect to work when invoked dynamically.

The COMClient\WscriptClient.js script executes correctly and demonstrates, that IDispatch is implemented by the CCW and works as expected:

PS E:\Sources\COM\COMClient> cscript.exe .\WScriptClient.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

PI: 3.140616091322624

The script is simple:

// Works as expected:
var server = new ActiveXObject("ComServerVbs.ServerVbs");
var pi = server.ComputePi();

WScript.Echo("PI: " + pi);

However, when running the COMClient.exe, I get the following exception when dynamically invoking the ComputePi() method:

Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.dll
An unhandled exception of type 'System.NotSupportedException' occurred in System.Private.CoreLib.dll
Specified method is not supported.
Full stack trace
   at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.cs:line 601
   at Microsoft.CSharp.RuntimeBinder.ComInterop.ComRuntimeHelpers.GetITypeInfoFromIDispatch(IDispatch dispatch) in /_/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/ComInterop/ComRuntimeHelpers.cs:line 120
   at Microsoft.CSharp.RuntimeBinder.ComInterop.IDispatchComObject.EnsureScanDefinedMethods() in /_/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/ComInterop/IDispatchComObject.cs:line 633
   at Microsoft.CSharp.RuntimeBinder.ComInterop.IDispatchComObject.System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) in /_/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/ComInterop/IDispatchComObject.cs:line 319
   at System.Dynamic.DynamicMetaObject.Create(Object value, Expression expression) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/DynamicMetaObject.cs:line 287
   at System.Dynamic.DynamicMetaObjectBinder.Bind(Object[] args, ReadOnlyCollection`1 parameters, LabelTarget returnLabel) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/DynamicMetaObjectBinder.cs:line 87
   at System.Runtime.CompilerServices.CallSiteBinder.BindCore[T](CallSite`1 site, Object[] args) in /_/src/libraries/System.Linq.Expressions/src/System/Runtime/CompilerServices/CallSiteBinder.cs:line 128
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/UpdateDelegates.Generated.cs:line 115
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/UpdateDelegates.Generated.cs:line 124
   at COMClient.Program.Main(String[] args) in E:\Sources\COM_Vbs\COMClient\Program.cs:line 18

The script is simple as well:

using System;

namespace COMClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var serverType = Type.GetTypeFromCLSID(new Guid(ContractGuids.ServerClass));
            var serverObject = Activator.CreateInstance(serverType);

            // This works:
            // var server = (IServer) serverObject;

            // This does not work:
            dynamic server = serverObject;

            var pi = server.ComputePi(); // <-- throws System.NotSupportedException (for dynamic call)
            Console.WriteLine($"\u03C0 = {pi}");
        }
    }
}

According to the full stack trace, the exception is being thrown in ComRuntimeHelpers.GetITypeInfoFromIDispatch(), where it is being thrown for the HRESULT of dispatch.TryGetTypeInfoCount(out uint typeCount).

This might be a bug. The remarks section states:

Some COM objects just dont expose typeinfo. In these cases, this method will return null.
Some COM objects do intend to expose typeinfo, but may not be able to do so if the type-library is not properly
registered. This will be considered as acceptable or as an error condition depending on throwIfMissingExpectedTypeInfo

The docs at COM Callable Wrapper: Simulating COM interfaces state, that ITypeInfo is not implemented by the CCW for .NET Core:

Interface Description
ITypeInfo (.NET Framework only) Provides type information for a class that is exactly the same as the type information produced by Tlbexp.exe.

Which might be indirectly backed up by #3740.

However, if a scripting host like WScript is able to dynamically call a dual interface via IDispatch, I would expect the same to be true for a .NET 5 client (at least for common cases).


I also created another project version, that explicitly generates a type library from an IDL file using the MIDL.exe tool (I changed the GUIDs for this project version).

Contract.idl
[
    uuid(B2EE0DB3-972B-4AE3-95DD-9DB2AD5B6CDB),
    version(1.0)
]
library COMServer
{
    importlib("stdole2.tlb");

    [
        object,
        oleautomation,
        dual,
        uuid(402FB956-E484-4C25-8A89-8E26C5B588CA),
        version(1.0)
    ]
    interface IServer : IDispatch {
        [id(1)]
        HRESULT ComputePi([out, retval] double* pRetVal);
    };

    [
        uuid(65012759-8F78-40EC-8BB7-48741B178251),
        version(1.0)
    ]
    coclass Server {
        [default] interface IServer;
    };
};

When the COM class is registered, the project also registers the type library (via [ComRegisterFunction]).

When my Server COM class is instantiated, I load the type library and retrieve the default ITypeInfo interface (provided by the type library parser) in the class constructor (see Essential COM page 353 by @donbox):

[ComVisible(true)]
[Guid(ContractGuids.ServerClass)]
[ProgId("ComServerTlb.ServerTlb")]
[ComDefaultInterface(typeof(IServer))]
public class Server : IServer, ITypeInfo
{
    private ITypeInfo _typeInfo;

    public Server()
    {
        var libid = new Guid(ContractGuids.TypeLibrary);
        Marshal.ThrowExceptionForHR(
            TypeLib.OleAut32.LoadRegTypeLib(ref libid, 1, 0, 0, out var typeLib));
        
        var iidIServer = new Guid(ContractGuids.ServerInterface);
        typeLib.GetTypeInfoOfGuid(ref iidIServer, out _typeInfo);
    }

    // ...

    #region ITypeInfo

    void ITypeInfo.AddressOfMember(int memid, INVOKEKIND invKind, out IntPtr ppv)
        => _typeInfo.AddressOfMember(memid, invKind, out ppv);

    // ...

My Server COM class explicitly implements ITypeInfo and forwards all calls to the retrieved default implementation. According to COM Callable Wrapper: Simulating COM interfaces, my explicit ITypeInfo implementation should be honored:

A .NET class can override the default behavior by providing its own implementation of these interfaces.

The COMClient project demonstrates, that the type information is available via the interface, but the internal ComRuntimeHelpers.GetITypeInfoFromIDispatch() call by .NET when dynamically invoking ComputePi() throws the same NotSupportedException as before:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices.ComTypes;

namespace COMClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var serverType = Type.GetTypeFromCLSID(new Guid(ContractGuids.ServerClass));
            var serverObject = Activator.CreateInstance(serverType);

            // ITypeInfo is available and returns expected data.
            var typeInfo = (ITypeInfo)serverObject;
            
            string[] names = new string[256];
            typeInfo.GetNames(1, names, 256, out var namesCount);
            
            Trace.Assert(namesCount == 1);
            Trace.Assert(names[0] == "ComputePi");

            // This works:
            // var server = (IServer) serverObject;

            // This does not work:
            dynamic server = serverObject;

            var pi = server.ComputePi(); // <-- throws System.NotSupportedException (for dynamic call)
            Console.WriteLine($"\u03C0 = {pi}");
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions