Click here to Skip to main content
15,881,173 members
Articles / Programming Languages / C#

Reflection with IDispatch-based COM objects

Rate me:
Please Sign up or sign in to vote.
4.86/5 (29 votes)
24 Mar 2022CPOL5 min read 67.6K   3.3K   35   21
Use .NET's TypeToTypeInfoMarshaler to get a full .NET type with member information from an IDispatch-based COM object.

Console

Introduction

.NET's Reflection API provides rich information about a managed type's properties, methods, and events. However, it doesn't work as well for unmanaged COM types. The closest thing COM had to reflection was IDispatch's ability to return ITypeInfo, and .NET's reflection API doesn't automatically use ITypeInfo for an IDispatch-based COM object. It's usually possible to get rich type information, but it takes some additional work via a custom declaration of IDispatch using .NET's built-in TypeToTypeInfoMarshaler.

Background

If you're working with a strongly-typed COM object where you've referenced an interop assembly (e.g., a PIA or one generated by TlbImp.exe), then rich type information is automatically available via reflection through the runtime callable wrapper. However, if you've just been passed an object of unknown type (e.g., one created by unmanaged code or by Activator.CreateInstance), then using reflection on it may be disappointing. If the object is an unmanaged COM object, then the default reflection results will be for the System.__ComObject type, which is an internal type defined in mscorlib.dll. For example:

C#
Type fsoType = Type.GetTypeFromProgID("Scripting.FileSystemObject");
object fso = Activator.CreateInstance(fsoType);
Console.WriteLine("TypeName: {0}", fso.GetType().FullName);
foreach (MemberInfo member in fso.GetType().GetMembers())
{
    Console.WriteLine("{0} -- {1}", member.MemberType, member);
}

Produces the output:

TypeName: System.__ComObject
Method -- System.Object GetLifetimeService()
Method -- System.Object InitializeLifetimeService()
Method -- System.Runtime.Remoting.ObjRef CreateObjRef(System.Type)
Method -- System.String ToString()
Method -- Boolean Equals(System.Object)
Method -- Int32 GetHashCode()
Method -- System.Type GetType()

Getting the type information for .NET's System.__ComObject is rarely useful. It's much better to get the type information from the underlying COM object's IDispatch implementation, but that takes a little more work. The DispatchUtility class (in the attached sample code) privately declares the IDispatchInfo interface using IDispatch's interface ID (IID), but it only declares the first three methods of IDispatch since that's all we need to get the type information:

C#
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
private interface IDispatchInfo
{
    // Gets the number of Types that the object provides (0 or 1).
    [PreserveSig]
    int GetTypeInfoCount(out int typeInfoCount);

    // Gets the Type information for an object if GetTypeInfoCount returned 1.
    void GetTypeInfo(int typeInfoIndex, int lcid,
        [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef =
        typeof(System.Runtime.InteropServices.CustomMarshalers.TypeToTypeInfoMarshaler))]
        out Type typeInfo);

    // Gets the DISPID of the specified member name.
    [PreserveSig]
    int GetDispId(ref Guid riid, ref string name, int nameCount, int lcid, 
        out int dispId);

    // NOTE: The real IDispatch also has an Invoke method next, but we don't need it.
}

The real work of gathering and translating the type information for .NET is handled by the TypeToTypeInfoMarshaler on IDispatchInfo.GetTypeInfo's last parameter. The original IDispatch interface (declared in the Windows SDK's OAIdl.idl file) declares GetTypeInfo with an output of ITypeInfo, but .NET's TypeToTypeInfoMarshaler will turn that into a rich .NET Type instance.

IDispatchInfo also provides a simplification of IDispatch.GetIDsOfNames that only works for one name at a time. It declares the method as GetDispId instead, and it adjusts the parameter declarations so they'll work correctly for a single ID and name.

