Introduction
>This articles is based on the .NET 2.0 Beta, but the principals should work perfectly for the released version.Even though .NET 2005 Beta 2 is supposed to be "feature complete" for version 2.0 of the Microsoft .NET Framework, it is not easy to find all the changes and additions to the namespaces and types compared to the other shipped versions. In the first article of this series I covered using reflection to dig into the CLR, I explained where assemblies are stored, how things changed in .NET 2005 and which method to use to get a System.Reflection.Assembly object corresponding to the assembly you want to examine. The second article took a look at using the Assembly class to explore an assembly. This the third and last article will explore the details of using the Type class to explore an assembly. In particular, the areas I'll cover are:
- Generics and reflection , in .NET v2.0 the new notion of generics becomes common within the CLR. I show how the Reflection API has been updated to take generics into account for type members.
- Fields , in the case of static fields, it is interesting to get the values of these constants.
- Properties , the accessibility of these members is not straightforward.
- Constructors, methods and delegate handlers . The Reflection API allows you to find out the parameters and return type of any method and in this article you will see how to apply this knowledge to know the signature of event handlers.
- P/Invoke signatures , I talk about using unmanaged reflection to decypher the P/Invoke wrapper methods. This is a great way to find how the CLR itself is defining the right way to call the Windows APIs.
- Enumeration can involve either flags that simplify bitwise operations or simple numeric values. I'll show you how to make the difference and in the case of flags, how to optimize the string representation, especially with the usage of masks.
- New constructs in Managed C++ . in .NET v2.0 Managed C++ has been greatly enhanced and the syntax simplified. You'll see how you should change your coding habits to reflect these updates.
System Requirements
To run the code for this sample you should have:
Installing and Compiling the Sample Code
Installing and Compiling the Sample Code
The C# sample download for this series contains several VS .NET solutions for C# and C++ projects. Note that the download for this article is identical to the downloads for the other two articles of the series.
- LoadClrAssembly11 : A console application test to load mscorlib.dll and system.dll compiled with CLR v1.1
- LoadClrAssembly : A console application test to load mscorlib.dll and system.dll compiled with CLR v2.0 beta 2
- LoadedAssemblies : Sample code for v1.1\.NET 2005 beta 1 + beta 2 executables to see what assemblies are loaded. To compile on your machine, open a VS Command Prompt and type "csc /out:AssembliesXXX program.cs" where XXX is the version of the CLR you are using to compile.
- CLRDump11 : A tool to dump assemblies compiled with CLR v1.1
- CLRDumpBeta1 : A tool to dump assemblies compiled with CLR v2.0 beta 1
- CLRDump : A tool to dump assemblies compiled with CLR v2.0 beta 2
Dumping Type Content
In the previous two articles, I explained how to list and detail the types defined within assemblies sorted by namespaces. In this article I plan to cover how to find and describe each kind of type member: nested types, constructors, methods and operators, properties, events and fields.
Before analyzing type members, a last category of types needs to be enumerated. The nested types are not taken into account at the assembly level by the GetTypes() or GetExportedTypes() methods. They are detected at the level of the type defining them through Type.GetNestedType() (see the DumpNestedTypes() method in CLRContent.cs for the implementation details). The Microsoft documentation for this parameterless Type method is misleading when it states that it returns "an array of Type objects representing all the types nested within the current Type". In fact, it only returns the public nested types. If you want all the types you have to use the overload that takes a BindingFlags as a parameter and pass BindingFlags.Public | BindingFlags.NonPublic .
It is possible to define five different kinds of members for a type: constructors, methods, properties, events and fields. Given a Type object, several ways exist to retrieve the list of its members. You can either use GetMembers( , which returns all kinds of members, or the method dedicated to a specific kind. Anyway, all of them have a parameterless overload that returns only public members. The other overload takes a BindingFlags enumeration parameter used to filter the returned members. Obviously, the BindingFlags.Public and BindingFlags.NonPublic values allow you to choose the accessibility level you need; BindingFlags.Static is perfect for static members, while BindingFlags.Instance requests non static members. As you'll see later on, the two DeclaredOnly and FlattenHierarchy BindingFlags values are also useful to outline inheritance relationships.
For each kind of member, I build a string that concatenates each detail that can be different. Once built, this text can be used to detect the difference between two versions of the same type (see the GetXXXSignature() methods, such as GetConstructorSignature() or GetMethodSignature() in ReflectionHelpers.cs for implementation details).
Constructors and Methods
The constructors are listed in the DumpConstructors() helper that retrieves them in two steps, using Type.GetConstructors() . When you ask for the Name property of the ConstructorInfo objects returned by GetConstructors() , you either get . cctor or . ctor depending on whether they are static or not. If you are used to seeing the name of the type as the name of a constructor, you can easily retrieve this type using the DeclaringType property from the ConstructorInfo object. Don't be afraid to detect a constructor for your type even though you have not defined one yourself, it is automatically created by the compiler and it simply calls the base class constructor.
When a constructor or a method is defined with parameters, it is possible to get their description through an array of ParameterInfo objects returned by the GetParameters() method. The ReflectionHelpers.GetParametersSignature() method takes a look at each of these ParameterInfo objects given as parameter, and builds the returned string with the following format: (Type FirstParam,…, Type LastParam) . In addition to the parameter type and name obtained through ParameterInfo.ParameterType.Name and the ParameterInfo.Name property, it is also possible to know if a parameter is "in" ( IsIn property), "out" ( IsOut property), "ref" ( IsRef property), or "retval" ( IsRetval property). The C++ Managed Extensions and VB.NET allow you to define optional parameters with default values. In that case, the IsOptional property returns true and DefaultValue returns the object corresponding to this default value. In order to discriminate between no default value and a null default value, the DefaultValue property returns DBNull.Value for the former and null for the latter. If any part of a constructor or a method signature changes, it must be seen as a difference in the contract, especially when the accessibility is public.
The methods of a given type are retrieved using GetMethods() with the same BindingFlags parameters as the GetConstructors() method. Each call returns an array of MethodInfo objects, actually one per method. It is interesting to note that no constructor is returned in this array while properties accessors and overloaded operators are present. These methods, mostly used by compilers and the CLR, are detected by the MethodInfo.IsSpecialName property, which is checked by the DumpMethods() to avoid listing both properties as you will see later on, and their corresponding get_ / set_ prefixed accessors as methods. Also, when you define an event in C#, the compiler creates behind the scene two helper methods prefixed by add_ and remove_ . Those helpers are called behind the scene when you add or remove a delegate to an event in C# using respectively += and -= syntax. But from a comparison point of view, these special methods are not used: The rich metadata attached to properties and events are sufficient as you will soon see.
The overloaded operators are the last kind of specially named methods, they are all prefixed by op_ . The C# syntax used to define such an operator is not trivial, and personally I would prefer to see this as the way it is used rather than its C# syntax. For example, the CLR allows you to get a TimeSpan when you subtract two DateTime s by defining the following DateTime static method: TimeSpan op_Subtraction(DateTime d1, DateTime d2) . This allows you to write the following piece of code:
DateTime yesterday; … DateTime today; … TimeSpan duration = today - yesterday;
For a better output, the latter syntax TimeSpan = DateTime - DateTime should be shown, while, you get the former method-oriented syntax ( op_Subtraction …) if you just use the MethodInfo.Name property. If you are interested in the implementation details for formatting each possible operator, you should take a look at the Operators class. Since each operator is defined by a MethodInfo , you already know how to extract the return type using the ReturnType or any parameter with my GetParameters() helper method.
The prototype of a method is built by the GetMethodSignature() helper the same way as constructors, but I also need additional pieces of information to be displayed. First, in the case of interfaces, the methods are pure virtual and = 0 is added, they are detected if MethodInfo.IsAbstract returns true. Second, it would be nice to see when a method is a wrapper to an unmanaged function exported by a DLL.
Decyphering P/Invoke Wrappers
This kind of method is defined in C# with a syntax based on the attachment of a System.Runtime.InteropServices.DllImportAttribute with the name of the DLL, the function to be imported, and other details such as the charset to expect for string parameters. When I first tried to detect such a wrapper, I followed the "attribute" path, thinking that, from the MethodInfo , I could either call GetCustomAttributes() with a DllImportAttribute type as parameter or Attribute.GetCustomAttributes . Unfortunately, until version 2.0 of the .NET Framework, DllImportAttribute was not handled as a custom attribute and was not taken into account by the previous methods. Digging further into the managed reflection, the value returned by MethodInfo.Attributes property contains the MethodAttributes.PinvokeImpl flag for such a wrapper. The next step is to find out the name of the DLL and the imported function.
So, with version 1.x of the .NET Framework, it is necessary to return to the unmanaged world. The .NET Framework SDK Metadata Unmanaged API.doc documentation file provides a hint and the source code of ILDASM found in the Rotor distribution providing the implementation details. I have built my own Managed C++ wrapper method named GetPInvokeInfo() to retrieve the DLL and the imported function, given an assembly, a type and a method (see ReflectHelper.cpp for the complete implementation details):
bool ReflectHelperClass::GetPInvokeInfo(
String* szAssembly,
String* szType,
String* szMethod,
String** szDll,
String**
szAPI
)
{
// variable declaration
...
__try
{
// convert the managed strings into UNICODE strings
szPtrAssembly = Marshal::StringToCoTaskMemUni(szAssembly);
wchar_t* wszAssemblyName = (wchar_t*)szPtrAssembly.ToPointer();
SzPtrType = Marshal::StringToCoTaskMemUni(szType);
wchar_t* wszTypeName = (wchar_t*)SzPtrType.ToPointer();
SzPtrMethod = Marshal::StringToCoTaskMemUni(szMethod);
wchar_t* wszMethodName = (wchar_t*)SzPtrMethod.ToPointer();
// bind to the unmanaged API if needed
hr =
::CoCreateInstance(
CLSID_CorMetaDataDispenser,
0,
CLSCTX_INPROC_SERVER,
IID_IMetaDataDispenser,
(void**)&pIMetaDataDispenser
);
if (FAILED(hr))
{
// this should never occur...
return(false);
}
// grab unmanaged reflection interface corresponding to the assembly
hr =
pIMetaDataDispenser->OpenScope(
wszAssemblyName,
ofRead,
IID_IMetaDataImport,
(IUnknown**)&pIMetaDataImport
);
if (FAILED(hr))
{
return(false);
}
// get the type
mdTypeDef tkType = mdTokenNil;
hr = pIMetaDataImport->FindTypeDefByName(
wszTypeName,
mdTokenNil,
&tkType
);
if (FAILED(hr))
{
return(false);
}
// get the method
mdMethodDef tkMethod = mdTokenNil;
hr = pIMetaDataImport->FindMethod(
tkType,
wszMethodName,
NULL,
0,
&tkMethod
);
if (FAILED(hr))
{
return(false);
}
// get the PInvoke information
DWORD dwMappingFlags;
wchar_t wszImportMethod[512+1];
DWORD dwCopiedChar = 0;
mdModuleRef tkImportDLL;
hr = pIMetaDataImport->GetPinvokeMap(
tkMethod,
&dwMappingFlags,
wszImportMethod,
512,
&dwCopiedChar,
&tkImportDLL
);
if (FAILED(hr))
{
return(false);
}
// get the import DLL
wchar_t wszImportDLL[_MAX_PATH];
hr = pIMetaDataImport->GetModuleRefProps(
tkImportDLL,
wszImportDLL,
_MAX_PATH,
&dwCopiedChar
);
if (FAILED(hr))
{
return(false);
}
// format the expected result
*szDll = new String(wszImportDLL);
*szAPI = new String(wszImportMethod);
return(true);
}
__finally
{
// don't forget to release marshalled strings
…
}
return(false);
}
The workflow is basically the same as explained in the GetDefinedTypes() in order to get a pointer to the IMetaDataImport interface corresponding to the assembly. Second, instead of enumerating all the types until the one you are interested in is found, FindTypeDefByName() directly returns a token for the given type. With the method name and the type token, FindMethod() returns a method token usable by GetPinvokeMap() .
Given such a method token, in addition to a token for the DLL, GetPinvokeMap() returns the name of the imported function and some mapping flags such as charset details (see CorPinvokeMap enumeration in corhdr.h ). The final missing information is the name of the DLL corresponding to the module reference token, but it is easily returned by GetModuleRefProps() .
I have to warn you here, detecting P/Invoke signatures generates a significant performance hit.
Methods and Generics
As you have seen for types, the version 2.0 of the CLR allows you to create generics. In addition to generic types, you can define generic methods and properties as well:
public class GenericClass<T>
where T : new()
{
public void NotGenericMethod(T t)
{
}
public void OverGenericMethod<TT>(TT tt)
{
}
delegate T OverGenericDelegate<TT>(T t, TT tt);
T t
{
get
{
return(new T());
}
}
public void TriggerGenericEvent(T t)
{
if (gNotify != null)
{
T tResult = gNotify(t);
Console.WriteLine(tResult.ToString());
}
}
public delegate T GenericDelegate(T t);
public event GenericDelegate gNotify;
}
As you can see, almost every member can have generic type parameters. It is even possible to define a generic method inside a non generic type such as:
public class NonGenericClass
{
public void GenericMethod<T>(T t)
{
Console.WriteLine(t.ToString());
}
}
Here is the source code to get the name of a method using C#:
static private string GetMethodName(MethodInfo CurrentMethod)
{
// take into account generics
StringBuilder sbText = new StringBuilder();
// Note: FullName contain the namespace (.) and containing type (+)
sbText.Append(CurrentMethod.Name);
// check for generics
if (CurrentMethod.IsGenericMethod)
{
Type[] GenericParameters = CurrentMethod.GetGenericArguments();
if (GenericParameters.Length > 0)
{
sbText.Append("<");
Type CurrentGenericParameter = null;
for (
int CurrentParameter = 0;
CurrentParameter < GenericParameters.Length;
CurrentParameter++
)
{
CurrentGenericParameter =
GenericParameters[CurrentParameter];
sbText.Append(GetTypeName(CurrentGenericParameter));
if (CurrentParameter < GenericParameters.Length-1)
sbText.Append(", ");
}
sbText.Append(">");
}
}
return(sbText.ToString());
}
The IsGenericMethod property is used to detect a method with generic parameters. Don't get me wrong here, this does not mean IsGenericMethod returns true when a method has a generic in its parameters. It rather returns true when the method itself is generic. For example, it is true for NonGenericClass.GenericMethod<T>(T t) but false for GenericClass<T>.NotGenericMethod(T t) . Note that the MethodInfo.HasGenericArguments property will be deprecated before .NET 2005 ships and IsGenericMethod should be used instead. Not a bad decision because its name would have been misleading.
Decyphering Enumeration Flags
Some additional pieces of information are also available for a method or a constructor through the GetMethodImplementationFlags or the Attributes property. The former returns MethodImplAttributes and the latter MethodAttributes ,both are enumerations wrongly described as having the FlagsAttribute in the documentation (same thing for EventAttributes and FieldAttributes ). This does not seem really important until you try to get a textual representation using ToString() which will return the numeric value instead of the concatenation of each ORed value for a flag bitwise enumeration. Even if you force ToString() to look for flags with the F parameter, you might get fuzzy output because some values are defined to be used as ORed masks and they will unnecessarily appear.
The best way to know how to the extract meaningful information from these enumerations is to find their corresponding unmanaged siblings from the file Cor.h ; CorMethodImpl for the former and CorMethodAttr for the latter are listed here:
typedef enum CorMethodAttr
{
// member access mask - Use this mask to retrieve accessibility
information.
mdMemberAccessMask = 0x0007,
mdPrivateScope = 0x0000, // Member not referenceable.
mdPrivate = 0x0001, // Accessible only by the
// parent type.
mdFamANDAssem = 0x0002, // Accessible by sub-types
// only in this Assembly.
mdAssem = 0x0003, // Accessibly by
//anyone in the Assembly.
mdFamily = 0x0004, // Accessible only by type
//and sub-types.
mdFamORAssem = 0x0005, // Accessibly by
// sub-types anywhere, plus anyone in assembly.
mdPublic = 0x0006, // Accessibly by anyone
// who has visibility to this scope.
// end member access mask
// method contract attributes.
mdStatic = 0x0010, // Defined on type,
// else per instance.
mdFinal = 0x0020, // Method may not be
// overridden.
mdVirtual = 0x0040, // Method virtual.
mdHideBySig = 0x0080, // Method hides by
// name+sig, else just by name.
// vtable layout mask - Use this mask to retrieve vtable attributes.
mdVtableLayoutMask = 0x0100,
mdReuseSlot = 0x0000, // The default.
mdNewSlot = 0x0100, // Method always
// gets a new slot in the vtable.
// end vtable layout mask
...
} CorMethodAttr;
typedef enum CorMethodImpl
{
// code impl mask
miCodeTypeMask = 0x0003, // Flags about code type.
miIL = 0x0000, // Method impl is IL.
miNative = 0x0001, // Method impl is native.
miOPTIL = 0x0002, // Method impl is OPTIL
miRuntime = 0x0003, // Method impl is provided by
// the runtime.
// end code impl mask
// managed mask
miManagedMask = 0x0004, // Flags specifying whether
// the code is managed or unmanaged.
miUnmanaged = 0x0004, // Method impl is unmanaged,
// otherwise managed.
miManaged = 0x0000, // Method impl is managed.
// end managed mask
...
} CorMethodImpl;
For both, the first part of the enumeration holds a non flag type of enumeration, but the rest can be treated as a set of bitwise flags. To make a difference, some special values are used as masks and you see them highlighted in CorMethodAttr and CorMethodImpl definitions above. I have written the FormatFlags() helper method to get the textual representation of a partial set of bits corresponding to a mask from a mixed bitwise enumeration:
static string FormatFlags(Enum FlagEnum, Type EnumType, int iMask)
{
// get each defined value in the enumeration
FieldInfo[] EnumFields =
EnumType.GetFields(BindingFlags.Public | BindingFlags.Static);
// Since ToString returns a numeric value, we are using int.Parse
// to get an int.
// It is possible to use its numeric value as a bitmask; which is
// not the case with the enum itself
StringBuilder szInfo = new StringBuilder();
int iValue = int.Parse(FlagEnum.ToString());
// only take into account the masked values
iValue &= iMask;
for (int i = 0; i < EnumFields.Length; i++)
{
// get the value in numeric format
int fieldvalue = (int)EnumFields[i].GetValue(null);
// need a special check for 0 value: (0 & xx == 0) is always true
if (fieldvalue != 0)
{
// make the bitwise comparison and
// display the corresponding string value
if ((fieldvalue & iValue) == fieldvalue)
{
sbInfo.AppendFormat("{0} ", EnumFields[i].Name);
}
}
}
// remove trailing spaces if any
if (szInfo.Length > 0)
szInfo.Remove(szInfo.Length-1, 1);
return(szInfo.ToString());
}
This method is called with an enumeration object, its type and a bitmask corresponding to the area of the enumeration you are interested in. For example, given a MethodInfo named mi , FormatFlags(mi.Attributes, typeof(System.Reflection.MethodAttributes), 0xF0) returns the textual representation of the bits corresponding to the 0x10-0x80 range in the enumeration since 0xF0 = 0x10+0x20+0x40+0x80.
The implementation relies on the Enum.GetFields() method that returns each possible element of an enumeration with its associated value. The C# compiler does not allow you to use bitwise operators on an Enumeration object even though the underlying type is an integer. As explained previously, calling ToString() without a parameter returns the numeric value in textual form. The next step is to transform this string into an int using the Parse() method of the integer type. The given mask is applied to the resulting number to get the bits we need to compare each value in the enumeration. These values are available calling GetValue() on each FieldInfo object returned by GetFields() . Since a 0 value cannot be tested with a bitwise AND operator, it is not possible to know if the corresponding element should appear. For other values, if the bitwise comparison is a success, the element string name is retrieved from the FieldInfo using its Name property.
What's new in Managed C++ Destructors
I have a final detail to point out, related to type methods. Even though Finalize() and IDisposable are the credo in the C# world, Managed C++ still lets you create real destructors. Behind the scene, a fake __dtor() method is created by the compiler, and called when delete is executed. You can also do the same in C# but your destructor will be transformed into a Finalize() override at the IL and metadata level. The C++ __dtor() method has nothing special but its name. Hence, IsSpecialName returns false and its Attributes are Virtual , VtableLayoutMask , and NewSlot . Anyway, this is enough to replace __dtor() by ~ followed by the name of the class name to build a C++-like syntax.
This was how it worked under the 1.1 CLR, in v2.0 of the CLR things have changed a bit. In addition to drastically updating the syntax in a good way, Microsoft has also changed the semantics behind the notion of destructor and finalizer. You can find the details in the article deterministic finalization but it is easier to understand the Managed C++ side of the topic through a example but. Here is a simple reference type class defined with the new 2.0 syntax:
public ref class ReferenceTypeClass
{
public:
ReferenceTypeClass(void);
~ReferenceTypeClass(void);
!ReferenceTypeClass(void);
};
ReferenceTypeClass::ReferenceTypeClass(void)
{
Console::WriteLine("ReferenceTypeClass()");
}
ReferenceTypeClass::~ReferenceTypeClass(void)
{
Console::WriteLine("~ReferenceTypeClass()");
}
ReferenceTypeClass::!ReferenceTypeClass(void)
{
Console::WriteLine("!ReferenceTypeClass()");
}
As you can see, I have defined two methods in addition to the constructor:
- the destructor syntax stays the same with the class name prefixed by ~
- the finalizer method name is built following the same pattern but with a ! as the prefix
If you compile this class in an assembly and open the ObjectBrowser on it, you might be surprised by the signature of the finalizer as seen in Figure 1:

Figure 1. Object Browser showing a finalizer method
I have not defined my method to return an integer! Maybe this is a bug in Beta 2. However, let's debug the following simple console application to figure out what the compiler is really generating behind the scenes:
int main(array<System::String ^> ^args)
{
ReferenceTypeClass^ r1 = gcnew ReferenceTypeClass;
Console::WriteLine(L"before delete");
delete r1; Console::WriteLine(L"after delete"); ReferenceTypeClass^ r2 = gcnew ReferenceTypeClass; Console::WriteLine(L"last line of the main()"); return 0; }
Here is the resulting output to the console:
ReferenceTypeClass()
before delete
~ReferenceTypeClass()
after delete
ReferenceTypeClass()
last line of the main()
!ReferenceTypeClass()
Set a breakpoint in both the ~ and ! methods and see what happens. First, the destructor is called with the stacktrace seen in Figure 2:

Figure 2. Stack Trace for the destructor
Well… It seems that delete is calling IDisposable.Dispose() . which calls Dispose(true) that calls the ~ destructor method.
Then, when the ! finalizer method gets called during what seems to be the last garbage collection of the application:

Figure 3. Calling the finalizer method
A Finalize() method calls Dispose(false) which finally executes the finalizer.
As you can see, the Managed C++ compiler takes care of implementing this complicated pattern for you, creating a Finalize() and two Dispose() methods for you. Therefore, with .NET 2005 assemblies written in Managed C++, don't expect to find the same __dtor() method as in 1.1.
Dumping Properties
When you define a property in a type, the compiler generates a specific metadata entry in addition to the definition of its get/set accessors. The Type.GetProperties() method returns an array of PropertyInfo , each providing their name through the Name property, and their type with the PropertyType property. Unlike methods or constructors, you don't have access to the accessibility level of a property. I have defined an AccessibilityLevel enumeration that lists the possible values with a C# naming convention unlike CorMethodAttr or CorFieldAttr :
public enum AccessibilityLevel : int
{
Private = 0x00000001,
ProtectedAndAssembly = 0x00000002,
Internal = 0x00000003, // --> Assembly
Protected = 0x00000004, // --> Family
ProtectedInternal = 0x00000005, // --> FamilyOrAssembly
Public = 0x00000006,
}
No IsPublic or IsPrivate is defined by PropertyInfo . Instead, you have to take a look at the accessors. If CanRead returns true, GetGetMethod() lets you grab a MethodInfo which describes the get accessor. You should pass false as a parameter to this method if you only require a public accessor and true otherwise. For the set accessor, you simply have to follow the same pattern, this time using CanWrite and GetSetMethod . With a MethodInfo , you can either check the value returned by the Attributes property, in the range of MethodAttributes.MemberAccessMask whose value is 0x7, or rely on its IsPublic and siblings properties.
static AccessibilityLevel GetPropertyAccessibilityLevel(PropertyInfo pi)
{
// Note: this is a strange way to get the accessibility...
// but I need to look for accessors since a property
// has no native accessibility
if (pi.CanRead)
{
// try to get public GET accessor
MethodInfo mi = pi.GetGetMethod(true);
if (mi != null)
{
int iValue = int.Parse(mi.Attributes.ToString("D"));
return((AccessibilityLevel)(iValue & 0x7));
}
}
if (pi.CanWrite)
{
// try to get public SET accessor
MethodInfo mi = pi.GetSetMethod(true);
if (mi != null)
{
int iValue = int.Parse(mi.Attributes.ToString("D"));
return((AccessibilityLevel)(iValue & 0x7));
}
}
Debug.Fail("There is a bug into the Reflection :^)");
return(AccessibilityLevel.Public);
}
The System.Collections namespace contains a lot of types derived from IDictionary which allow you to pass an object as a key to get the corresponding stored object with an array-like syntax. In C#, when you define such an indexer, the syntax to follow is similar to a property for the get / set accessors, but the name of the property is this and the parameters are specified between square brackerts [ ] instead of parens ( ) .
// indexer
public int this[int x]
{
get
{
return(...);
}
}
When you build your assembly, the compiler creates a special property named Item instead, with the corresponding get_Item/set_Item accessors if needed. Since you can define more than one parameter for an indexer property, PropertyInfo.GetIndexParameters() returns an array of ParameterInfo , one for each parameter. Otherwise, for a standard parameterless property, instead of null , an empty array is returned. The already detailed GetParametersSignature() helper method builds the same textual layout of the parameters, same as what has been done for a constructor or a method.
Dumping Fields
Properties usually offer a public façade to protected or private internal fields. Type.GetFields() catches them all with the same BindingFlags you have already already seen. Each field is described by a FieldInfo object from which you extract the name and type using its Name and FieldType properties. Unlike properties, their accessibility level is available using the IsPublic , IsPrivate and sibling properties.
In C# and other managed languages, it is possible to define static , constant and readonly fields with a default value. Using reflection, you are able to detect those cases and retrieve their value and format it according to its type (see GetFieldSignature() ). Let's take a look at the following field definition for the MyFields class:
class MyFields
{
public int iInstance = 10;
public readonly int iReadOnly = 20;
public const int iConst = 30;
public static int iStatic = 40;
public static readonly int iStaticReadOnly = 50;
public static readonly string[] szInfos;
// C# does not allow static const
// public static const int iStaticConst = 60;
static MyFields()
{
// impossible to initialize non static fields
// in a static constructor
// iInstance = 11;
// iReadOnly = 21;
// iConst = 31;
iStatic = 41;
iStaticReadOnly = 51;
// initialize complicated fields
szInfos = new string[] {"First", "Middle", "Last"};
}
}
The meaningful information from the corresponding FieldInfo is listed below:
| Name | IsStatic | Attributes | GetValue(null) |
|---|---|---|---|
| iConst | true | Literal HasDefault | 30 |
| iReadOnly | false | InitOnly | not applicable because not a static field |
| iInstance | false | not applicable because not a static field | |
| iStatic | true | 41 | |
| iStaticReadOnly | true | InitOnly | 51 |
The notion of a const field in C# is translated into a static field with Literal and HasDefault in the metadata. If you change the value of your static or const fields in a static constructor, this new value is kept rather than the value set in the definition itself. It is easy to get this static value using FieldInfo.GetValue() , passing null as parameter. At that point, you should wonder whether the value is computed either at compile time or at runtime when the reflection code is executed. As a test, if you define a static DateTime field and call DateTime.Now() in the static constructor, the value you get from FieldInfo.GetValue(null) is the value of your current time, not the time when the code was compiled. You should note that calling GetValue() could generate an exception.
In the case of a non static field (except for a const member since it is transformed into a static field behind the scenes), a TargetException is thrown with Non-static field requires a target as the exception description. Instead, you have to pass an object of that type to GetValue() , you should note that for both static and non static fields it is possible for the default value to be null.
Once you have the value, you might be interested in formatting it according to its type. For example, if it is a string , display it between double quotes and if it is a Char , between simple quotes. The type is returned by FieldInfo.FieldType , and you simply have to compare it with Type.GetType("System.String") or Type.GetType("System.Char") . The .NET Framework also contains types with static array fields such as the Char[] System.IO.Path.InvalidPathChars field. The following source code builds a textual representation of an array as a list, wrapped by parenthesis and separated by commas.
static public string GetFieldSignature(FieldInfo fi)
{
// build the signature with the following format:
// name : type (= value)
// Note: the value is available only for static fields
StringBuilder szText = new StringBuilder();
...
// Note: handle "xxx[]"
if (fi.FieldType.IsArray)
{
Array values = (Array)value;
sbText.Append(" = {");
for (
int iCurrent = 0;
iCurrent < values.Length;
iCurrent++
)
{
object oValue = values.GetValue(iCurrent);
string stringValue = oValue.ToString();
int iLength = stringValue.Length;
if (iLength == 1)
{
// Note: check not to break StringBuilder.Append
if (
(stringValue.Length != 0) &&
(stringValue[0] != '\0')
)
{
sbText.Append(stringValue);
}
}
else
if (iLength != 0)
{
sbText.Append(stringValue);
}
// add a ',' separator except for the last element
if (iCurrent < values.Length - 1)
sbText.Append(", ");
}
sbText.Append("};");
}
...
}
From the Type object corresponding to the field type, IsArray returns true when you are facing an array. The next step is to cast the object returned by TypeInfo.GetValue() into an Array . Once you grab this array, each element is retrieved using GetValue() , and its textual format is returned by ToString() . That's it!
The result of running the previous code on MyFields.szInfos would be the "{First, Middle, Last}" string.
Dumping Events
The DumpEvents() helper gathers the final kind of members calling Type.GetEvents() that returns an array of EventInfo . Except its Name property, you won't get much detail from an EventInfo . What is meaningful from a developers point of view is to know the signature of the delegate one will be required to implement for an event. The EventInfo.EventHandlerType property returns the Type corresponding to "the underlying event-handler delegate associated with the event" as you can read in the MSDN documentation. Once you have the Type , you need to get the MethodInfo corresponding to its Invoke() method, which has exactly the signature of the delegate method you are looking for. You already know how to get the textual output for parameters of a MethodInfo using my GetMethodSignature() helper method.
Further Work
It is now time to compare the dump of different versions of the same assembly. With the helper methods we have developed in this article series and some creativity you can continue to find the differences between versions of the .NET framework as they are released.
The ReflectHelper.cs file is also full of simple methods that you should be able to easily re-use in your own code. Feel free to dig into the source code provided and you'll find helper methods that I have not detailed here such as the ParseGlobalXXX() methods in AssemblyDescription.cs that show how to list the global members of an assembly such as what you get when you compile Managed C++.
Conclusion
This third and final article of this series was about using reflection to compare different versions of the CLR. I've shown you how to use both managed and unmanaged reflection to dump any kind of type member, even P/Invoke methods. I have also described how to use reflection to handle generic parameters in methods, one of the new features of the new version of the framework. Lastly I have covered a lot of ground over the inner workings of reflection in the various versions of .NET and what to look for the the new framework when it is released.
