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:
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:
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
private interface IDispatchInfo
{
[PreserveSig]
int GetTypeInfoCount(out int typeInfoCount);
void GetTypeInfo(int typeInfoIndex, int lcid,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef =
typeof(System.Runtime.InteropServices.CustomMarshalers.TypeToTypeInfoMarshaler))]
out Type typeInfo);
[PreserveSig]
int GetDispId(ref Guid riid, ref string name, int nameCount, int lcid,
out int dispId);
}
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 DISPID
s, and to dynamically invoke members by name or DISPID
.
public static class DispatchUtility
{
public static bool ImplementsIDispatch(object obj) { ... }
public static Type GetType(object obj, bool throwIfNotFound) { ... }
public static bool TryGetDispId(object obj, string name, out int dispId) { ... }
public static object Invoke(object obj, int dispId, object[] args) { ... }
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()
:
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