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 chapter 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 chapter 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

Value types and reference types were first introduced in Chapter 2 (particularly note Figure 2.1). Three categories of value types, built-in value types, user-defined value types, and enumerations exist. 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 17.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. If you want to modify this or the associated test source, the complete source is included in AssemblySample and AssemblySampleSubAssembly.

Listing 17.1. An 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. Except for boxed value types and pointer types, this chapter does cover all of the types in this assembly. After compiling the assembly library, you run code, part of which is found in Listing 17.2. This listing shows how all of the type information included in the source is available at runtime.

Listing 17.2. Listing 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 of the following form:

Name <,Culture = CultureInfo> <,Version = Major.Minor.Build.Revision> <,SN = StrongName> <
,PK = PublicKeyToken> ''

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.

Note

The Culture associated with an assembly is an important part of globalization and internationalization. These topics are covered in more detail in Chapter 18, “Globalization/Localization.”


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.

Note

Details of strong names and public/private key pairs have already been provided in Chapters 6, “Publishing Applications,” and 16, “.NET Security.”


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 object—they 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(@"....SubAssemblyindebugSubAssembly.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 17.1 lists some of the more interesting properties and methods of an Assembly.

Table 17.1. Assembly 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. This method is important later in the chapter when late binding is discussed.

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);

Again, refer to the late-binding section later in this chapter.
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 attributes— not 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. Satellite assemblies will be covered in more detail in Chapter 18. 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 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:asindebugsubassembly.dll

Consider this in contrast to 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 17.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 17.317.6 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 17.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 17.3) is familiar in that you just obtain all of the Types in the Assembly.

Listing 17.3. Listing 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 17.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 17.4, you start processing each type.

Listing 17.4. Getting Member Information for Each Type
MemberInfo[] mmi = t.GetMembers();
// Get and display the DeclaringType method.
Console.WriteLine("
There 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 17.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 17.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. Also note 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 1. 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 6 members, yet there are only 2 integer fields. Sample has 13 members, but it has only 3 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 17.5 shows how to list the information in the MemberInfo class.

Listing 17.5. Getting 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:

if(m is MethodInfo)

to determine if the member of the array is one of the specific objects. 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 in this chapter 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 17.6.

Listing 17.6. Getting 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 17.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 17.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 17.7.

Listing 17.7. Adding 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 17.8 shows how to query a particular field for an attribute.

Listing 17.8. Querying 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 17.9.

Listing 17.9. Retrieving Custom Attributes
if(Attribute.GetCustomAttribute(m, typeof(Attribute)) != null)
        Console.WriteLine ("{0}  {1} ", "     ", Attribute.GetCustomAttribute(m, typeof
(Attribute)));

Running the code in Listings 17.717.9 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 17.1017.12 is in the CustomAttribute directory. Listing 17.10 demonstrates some of the characteristics of a simple user-defined attribute.

Listing 17.10. User-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 17.10 will be modified slightly so that you can pass a string to the attribute. The result is Listing 17.11.

Listing 17.11. User-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 17.12.

Listing 17.12. User-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 declaration type. It is
 valid on 'class' declarations only.

With attributes and reflection, the possibilities are limitless. Attributes are a truly groundbreaking feature.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.23.101.60