IDispatchInfo omits the fourth IDispatch method (i.e., Invoke) because there are already several ways to do dynamic invocation in .NET (e.g., via Type.InvokeMember using the "[DISPID=n]" syntax or via C#'s dynamic keyword). Since the first three methods provide metadata discovery for type information and DISPIDs, they're all we really need.

Note: It's safe to use this partial implementation of IDispatch when requesting information from an existing unmanaged COM object because the C# compiler will generate this interface's vtable the same as the original IDispatch interface up through its first three methods. However, it would not be safe to implement IDispatchInfo on a managed object and pass it to any unmanaged code that expected a real IDispatch. With only a partial IDispatch vtable all kinds of badness could happen (e.g., access violations and memory corruption) if something tried to use later vtable members such as Invoke. That's one reason IDispatchInfo is declared as a private nested interface in DispatchUtility.

Using the Code

The DispatchUtility class provides static methods to check if an object implements IDispatch, to get .NET Type information, to get DISPIDs, and to dynamically invoke members by name or DISPID.

C#
public static class DispatchUtility
{
    // Gets whether the specified object implements IDispatch.
    public static bool ImplementsIDispatch(object obj)  { ... }

    // Gets a Type that can be used with reflection.
    public static Type GetType(object obj, bool throwIfNotFound) { ... }

    // Tries to get the DISPID for the requested member name.
    public static bool TryGetDispId(object obj, string name, out int dispId) { ... }

    // Invokes a member by DISPID.
    public static object Invoke(object obj, int dispId, object[] args) { ... }

    // Invokes a member by name.
    public static object Invoke(object obj, string memberName, object[] args) { ... }
}

We can modify the earlier sample code to use DispatchUtility.GetType(fso, true) instead of fso.GetType():

C#
Type fsoType = Type.GetTypeFromProgID("Scripting.FileSystemObject");
object fso = Activator.CreateInstance(fsoType);
Type dispatchType = DispatchUtility.GetType(fso, true);
Console.WriteLine("TypeName: {0}", dispatchType.FullName);
foreach (MemberInfo member in dispatchType.GetMembers())
{
    Console.WriteLine("{0} -- {1}", member.MemberType, member);
}

That produces the following output:

TypeName: Scripting.IFileSystem3
Method -- Scripting.Drives get_Drives()
Method -- System.String BuildPath(System.String, System.String)
Method -- System.String GetDriveName(System.String)
Method -- System.String GetParentFolderName(System.String)
Method -- System.String GetFileName(System.String)
Method -- System.String GetBaseName(System.String)
Method -- System.String GetExtensionName(System.String)
Method -- System.String GetAbsolutePathName(System.String)
Method -- System.String GetTempName()
Method -- Boolean DriveExists(System.String)
Method -- Boolean FileExists(System.String)
Method -- Boolean FolderExists(System.String)
Method -- Scripting.Drive GetDrive(System.String)
Method -- Scripting.File GetFile(System.String)
Method -- Scripting.Folder GetFolder(System.String)
Method -- Scripting.Folder GetSpecialFolder(Scripting.SpecialFolderConst)
Method -- Void DeleteFile(System.String, Boolean)
Method -- Void DeleteFolder(System.String, Boolean)
Method -- Void MoveFile(System.String, System.String)
Method -- Void MoveFolder(System.String, System.String)
Method -- Void CopyFile(System.String, System.String, Boolean)
Method -- Void CopyFolder(System.String, System.String, Boolean)
Method -- Scripting.Folder CreateFolder(System.String)
Method -- Scripting.TextStream CreateTextFile(System.String, Boolean, Boolean)
Method -- Scripting.TextStream OpenTextFile
          (System.String, Scripting.IOMode, Boolean, Scripting.Tristate)
Method -- Scripting.TextStream GetStandardStream(Scripting.StandardStreamTypes, Boolean)
Method -- System.String GetFileVersion(System.String)
Property -- Scripting.Drives Drives

With DispatchUtility.GetType, we get rich type information such as property and method details and a good interface type name. This is much better than getting the members of System.__ComObject. This works because a type library is registered for the COM type we're using, so when DispatchUtility internally calls IDispatch.GetTypeInfo, it is able to return an ITypeInfo. Then .NET's TypeToTypeInfoMarshaler turns the ITypeInfo into a .NET Type.

If the IDispatch.GetTypeInfo method can't return an ITypeInfo (e.g., if there's no type library registered for that object), then we won't be able to get a .NET Type instance. This limitation affects most "expando" objects that implement IDispatchEx where members can be added and removed dynamically at run-time (e.g., JScript objects). Typically, ITypeInfo will only return static type information, so dynamically added members won't be reported for IDispatchEx-based objects.

