Reflection: Browsing Code Information, Part 1
One of the key distinctions between the Microsoft managed environment and previous unmanaged environments is that the managed environment has a rich and extensible set of metadata that is associated with the code that is running. Of course, this metadata would not be much good if you could not easily access it. The classes that give access to the metadata of a running program are in the System.Reflection namespace.
The System.Reflection namespace contains classes that allow the programmer to obtain information about the application that is running and to dynamically add types, values, and objects to a running application. It is also possible to query a given Assembly for a type and obtain information about the metadata associated with that Assembly. Remember: An AppDomain provides an isolated runtime environment. An AppDomain consists of one or more assemblies, that when loaded, form the source for execution instructions. Assemblies contain types and values.
This series of articles and the System.Reflection namespace are about retrieving information about these types and values and dynamically creating assemblies that contain those types and values. You will learn about attributes and how they can be used in a running program to alter the execution and behavior of that program. Microsoft has provided a means of extending attributes so that it is possible to build a custom attribute that your program can use to dynamically change the program's behavior. This series of articles shows you how to build and use those custom attributes, and also looks at dynamically generating code at runtime. The concept of code that generates code has long been exclusively part of languages like Lisp and Scheme. Now, the .NET Framework brings this capability to all languages that participate in it. This series also covers serialization, which is an important consumer of the reflection services provided by the CLR and the .NET Framework.
Using Reflection to Obtain Type Information
Reflection is used in the process of obtaining type information at runtime. Much like an AppDomain provides a runtime environment or home for a Thread, the Assembly provides an overall wrapper around the type information associated with an application.
This section illustrates the information that is available through reflection about an Assembly. Although the type is the root of all reflection operations, the Assembly is the home for types. This article series looks at the assembly and its associated Assembly class first, and then comes back to type and its associated class Type. Next, it specifically looks at that Type class. The Type class allows the programmer to obtain, at runtime, specific metadata about a particular type. Finally, the series goes into some detail about attributes. Attributes allow a programmer to add metadata to a given type. Custom attributes allow the programmer to extend the attribute model to user-defined metadata. The CLR and the .NET Framework use attributes extensively. For attributes to be fully useful, they must be discoverable at runtime. Custom attributes are discovered and read at runtime by using reflection technology.
NOTE
Three categories of value types exist: built-in value types, user-defined value types, and enumerations. Reference types break down into classes, user-defined classes, boxed-value types, delegates, pointers, and interfaces.
Obtaining Type Information from an Assembly at Runtime by Using Reflection
The Assembly class provides the programmer with a good deal of information about the underlying code. Of course, the Assembly class would not provide all of this information without the services provided by the classes in the System.Reflection namespace. To demonstrate some of the functionality offered by the Assembly class, look at the example assembly in Listing 1. This is the code that will be "reflected" upon. As many different types as possible were included in this sample so that you could easily see what information is available at runtime using the reflections services.
Listing 1An Assembly TypeSampler
using System; namespace TypeSampler { // User-defined value type. public struct MyStruct { public int a; public int b; } public enum Sample { Sample1, Sample2, Sample3}; // Interfaces public interface IMyInterface { // Method returns an array string [] MyInterfaceMethod(); } public interface IYourInterface { void YourInterfaceMethod(MyDelegate callback); } // Delegate public delegate void MyDelegate(string msg); // Class public class MyClass: IMyInterface, IYourInterface { // Fields private int myInt; private string myString; private Sample mySample; // Constructor public MyClass() { myInt = 0; myString = "unknown"; } // Method public MyStruct MyMethod(int a, int b) { MyStruct ms = new MyStruct(); ms.a = a; ms.b = b; return ms; } // Interface implementations public string [] MyInterfaceMethod() { string [] returnArray = new string[]{"This","is","returned", "from", "MyMethod",
"in", "IMyInterface"}; return returnArray; } public void YourInterfaceMethod(MyDelegate yourCallback) { yourCallback("Hello, You!"); } // Property public int MyInt { get { return myInt; } set { myInt = value; } } // Another property public string MyString { get { return myString; } } // Yet another property public Sample MyEnum { get { return mySample; } set { mySample = value; } } } }
The main purpose of this assembly is to provide samples of most of the types available. After compiling the assembly library, you run code, part of which is found in Listing 2. This listing shows how all of the type information included in the source is available at runtime.
Listing 2Listing the Types in an Assembly
try { Assembly a = Assembly.Load("SubAssembly"); Console.WriteLine("Listing all of the types for {0}", a.FullName); Type [] types = a.GetTypes(); foreach(Type t in types) { Console.WriteLine("Type: {0}", t); } } catch(Exception e) { Console.WriteLine(e.ToString()); }
To "reflect" on an Assembly, you must first load it. This listing introduces two Assembly methods. The first is the Load method. This method loads an Assembly based on its display name. For those of you with a strong background in Windows programming, this might take some getting used to. This is not the name of the DLL, even if it is a DLL in which the Assembly is contained. It is the name of the assembly. Actually, the preceding code loads an Assembly based on a display name that is only partially specified. The full display name of the Assembly would be in the following form:
Name <,Culture = CultureInfo> <,Version = Major.Minor.Build.Revision><,SN = StrongName> <,PK = PublicKeyToken> '\0'
All of the information in <> is optional. Only the Name portion of the display name is supplied. CultureInfo is an RFC 1766 format-defined culture. If Culture is specified as iv, "", or neutral, it is known as the default or invariant culture. An invariant culture is culture-insensitive. It is associated with the English language but not with a country. This is the default for assemblies that are created with Visual Studio.NET.
The Version string calls out a Major, Minor, Build, and Revision number. If you want to load a specific version of an Assembly, then you would specify the version information along with the assembly name. Wild-card characters are accepted for each of the Version numbers. For example, "Version = 1.0.*", specifies that you want version 1.0 and will take any Build or Revision number.
You can specify a strong name with SN. This is a hexadecimal-encoded value representation of the low-order 64 bits of the hash value of the public key that is assigned to the assembly. The default is to create an assembly without a strong name; the value in that case would be null.
The PK portion of the Assembly name is a hexadecimal representation of the public key of a public/private key pair. If the full public key is known and is specified, then the SN can be ignored because the full public key is a superset for the strong name.
With this new information about the display name, the following supply correct arguments to the static Load method of the Assembly class:
Assembly a = Assembly.Load("SubAssembly"); Assembly a = Assembly.Load("SubAssembly, Version=1.*"); Assembly a = Assembly.Load("SubAssembly, Version=1.0.597.2662"); Assembly a = Assembly.Load("SubAssembly, Culture=iv"); Assembly a = Assembly.Load("SubAssembly, Culture=\"\""); Assembly a = Assembly.Load("SubAssembly, SN=d8d19842315c20be");
Note that besides the Name of the assembly, all of the other portions of the display name are optional and only serve to disambiguate a specific assembly among a group of possible choices. If only one assembly exists, then just specifying the Name is all that is required.
Because all of this is rather complicated, Microsoft has provided an AssemblyName class that encapsulates the Name, Culture, Version, and strong name. It might be easier in your application to build a proper AssemblyName and then call the overloaded static Load method that takes an AssemblyName object as a parameter. You can encode the assembly information in a string or build an AssemblyName objectthey both achieve the same result.
If you can't get used to an assembly name, you can also use the LoadFrom method. This method takes the path or filename to the .DLL or .EXE. As an alternative to Load, you could retrieve an Assembly with the following statement:
Assembly a = Assembly.LoadFrom(@"..\..\SubAssembly\bin\debug\SubAssembly.dll");
What about errors? As with all .NET APIs, errors are handled with exceptions. Load and LoadFrom can throw an ArgumentNullException if the string reference passed is null. These methods also throw ArgumentException if the string length is zero. If the assembly cannot be found (in the case of Load) or if the file specified by the path does not exist (in the case of LoadFrom), these methods throw FileNotFoundException. If the file has been tampered with and this file is signed, then Load and LoadFrom throw FileLoadException. LoadFrom requires appropriate permission to determine the current working directory if executing locally, and permission to load a path preceded by "file:\\". Otherwise, a security exception will be thrown.
After obtaining the Assembly, the short sample obtains an array of Type contained in the Assembly with the GetTypes method. You should only be concerned with the Type names that you can see, so you iterate through the array and print the types that are contained in the Assembly. This is obtaining the type information at runtime, which is what reflection is all about. For this sample, the output looks like this:
Listing all of the types for SubAssembly, Version=1.0.597.7307, Culture=neutral, PublicKeyToken=null Type: TypeSampler.MyStruct Type: TypeSampler.Sample Type: TypeSampler.IMyInterface Type: TypeSampler.IYourInterface Type: TypeSampler.MyDelegate Type: TypeSampler.MyClass
The full display name is printed out followed by the Types defined in the Assembly. You can see the user-defined value type MyStruct; the enumerator type Sample; the two interfaces, IMyIterface, and IYourInterface; the delegate MyDelegate; and the user-defined class MyClass.
After you have an Assembly, you can retrieve most of the information that is encapsulated in the Assembly. Table 1 lists some of the more interesting properties and methods of an Assembly.
Table 1Assembly Methods and Properties
Method or Property |
Description |
CreateInstance |
This method in its simplest form takes a single string argument as the name of the type to create an instance of. Example: TypeSampler.MyClass mc = (TypeSampler.MyClass) a.CreateInstance("TypeSampler.MyClass"); The problem with this example is that the object must be defined to call it. If the assembly can be referenced to define the class, then has it not already been early bound? You could call it this way, but typically this much information is not available for late-binding. The following is a more real-world example: Object mc = a.CreateInstance("TypeSampler.MyClass"); mc.InvokeMember(. . .); Or bypassing this call entirely: Type mct = a.GetType("TypeSampler.MyClass"); object obj = Activator.CreateInstance(mct); MethodInfo mi = mct.GetMethod("MyMethod"); object[] params = new object[2]; Params[0] = 1; Params[1] = 2; mi.Invoke(obj, params); |
GetAssembly |
This static method returns an Assembly given a Type. If, for example, you wanted to find out where the type double is defined, use the following code: a = Assembly.GetAssembly(typeof(double)); Console.WriteLine(a.FullName); An alternative to this would be: a = typeof(double).Assembly; Console.WriteLine(a.FullName); This code will output the full display name for the mscorlib Assembly. Or a = Assembly.GetAssembly(typeof(TypeSampler.MyClass)); Console.WriteLine(a.FullName); The full display name here would point to the SubAssembly Assembly. |
GetCallingAssembly |
This returns the Assembly that called the assembly that is calling this method. You need to think about the definition alone. Look at some sample code: MyClass() { myString = Assembly.GetCallingAssembly().FullName; } . . . public string MyString { get { return myString; } } Look now at the method in MyClass: string MyGetCallingAssemblyMethod() { return Assembly.GetCallingAssembly().FullName; } Finally, look at the following code that exercises this functionality: Assembly a = Assembly.Load("SubAssembly"); TypeSampler.MyClass mc = (TypeSampler.MyClass)a.CreateInstance("TypeSampler.MyClass"); Console.WriteLine(Assembly.GetCallingAssembly().FullName); Console.WriteLine(mc.MyString); Console.WriteLine(mc.MyGetCallingAssemblyMethod); Forget about the convoluted CreateInstance call. Why not just early bind? This sequence of calls will return a reference to the mscorlib assembly (the construction occurs from mscorlib in the "late" bind sample) and two references to the assembly in which this code resides. If you were to replace the CreateInstance call with the following: TypeSampler.MyClass mc = new TypeSampler.MyClass(); then all three Console.WriteLine calls would output references to the assembly that is running this code. |
GetCustomAttributes |
This returns an array of objects that are instances of the custom attribute classes associated with this assembly. Example: Object [] attributes = a.GetCustomAttributes(true); foreach(object o in attributes) { Console.WriteLine(o); } On a default project (a project created by Visual Studio before a user modifies it), this code would print some lines that looked like this: System.Reflection.AssemblyKeyNameAttribute System.Reflection.AssemblyKeyFileAttribute System.Reflection.AssemblyDelaySignAttribute System.Reflection.AssemblyTrademarkAttribute System.Reflection.AssemblyCopyrightAttribute System.Reflection.AssemblyProductAttribute System.Reflection.AssemblyCompanyAttribute System.Reflection.AssemblyConfigurationAttribute System.Reflection.AssemblyDescriptionAttribute System.Reflection.AssemblyTitleAttribute System.Diagnostics.DebuggableAttribute Note that this is not all of the custom attributes in the assembly. These are only the assembly level attributes. If custom attributes are associated with a type in the assembly, then you can obtain those attributes from the Type.GetCustomAttributes method. Don't confuse these attributes with the attributes that are returned from Type.Attributes, which are access-level attributesnot custom attributes. |
GetEntryAssembly |
The static method returns the Assembly that contains the entry point. Typically in C#, this is the Assembly that contains Main(). |
GetExportedTypes |
This function returns the types that are public only, as opposed to GetTypes that return all assembly-level types. |
GetLoadedModules |
This returns an array of Modules that are loaded as part of the assembly. This method, as opposed to GetModules, returns a list of all of the Modules that are currently loaded. It is possible with a delay-loaded Module(s) that not all of the Module(s) associated with an Assembly are currently loaded. |
GetManifestResourceNames |
This returns an array of names of the resources in the assembly. |
GetModules |
This returns an array of Modules that are part of this assembly. This would only be different from GetLoadedModules if a Module is delay-loaded on an as-needed basis. This method returns all of the Module(s) associated with an Assembly. |
GetName |
This method allows the programmer to retrieve an AssemblyName instance associated with this assembly. Using the AssemblyName class allows you to retrieve just the Version or just the Name without having to parse the full display name returned by the FullName property. |
GetReferencedAssemblies |
This returns an array of AssemblyName instances for each referenced assembly. For a simple case, this might just return a reference to the mscorlib. |
GetSatelliteAssembly |
The method returns a satellite Assembly that corresponds to the CultureInfo passed as an argument. For now, consider a satellite assembly to be roughly equivalent to an unmanaged resource assembly. It contains localized resources as opposed to a regular assembly, which uses non-localized resources and a fixed culture. |
GetTypes |
Return an array of the Types at the assembly level. The following code prints the type names that are exposed at the assembly level. Type [] types = a.GetTypes(); foreach(Type t in types) { Console.WriteLine("Type: {0}", t); } A corresponding GetType returns a single Type based on a type name passed as an argument. |
IsDefined |
This returns true or false if the passed Type is defined in the assembly. |
Load |
Load and LoadFrom have been discussed at length previously. They allow a user to load a specific assembly based on the criteria passed as arguments. These functions return an Assembly referring to the loaded assembly or they throw an exception if an error exists. |
CodeBase |
This property returns the full path to where the assembly was specified originally. On a simple assembly, the string returned from this property. For a simple assembly, this path looks like this: file:///F:/as/bin/Debug/SubAssembly.DLL |
EntryPoint |
This property returns a MethodInfo object that describes the entry point of the Assembly. Typically, this is Main()in C#. If this is property is called on an assembly that does not have an entry point, null is returned (nothing in VB). |
Evidence |
This property returns an Evidence object, which is a collection of XML specifications for what security policies affect this assembly. |
FullName |
This property returns a string of the full display name for the assembly. |
GlobalAssemblyCache |
This property returns true if the assembly is loaded in the global assembly cache. It returns false otherwise. |
Location |
This property returns the location of the file containing the assembly. For a simple assembly, it looks like this: f:\as\bin\debug\subassembly.dll Consider this in contrast with the CodeBase format. |
Using the Type Class
After you have a Type, you are set to drill down further into the characteristics of the instance of the Type. Listing 2 shows the Types that were visible at the Assembly level. Rather than build another table of methods and properties, it would be good to look at some code. Listings 36 show source code that is contained in the ListTypes subdirectory. You will be extracting type information from an assembly that contains the same code as in Listing 1, so refer back to that listing to get a feel for the kind of type information that should be expected. Notice that you will be listing information about the program that is running. The first listing (Listing 3) is familiar in that you just obtain all of the Types in the Assembly.
Listing 3Listing the Types in an Assembly
// Load an assembly using its filename. Assembly a = Assembly.GetExecutingAssembly(); // Get the type names from the assembly. Type [] types = a.GetTypes (); foreach (Type t in types)
Listing 3 shows that you get the Assembly from GetExecutingAssembly. This method returns the Assembly that is associated with the currently running code. With that Assembly, you get an array of Types that are visible in the Assembly. With that array, you start up a loop to process each of the Types. In Listing 4, you start processing each type.
Listing 4Getting Member Information for Each Type
MemberInfo[] mmi = t.GetMembers(); // Get and display the DeclaringType method. Console.WriteLine("\nThere are {0} members in {1}.", mmi.Length, t.FullName); Console.WriteLine("{0} is {1}", t.FullName, (t.IsAbstract ? "abstract " : "") + (t.IsArray ? "array " : "") + (t.IsClass ? "class " : "") + (t.IsContextful ? "context " : "") + (t.IsInterface ? "interface " : "") + (t.IsPointer ? "pointer " : "") + (t.IsPrimitive ? "primitive " : "") + (t.IsPublic ? "public " : "") + (t.IsSealed ? "sealed " : "") + (t.IsSerializable ? "serializable " : "") + (t.IsValueType ? "value " : "")); Console.WriteLine ("// Members");
Filtering out all of the output with the exception for that generated that specifically pertains to the assembly types in Listing 2 leaves you with the following:
There are 6 members in ListTypes.MyStruct. ListTypes.MyStruct is public sealed value There are 13 members in ListTypes.Sample. ListTypes.Sample is public sealed serializable value There are 1 members in ListTypes.IMyInterface. ListTypes.IMyInterface is abstract interface public There are 1 members in ListTypes.IYourInterface. ListTypes.IYourInterface is abstract interface public There are 16 members in ListTypes.MyDelegate. ListTypes.MyDelegate is class public sealed serializable There are 16 members in ListTypes.MyClass. ListTypes.MyClass is class public There are 6 members in ListTypes.ListTypesMain. ListTypes.ListTypesMain is class
Notice that the types obtained through reflection are the same Types that were shown in Listing 1. The only addition is that you call GetMembers on each Type. In addition, you can tell the access level of the type along with some other characteristics. It is interesting to note that MyStruct is sealed, which supports the rule that structs cannot be a base class for anything else. Note also that the two interfaces are abstract. (Again, this is consistent with the idea of an interface. The interesting part is that it is not explicitly marked as abstract.) Finally, note that Sample and MyDelegate are serializable. You don't have private types (yet), so all of the Types are public.
This function returns an array of MemberInfo objects that describe each of the members of the type. Both of the interfaces have a member count of one. Each of the interfaces have only one member function, so that makes sense. However, the rest of the Types seem to have too many members. MyStruct has six members, yet there are only two integer fields. Sample has 13 members, but it has only three enumerated values. MyDelegate does not seem to have members, yet 16 members show up. Finally, MyClass has 16 members, but only 10 show up. Listing 5 shows how to list the information in the MemberInfo class.
Listing 5Getting Member Information of Each Type
public static void PrintMembers(MemberInfo [] ms) { foreach (MemberInfo m in ms) { if(m is ConstructorInfo) { Console.WriteLine ("{0}{1}{2}", " ", m,(((MethodBase)m).IsConstructor ? "constructor" : "")); Console.WriteLine ("{0} A member of {1}", " ",
((ConstructorInfo)m).DeclaringType); Console.WriteLine ("{0} Calling Convention {1}", " ",
((ConstructorInfo)m).CallingConvention); ParameterInfo [] pi = ((ConstructorInfo)m).GetParameters(); Console.WriteLine ("{0} There are {1} parameter(s).", " ",
pi.Length); foreach (ParameterInfo p in pi) { Console.WriteLine ("{0} {1} {2}", " ",
p.ParameterType,p.Name); } } else if(m is MethodInfo) { Console.WriteLine ("{0}{1}", " ", m); Console.WriteLine ("{0} A member of {1}", " ",
((MethodInfo)m).DeclaringType); Console.WriteLine ("{0} Calling Convention {1}", " ",
((MethodInfo)m).CallingConvention); ParameterInfo [] pi = ((MethodInfo)m).GetParameters(); Console.WriteLine ("{0} There are {1} parameter(s).", " ",
pi.Length); foreach (ParameterInfo p in pi) { Console.WriteLine ("{0} {1} {2}", " ",
p.ParameterType, p.Name); } Console.WriteLine ("{0} Returns {1}", " ",
((MethodInfo)m).ReturnType); } else if(m is FieldInfo) { Console.WriteLine ("{0}{1}", " ", m); // Get the MethodAttribute enumerated value FieldAttributes fieldAttributes = ((FieldInfo)m).Attributes; Console.Write ("{0} ", " "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.Private) Console.Write("Private "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.Public) Console.Write("Public "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.Assembly) Console.Write("Assembly "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.FamANDAssem) Console.Write("FamANDAssem "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.Family) Console.Write("Family "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.FamORAssem) Console.Write("FamORAssem "); if((fieldAttributes & FieldAttributes.FieldAccessMask) ==
FieldAttributes.FamORAssem) Console.Write("FamORAssem "); if((fieldAttributes & FieldAttributes.HasDefault) != 0) Console.Write("HasDefault "); if((fieldAttributes & FieldAttributes.HasFieldMarshal) != 0) Console.Write("HasFieldMarshal "); if((fieldAttributes & FieldAttributes.HasFieldRVA) != 0) Console.Write("HasFieldRVA "); if((fieldAttributes & FieldAttributes.InitOnly) != 0) Console.Write("InitOnly "); if((fieldAttributes & FieldAttributes.Literal) != 0) Console.Write("Literal "); if((fieldAttributes & FieldAttributes.NotSerialized) != 0) Console.Write("NotSerialized "); if((fieldAttributes & FieldAttributes.PinvokeImpl) != 0) Console.Write("PinvokeImpl "); if((fieldAttributes & FieldAttributes.PrivateScope) != 0) Console.Write("PrivateScope "); if((fieldAttributes & FieldAttributes.RTSpecialName) != 0) Console.Write("RTSpecialName "); if((fieldAttributes & FieldAttributes.SpecialName) != 0) Console.Write("SpecialName "); if((fieldAttributes & FieldAttributes.Static) != 0) Console.Write("Static "); Console.WriteLine (((FieldInfo)m).FieldType); Console.WriteLine ("A member of {0}", ((FieldInfo)m).DeclaringType); Console.WriteLine ("{0} {1}", " ",
Attribute.GetCustomAttribute(m, typeof(Attribute))); } else if(m is PropertyInfo) { Console.WriteLine ("{0}{1}", " ", m); Console.WriteLine ("{0} CanRead: {1}", " ",
((PropertyInfo)m).CanRead); Console.WriteLine ("{0} CanWrite: {1}", " ",
((PropertyInfo)m).CanWrite); Console.WriteLine ("{0} IsSpecialName: {1}", " ",
((PropertyInfo)m).IsSpecialName); Console.WriteLine ("{0} Member of: {1}", " ",
((PropertyInfo)m).DeclaringType); Console.WriteLine ("{0} Member type: {1}", " ",
((PropertyInfo)m).MemberType); Console.WriteLine ("{0} Name: {1}", " ",
((PropertyInfo)m).Name); Console.WriteLine ("{0} Property type: {1}", " ",
((PropertyInfo)m).PropertyType); Console.WriteLine ("{0} Reflected type: {1}", " ",
((PropertyInfo)m).ReflectedType); if(((PropertyInfo)m).GetGetMethod() != null) { Console.WriteLine ("{0} Get: {1}", " ",
((PropertyInfo)m).GetGetMethod()); Console.WriteLine ("{0} Return type {1}",
" ", ((PropertyInfo)m).GetGetMethod().ReturnType); } if(((PropertyInfo)m).GetSetMethod() != null) { Console.WriteLine ("{0} Set: {1}", " ",
((PropertyInfo)m).GetSetMethod()); Console.WriteLine ("{0} Return type {1}", " ",
((PropertyInfo)m).GetSetMethod().ReturnType); } } else { Console.WriteLine ("{0}{1}", " ", m); } } Console.WriteLine(); }
One of the questions was that the structure MyStruct was listed as having six members when clearly only two members existed. Looking at the output for this Type shows where the extra members show up.
There are 6 members in ListTypes.MyStruct. ListTypes.MyStruct is public sealed value. Int32 a Public System.Int32 A member of ListTypes.MyStruct Int32 b Public System.Int32 A member of ListTypes.MyStruct Int32 GetHashCode() A member of System.ValueType Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.Int32 Boolean Equals(System.Object) A member of System.ValueType Calling Convention Standard, HasThis There are 1 parameter(s). System.Object obj Returns System.Boolean System.String ToString() A member of System.ValueType Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.String System.Type GetType() A member of System.Object Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.Type
Here you see that the first two members of MyStruct are the integer fields that you know about: fields a and b. The rest of the fields are member methods that a struct inherits when it is declared. The members GetHashCode, Equals, and ToString are all inherited from System.ValueType. The final method, GetType, comes from System.Object. The extra four members are there because of inheritance.
The structure has been completely dissected, and any information about the structure (metadata) can be obtained through reflection. This was done by obtaining an array of MemberInfo objects describing each member. You obtain specific information about a particular member by first obtaining the proper derived type. You then cast the MemberInfo object to the proper derived type and call the specific methods of the derived type.
The MemberInfo object is the base class for a few classes, as is illustrated in the following simple hierarchy:
Object MemberInfo EventInfo FieldInfo MethodBase ConstructorInfo MethodInfo PropertyInfo Type
Therefore, GetMembers returns an array of MemberInfo objects, but depending on the member, it might actually be an EventInfo, FieldInfo, MethodBase, PropertyInfo, or Type object. The code in PrintMembers uses the following construct to determine if the member of the array is one of the specific objects:
if(m is MethodInfo)
After the specific object has been determined, you have to call the methods on that object to obtain the information desired. Following is the output for the Sample enumeration:
There are 13 members in ListTypes.Sample. ListTypes.Sample is public sealed serializable value // Members Int32 value__ Public RTSpecialName SpecialName System.Int32 A member of ListTypes.Sample ListTypes.Sample Sample1 Public HasDefault Literal Static ListTypes.Sample A member of ListTypes.Sample ListTypes.Sample Sample2 Public HasDefault Literal Static ListTypes.Sample A member of ListTypes.Sample ListTypes.Sample Sample3 Public HasDefault Literal Static ListTypes.Sample A member of ListTypes.Sample System.String ToString(System.IFormatProvider) A member of System.Enum Calling Convention Standard, HasThis There are 1 parameter(s). System.IFormatProvider provider Returns System.String System.TypeCode GetTypeCode() A member of System.Enum Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.TypeCode System.String ToString(System.String, System.IFormatProvider) A member of System.Enum Calling Convention Standard, HasThis There are 2 parameter(s). System.String format System.IFormatProvider provider Returns System.String Int32 CompareTo(System.Object) A member of System.Enum Calling Convention Standard, HasThis There are 1 parameter(s). System.Object target Returns System.Int32 Int32 GetHashCode() A member of System.Enum Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.Int32 Boolean Equals(System.Object) A member of System.Enum Calling Convention Standard, HasThis There are 1 parameter(s). System.Object obj Returns System.Boolean System.String ToString() A member of System.Enum Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.String System.String ToString(System.String) A member of System.Enum Calling Convention Standard, HasThis There are 1 parameter(s). System.String format Returns System.String System.Type GetType() A member of System.Object Calling Convention Standard, HasThis There are 0 parameter(s). Returns System.Type
Again, you see the first four members that you expect. The first member is the integer that holds the value, and the next three are the different values of the enumerator. The next eight members are inherited from the Enum class. The last member, GetType, is inherited from System.Object.
The properties of the remaining types in the ListTypes namespace will not be shown here because the output is not what is important. What is important is the process of using reflection to query an Assembly for a Type and listing the properties of that Type.
A Type can be queried one other way. Rather than using the general GetMembers method and relying on runtime type information to get the category of member that is to be retrieved, you can use many methods (such as GetProperties) to query directly for a specific category of member. Look at the code in Listing 6.
Listing 6Getting Property Information
PropertyInfo [] pi; pi = t.GetProperties (BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); Console.WriteLine ("// Instance Properties"); PrintMembers (pi);
Here you can specify that you are just interested in properties (GetProperties) of a Type. You can also specify what kind of property that you are interested in with the BindingFlags. The preceding code specifies flags that will match instance, non-public, and public properties. That is basically saying that you want all properties (instance, public, protected, or private). As you can see, the BindingFlags has a FlagsAttribute so that the enumerated values can be combined (ORd together). The BindingFlags that specifically apply to retrieving information are as follows:
- DeclaredOnly
- FlattenHierarchy
- IgnoreCase
- IgnoreReturn
- Instance
- NonPublic
- Public
- Static
If the code associated with Listing 6 is compiled and run, then you get output that looks like this:
There are 16 members in ListTypes.MyDelegate. ListTypes.MyDelegate is class public sealed serializable // Instance Properties System.Reflection.MethodInfo Method CanRead: True CanWrite: False IsSpecialName: False Member of: System.Delegate Member type: Property Name: Method Property type: System.Reflection.MethodInfo Reflected type: ListTypes.MyDelegate Get: System.Reflection.MethodInfo get_Method() Return type System.Reflection.MethodInfo System.Object Target CanRead: True CanWrite: False IsSpecialName: False Member of: System.Delegate Member type: Property Name: Target Property type: System.Object Reflected type: ListTypes.MyDelegate Get: System.Object get_Target() Return type System.Object There are 16 members in ListTypes.MyClass. ListTypes.MyClass is class public // Instance Properties Int32 MyInt CanRead: True CanWrite: True IsSpecialName: False Member of: ListTypes.MyClass Member type: Property Name: MyInt Property type: System.Int32 Reflected type: ListTypes.MyClass Get: Int32 get_MyInt() Return type System.Int32 Set: Void set_MyInt(Int32) Return type System.Void System.String MyString CanRead: True CanWrite: False IsSpecialName: False Member of: ListTypes.MyClass Member type: Property Name: MyString Property type: System.String Reflected type: ListTypes.MyClass Get: System.String get_MyString() Return type System.String ListTypes.Sample MyEnum CanRead: True CanWrite: True IsSpecialName: False Member of: ListTypes.MyClass Member type: Property Name: MyEnum Property type: ListTypes.Sample Reflected type: ListTypes.MyClass Get: ListTypes.Sample get_MyEnum() Return type ListTypes.Sample Set: Void set_MyEnum(ListTypes.Sample) Return type System.Void
In the preceding output listing, only certain portions of the total output listing have been selected for illustration purposes. The first portion of the output shows the instance properties for MyDelegate. The second portion of the output shows the instance properties for MyClass. You might want to comment or uncomment various portions of the source code to retrieve specific information; listing all of the information that can be reflected on can be rather large. This is identical to the output that you received from GetMembers. Now you are able to retrieve the information that you want.
The following Type methods accept BindingFlags and allow a finer grain retrieval of Type information:
- GetMembers
- GetEvents
- GetConstructor
- GetConstructors
- GetMethod
- GetMethods
- GetField
- GetFields
- GetEvent
- GetProperty
- GetProperties
- GetMember
- FindMembers
In addition to the small code snippet in Listing 6, source code is also available for many of the preceding methods in the ListTypes directory.
Obtaining and Using Attributes at Runtime Using Reflection
One particular property of Type that might have been glossed over was that returned by Attribute.GetCustomAttribute. This is how you can programmatically (that is, at runtime) find attributes associated with a type. For example, you can use the XmlSerializer class to serialize MyClass. The default behavior is to serialize each member as a separate element. However, you can use attributes to modify this default behavior. An example of using attributes to modify the default serialization behavior is illustrated by the code in Listing 7.
Listing 7Adding Attributes to a Field
// Class public class MyClass: IMyInterface, IYourInterface { // Fields [XmlAttribute] private int myInt; [XmlAttribute] private string myString; [XmlAttribute] private Sample mySample;
The process of serialization is discussed in depth a little later. The following illustrates how the serialization code could query each field of a structure at runtime to see how it should serialize the object. Listing 8 shows how to query a particular field for an attribute.
Listing 8Querying Fields of a Type
FieldInfo [] fi; // Instance Fields fi = t.GetFields (BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); Console.WriteLine ("// Instance Fields"); PrintMembers (fi);
The portion of the PrintMembers method that keys on custom attributes looks like Listing 9.
Listing 9Retrieving Custom Attributes
if(Attribute.GetCustomAttribute(m, typeof(Attribute)) != null) Console.WriteLine ("{0} {1}", " ",Attribute.GetCustomAttribute(m, typeof(Attribute)));
Running the code in Listings 79 produces the following output:
ListTypes.MyClass is class public // Instance Fields Int32 myInt Private System.Int32 System.Xml.Serialization.XmlAttributeAttribute A member of ListTypes.MyClass System.String myString Private System.String System.Xml.Serialization.XmlAttributeAttribute A member of ListTypes.MyClass ListTypes.Sample mySample Private ListTypes.Sample System.Xml.Serialization.XmlAttributeAttribute A member of ListTypes.MyClass
Notice that the [XmlAttribute] associated with each of the fields is picked up. The Attribute.GetCustomAttibute or Attribute.GetCustomAttributes static methods allow a programmer access to the custom attributes associated with a member or assembly. A serialization engine obviously uses these attributes at runtime to change its behavior. Using reflection, the programmer has the same facilities available that a serialization engine has to modify a program's behavior using attributes. Although printing the attributes is informative, it is not probably what you would do in a real application. These methods return an instance of the custom attribute. Instead of printing the attribute, you could do the following:
Attribute attrib = Attribute.GetCustomAttribute(m, typeof(Attribute)); XmlAttributeAttribute xmlAttrib = attrib as XmlAttributeAttribute;
Alternatively, a GetCustomAttributes method exists in the MemberInfo class, so it is available to all of the classes that are derived from MemberInfo. If you prefer these methods, you would change the preceding code to the following:
Attribute attrib = m.GetCustomAttribute(typeof(Attribute), true); XmlAttributeAttribute xmlAttrib = attrib as XmlAttributeAttribute;
Now you can treat [XmlAttribute] as a class and call its methods, get/set properties, and so on. Note the convention that is recommended for all custom attributes is as follows: [Name] translates to a class NameAttribute.
Custom attributes are used throughout the .NET Framework. They allow the functionality of the Type to be significantly altered. Using the available attributes expands the usefulness of a Type and causes it to be more fully integrated with the rest of the framework. Attributes can be used to provide additional documentation information, define which fields participate in serialization, indicate that a Type will participate in a transaction, and so on.
Listing the functionality of each attribute would not be very productive. To get an idea of the number of attributes, pull up the online documentation for the Attribute class hierarchy and look at the many derived classes. Just remember two rules:
All custom attributes are enclosed in [].
The custom attribute applies to the type immediately following the custom attribute. Only comments and white space can fall between a custom attribute and the type to which it is to apply.
Customizing Metadata with User-Defined Attributes
In addition to using attributes that are part of the .NET Framework, it is also possible to define your own attributes. Defining your own attributes is easy. The code for Listings 1012 is in the CustomAttribute directory. Listing 10 demonstrates some of the characteristics of a simple user-defined attribute.
Listing 10User-Defined Custom Attributes
class HelloAttribute : Attribute { public HelloAttribute() { Console.WriteLine("Constructing Hello"); } } [Hello] class MyClass { public MyClass() { Console.WriteLine("Constructing MyClass"); Object [] attributes = typeof(MyClass).GetCustomAttributes(true); foreach(Attribute attrib in attributes) { Console.WriteLine(attrib); } } } class CustomAttributesMain { static void Main(string[] args) { MyClass m = new MyClass(); } }
This sample demonstrates building a simple user-defined attribute. It consists of the definition of the attribute (HelloAttribute), a class to which the attribute is applied, and an entry point for the sample (Main). In a real application, all three of these parts would most likely be in separate assemblies. Following are some key observations about this simple sample:
The user-defined attribute is actually a class. This is a concept that is key in understanding how to use and define attributes. Anything that can be done in a class can be done in a user-defined attribute.
All custom attributes must be derived from Attribute or they will not be recognized as an Attribute.
The name of the attribute is [Hello], yet the implementation of the attribute is in the class HelloAttribute. This is a convention for all attributes. When the compiler sees Hello as an attribute, it will append the word Attribute to the name and search for a class that implements the attribute. Following this convention will make it easy for the compiler to resolve the attribute to an implementation.
Notice the output from this sample:
Constructing MyClass Constructing Hello CustomAttributes.HelloAttribute
HelloAttribute is not instantiated until it is referred to or used. Here, MyClass wants to see if it has an attribute associated with it. It is only then that the constructor for HelloAttribute is called.
Now you will learn how to pass information from the attribute to the implementation of the attribute and eventually to the class that has been "attributed". Listing 10 will be modified slightly so that you can pass a string to the attribute. The result is Listing 11.
Listing 11User-Defined Custom Attributes with an Argument
class HelloAttribute : Attribute { private string message; public HelloAttribute(string message) { this.message = message; Console.WriteLine("Constructing Hello"); } public string Message { get { return message; } } } [Hello("Hello World!")] class MyClass { public MyClass() { Console.WriteLine("Constructing MyClass"); Object [] attributes = typeof(MyClass).GetCustomAttributes(true); foreach(Attribute attrib in attributes) { HelloAttribute ha = attrib as HelloAttribute; if(ha != null) { Console.WriteLine(ha.Message); } } } } class CustomAttributesMain { static void Main(string[] args) { MyClass m = new MyClass(); } }
Now the output looks like this:
Constructing MyClass Constructing Hello Hello World!
Using reflection, the message reaches the class to which the attribute was applied. Multiple arguments could be passed to the attribute, such as [Hello(A, B, C)]. The implementation of the attribute class would need to add a constructor: HelloAttribute(typea A, typeb B, typec C). Typea, typeb, and typec would specify the types required for each of the arguments. The compiler would do all of the same compile-time checking of the arguments specified in the attribute and those declared for the implementation class constructor.
All of the parameters that have been discussed so far with respect to attributes are "positional" arguments. They must occur in the order that the constructor specifies and have compatible types with the constructor arguments. It is possible to specify "optional" or "named" arguments, which would need to follow any of the positional arguments. An example of an optional or named argument to an attribute is illustrated in Listing 12.
Listing 12User-Defined Custom Attributes with a Named Argument
class HelloAttribute : Attribute { private int id; private string message; public HelloAttribute(string message) { id = 0; this.message = message; Console.WriteLine("Constructing Hello"); } public int Id { get { return id; } set { Console.WriteLine("Setting ID"); id = value; } } public string Message { get { return message; } } } [Hello("Hello World!", Id=123)] class MyClass { public MyClass() { Console.WriteLine("Constructing MyClass"); Object [] attributes = typeof(MyClass).GetCustomAttributes(true); foreach(Attribute attrib in attributes) { HelloAttribute ha = attrib as HelloAttribute; if(ha != null) { Console.WriteLine("Message: {0}", ha.Message); Console.WriteLine("Id: {0}", ha.Id); } } } } class CustomAttributesMain { static void Main(string[] args) { MyClass m = new MyClass(); } }
Now the output looks like this:
Constructing MyClass Constructing Hello Setting ID Message: Hello World! Id: 123
The HelloAttribute object is constructed using the fixed arguments that would be supplied to a constructor. Next, the positional or named arguments are matched with a property of the implementing class, and that property is set using the data in the attribute. This is why positional arguments must come first; otherwise, the object could not be properly constructed.
If you wanted to have more than one attribute, , you might be tempted to just do the following:
[Hello("Hello World!")] [Hello("Hello World, again!")] class MyClass
If you do this, however, you get a compiler error:
CustomAttributes.cs(37): Duplicate 'Hello' attribute
To change this behavior, you need AttributeUsageAttribute. This attribute has the following convention:
[AttributeUsage( validon (AttributeTargets.XXX), AllowMultiple=bool, InHerited=bool )]
The validon argument is an AttributeUsage enumerator called AttributeTargets. It has the following enumerator values:
- All
- Assembly
- Class
- Constructor
- Delegate
- Enum
- Event
- Field
- Interface
- Method
- Module
- Parameter
- Property
- ReturnValue
- Struct
To achieve the desired functionality (have multiple attributes), you would change the following lines on the class that implements that attribute:
[AttributeUsage(AttributeTargets.All, AllowMultiple=true)] class HelloAttribute : Attribute
Now instead of a compiler error, you get the following output:
Constructing MyClass Constructing Hello Constructing Hello Message: Hello World, again! Message: Hello World!
Notice that the attributes are retrieved in a LIFO (last-in-first-out) manner. This is the way it is, but you should not depend on ordering for multiple attributes.
As part of the AttributeUsage attribute, you can specify where this attribute is valid. So far, all of the attributes have been placed on a class. If that is not the way you want your attribute to be used, then change AttributeUsage as follows:
[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
Now if the user tries to apply the attribute to a field, the compiler generates an error message:
CustomAttributes.cs(41): Attribute 'Hello' is not valid on this declarationtype. It is valid on 'class' declarations only.
With attributes and reflection, the possibilities are limitless. Attributes are a truly groundbreaking feature.