The DispatchUtility class is implemented in a single file, so it is easy to integrate into an existing project. The class only requires assembly references to System.dll and CustomMarshalers.dll, which are part of the core .NET Framework. It should work on .NET 2.0 or later for "Any CPU". On .NET 4.0 or later the LinkDemands for UnmanagedCode permission aren't needed and can be ignored or removed.

Points Of Interest

Many articles on the web incorrectly say that you must reference a COM interop assembly to get rich type information in .NET, such as Microsoft Support article 320523 and StackOverflow posts "How to enumerate members of COM object in C#?" and "Get property names via reflection of an COM Object". Some more advanced articles say you should work with ITypeInfo directly such as "Inspecting COM Objects With Reflection" and "Obtain Type Information of IDispatch-Based COM Objects from Managed Code". Unfortunately, using ITypeInfo directly involves lots of manual interop work, and it doesn't give you a System.Type instance. However, using the TypeToTypeInfoMarshaler as discussed in this article is much easier, and it provides the rich type information in a standard .NET Type format.

History

  • 7th January, 2013: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Programmer, runner, GT MSCS

Comments and Discussions

 
QuestionCustomMarshalers does not exist? Pin
AJ Weber12-Oct-22 4:38
AJ Weber12-Oct-22 4:38 
AnswerRe: CustomMarshalers does not exist? Pin
AJ Weber12-Oct-22 5:05
AJ Weber12-Oct-22 5:05 
QuestionGood Pin
Leeladhar Ladia25-Mar-22 2:59
Leeladhar Ladia25-Mar-22 2:59 
QuestionNot work in Core3.1 console app. Pin
sheping zhu19-Feb-20 15:37
sheping zhu19-Feb-20 15:37 
AnswerRe: Not work in Core3.1 console app. Pin
Salami Army (Ashley Lewis)8-Nov-21 20:32
Salami Army (Ashley Lewis)8-Nov-21 20:32 
QuestionPoor attempt at VB - MarshalDirectiveException Pin
R.D.H.8-Jul-19 10:21
R.D.H.8-Jul-19 10:21 
QuestionAttempting to import the DispatchUtility into a PowerShell session produces an Assembly import error Pin
lapc5069-May-18 8:56
lapc5069-May-18 8:56 
AnswerRe: Attempting to import the DispatchUtility into a PowerShell session produces an Assembly import error Pin
Dimitri Rodis25-Dec-22 20:32
Dimitri Rodis25-Dec-22 20:32 
QuestionCannot access COM Object method Pin
cgtyoder6-Apr-17 11:00
cgtyoder6-Apr-17 11:00 
QuestionAccess violation when using GetType with an object from the web browser control Pin
Berti Burger1-Sep-16 5:26
Berti Burger1-Sep-16 5:26 
BugRe: Access violation when using GetType with an object from the web browser control Pin
Davelister16-Sep-16 13:56
Davelister16-Sep-16 13:56 
QuestionPerformance Pin
Jens Madsen, Højby10-Aug-15 19:42
Jens Madsen, Højby10-Aug-15 19:42 
QuestionWorks like a charm Pin
Patrick Kursawe10-Jun-15 2:55
Patrick Kursawe10-Jun-15 2:55 
Questionmodify properties of an object-classic ASP sent by reference to a method COM + Pin
Manuel Alegre12-Mar-15 8:14
Manuel Alegre12-Mar-15 8:14 
AnswerRe: modify properties of an object-classic ASP sent by reference to a method COM + Pin
Bill Menees12-Mar-15 16:30
Bill Menees12-Mar-15 16:30 
GeneralRe: modify properties of an object-classic ASP sent by reference to a method COM + Pin
Manuel Alegre13-Mar-15 10:00
Manuel Alegre13-Mar-15 10:00 
GeneralRe: modify properties of an object-classic ASP sent by reference to a method COM + Pin
Manuel Alegre13-Mar-15 23:18
Manuel Alegre13-Mar-15 23:18 
QuestionPerformance issue Pin
Mathieu Beliveau12-May-14 4:42
Mathieu Beliveau12-May-14 4:42 
AnswerRe: Performance issue Pin
Bill Menees16-May-14 10:46
Bill Menees16-May-14 10:46 
QuestionMy vote of 5 Pin
David Fenstemaker12-Feb-14 6:08
David Fenstemaker12-Feb-14 6:08 
GeneralMy vote of 4 Pin
Jens Madsen, Højby28-Sep-13 15:01
Jens Madsen, Højby28-Sep-13 15:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.