Chapter 20. Attributes and Reflection

Throughout this book, I have emphasized that a .NET application contains code, data, and metadata. Metadata is information about the data—that is, information about the types, code, assembly, and so forth—stored along with your program. This chapter explores how some of that metadata is created and used.

Attributes are a mechanism for adding metadata, such as compiler instructions and other data about your data, methods, and classes to the program itself. Attributes are inserted into the metadata and are visible through ILDASM and other metadata-reading tools.

Reflection is the process by which a program can read its own metadata, or metadata from another program. A program is said to reflect on itself or on another program, extracting metadata from the reflected assembly and using that metadata either to inform the user or to modify the program’s behavior.

Attributes

An attribute is an object that represents data you want to associate with an element in your program. The element to which you attach an attribute is referred to as the target of that attribute. For example, the attribute:

[NoIDispatch]

is associated with a class or an interface to indicate that the target class should derive from IUnknown rather than IDispatch when exporting to COM. I discuss COM interface programming in detail in Chapter 23.

Types of Attributes

Some attributes are supplied as part of the CLR, or by the framework. In addition, you are free to create your own custom attributes for your own purposes.

Most programmers will use only the attributes provided by the framework, though creating your own custom attributes can be a powerful tool when combined with reflection, as described later in this chapter.

Attribute targets

If you search through the CLR, you’ll find a great many attributes. Some attributes are applied to an assembly, others to a class or interface, and some, such as [WebMethod], are applied to class members. These are called the attribute targets. The possible attributes are declared in the AttributeTargets enumeration, and are detailed in Table 20-1.

Table 20-1. Possible attribute targets

Member name

Usage

All

Applied to any of the following elements: assembly, class, constructor, delegate, enum, event, field, interface, method, module, parameter, property, return value, or struct

Assembly

Applied to the assembly itself

Class

Applied to a class

Constructor

Applied to a given constructor

Delegate

Applied to a delegate

Enum

Applied to an enumeration

Event

Applied to an event

Field

Applied to a field

Interface

Applied to an interface

Method

Applied to a method

Module

Applied to a single module

Parameter

Applied to a parameter of a method

Property

Applied to a property (both get and set, if implemented)

ReturnValue

Applied to a return value

Struct

Applied to a struct

Applying attributes

You apply attributes to their targets by placing them in square brackets immediately before the target item (except in the case of assemblies, in which case you place them at the top of the file).

You can combine attributes by stacking one on top of another:

[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(".\keyFile.snk")]

You can also do this by separating the attributes with commas:

[assembly: AssemblyDelaySign(false),
 assembly: AssemblyKeyFile(".\keyFile.snk")]

Tip

You must place assembly attributes after all using statements and before any code.

Many attributes are used for interoperating with COM, as discussed in detail in Chapter 23. You’ve already seen use of one attribute ([WebMethod]) in Chapter 16. You’ll see other attributes, such as the [Serializable] attribute, used in the discussion of serialization in Chapter 22.

The System.Reflection namespace offers a number of attributes, including attributes for assemblies (such as the AssemblyKeyFileAttribute), for configuration, and for version attributes.

One of the attributes you are most likely to use in your everyday C# programming (if you aren’t interacting with COM) is [Serializable]. As you’ll see in Chapter 22, all you need to do to ensure that your class can be serialized to disk or to the Internet is add the [Serializable] attribute to the class:

[Serializable]
class MySerializableClass

The attribute tag is put in square brackets immediately before its target—in this case, the class declaration.

The key fact about attributes is that you know when you need them; the task will dictate their use.

Custom Attributes

You are free to create your own custom attributes and use them at runtime as you see fit. Suppose, for example, that your development organization wants to keep track of bug fixes. You already keep a database of all your bugs, but you’d like to tie your bug reports to specific fixes in the code.

You might add comments to your code along the lines of:

// Bug 323 fixed by Jesse Liberty 1/1/2008.

This would make it easy to see in your source code, but there is no enforced connection to Bug 323 in the database. A custom attribute might be just what you need. You would replace your comment with something like this:

[BugFixAttribute(323,"Jesse Liberty","1/1/2008",
Comment="Off by one error")]

You could then write a program to read through the metadata to find these bug-fix notations and update the database. The attribute would serve the purposes of a comment, but would also allow you to retrieve the information programmatically through tools you’d create.

Tip

This may be a somewhat artificial example, however, because these attributes would be compiled into the shipping code.

Declaring an attribute

Attributes, like most things in C#, are embodied in classes. To create a custom attribute, derive your new custom attribute class from System.Attribute:

public class BugFixAttribute : System.Attribute

You need to tell the compiler which kinds of elements this attribute can be used with (the attribute target). Specify this with (what else?) an attribute:

[AttributeUsage(AttributeTargets.Class |
    AttributeTargets.Constructor |
    AttributeTargets.Field |
    AttributeTargets.Method |
    AttributeTargets.Property,
    AllowMultiple = true)]

AttributeUsage is an attribute applied to attributes: a meta-attribute. It provides, if you will, meta-metadata—that is, data about the metadata. For the AttributeUsage attribute constructor, you pass two arguments.

The first argument is a set of flags that indicate the target—in this case, the class and its constructor, fields, methods, and properties. The second argument is a flag that indicates whether a given element might receive more than one such attribute. In this example, AllowMultiple is set to true, indicating that class members can have more than one BugFixAttribute assigned.

Naming an attribute

The new custom attribute in this example is named BugFixAttribute. The convention is to append the word Attribute to your attribute name. The compiler supports this by allowing you to call the attribute with the shorter version of the name. Thus, you can write:

[BugFix(123, "Jesse Liberty", "01/01/08", Comment="Off by one")]

The compiler will first look for an attribute named BugFix and, if it doesn’t find that, will then look for BugFixAttribute.

Constructing an attribute

Attributes take two types of parameters: positional and named. In the BugFix example, the programmer’s name, the bug ID, and the date are positional parameters, and comment is a named parameter. Positional parameters are passed in through the constructor, and must be passed in the order declared in the constructor:

public BugFixAttribute(int bugID, string programmer,
string date)
{
    this.BugID = bugID;
    this.Programmer = programmer;
    this.Date = date;
}

Named parameters are implemented as fields or as properties:

public string Comment    { get; set; }

It is common to create read-only properties for the positional parameters:

public int BugID    { get; private set; }

Using an attribute

Once you have defined an attribute, you can put it to work by placing it immediately before its target. To test the BugFixAttribute of the preceding example, the following program creates a simple class named MyMath and gives it two functions. Assign BugFixAttributes to the class to record its code-maintenance history:

[BugFixAttribute(121,"Jesse Liberty","01/03/08")]
[BugFixAttribute(107,"Jesse Liberty","01/04/08",
                 Comment="Fixed off by one errors")]
public class MyMath

These attributes are stored with the metadata. Example 20-1 shows the complete program.

Example 20-1. Working with custom attributes
using System;

namespace CustomAttributes
{
    // create custom attribute to be assigned to class members
    [AttributeUsage(AttributeTargets.Class |
                    AttributeTargets.Constructor |
                    AttributeTargets.Field |
                    AttributeTargets.Method |
                    AttributeTargets.Property,
                    AllowMultiple = true)]
    public class BugFixAttribute : System.Attribute
    {
        // attribute constructor for positional parameters
        public BugFixAttribute
        (
            int bugID,
            string programmer,
            string date
        )
        {
            this.BugID = bugID;
            this.Programmer = programmer;
            this.Date = date;
        }

        // accessors
        public int BugID { get; private set; }
        public string Date { get; private set; }
        public string Programmer { get; private set; }

        // property for named parameter
        public string Comment { get; set; }
    }

    // ********* assign the attributes to the class ********

    [BugFixAttribute(121, "Jesse Liberty", "01/03/08")]
    [BugFixAttribute(107, "Jesse Liberty", "01/04/08",
                        Comment = "Fixed off by one errors")]
    public class MyMath
    {
        public double DoFunc1(double param1)
        {
            return param1 + DoFunc2(param1);
        }

        public double DoFunc2(double param1)
        {
            return param1 / 3;
        }
    }

    public class Tester
    {
        static void Main(string[] args)
        {
            MyMath mm = new MyMath( );
            Console.WriteLine("Calling DoFunc(7). Result: {0}",
            mm.DoFunc1(7));
        }
    }
}

Output:
Calling DoFunc(7). Result: 9.3333333333333333

As you can see, the attributes had absolutely no impact on the output. In fact, for the moment, you have only my word that the attributes exist at all. A quick look at the metadata using ILDASM does reveal that the attributes are in place, however, as shown in Figure 20-1. You’ll see how to get at this metadata and use it in your program in the next section.

The metadata in the assembly
Figure 20-1. The metadata in the assembly

Reflection

For the attributes in the metadata to be useful, you need a way to access them, ideally during runtime. The classes in the Reflection namespace, along with the System.Type class, provide support for examining and interacting with the metadata.

Reflection is generally used for any of four tasks:

Viewing metadata

This might be used by tools and utilities that wish to display metadata.

Performing type discovery

This allows you to examine the types in an assembly and interact with or instantiate those types. This can be useful in creating custom scripts. For example, you might want to allow your users to interact with your program using a script language, such as JavaScript, or a scripting language you create yourself.

Late binding to methods and properties

This allows the programmer to invoke properties and methods on objects dynamically instantiated, based on type discovery. This is also known as dynamic invocation.

Creating types at runtime (reflection emit)

The ultimate use of reflection is to create new types at runtime and then to use those types to perform tasks. You might do this when a custom class, created at runtime, will run significantly faster than more generic code created at compile time.

Viewing Metadata

In this section, you will use the C# reflection support to read the metadata in the MyMath class.

Start by obtaining an object of the type MemberInfo. This object, in the System.Reflection namespace, is provided to discover the attributes of a member and to provide access to the metadata:

System.Reflection.MemberInfo inf = typeof(MyMath);

Call the typeof operator on the MyMath type, which returns an object of type Type, which derives from MemberInfo.

Tip

The Type class is the heart of the reflection classes. Type encapsulates a representation of the type of an object. The Type class is the primary way to access metadata. Type derives from MemberInfo and encapsulates information about the members of a class (e.g., methods, properties, fields, events, etc.).

The next step is to call GetCustomAttributes on this MemberInfo object, passing in the type of the attribute you want to find. You get back an array of objects, each of type BugFixAttribute:

object[] attributes;
attributes =
    inf.GetCustomAttributes(typeof(BugFixAttribute),false);

You can now iterate through this array, printing out the properties of the BugFixAttribute object. Example 20-2 replaces the Tester class from Example 20-1.

Example 20-2. Using reflection
public static void Main(string[] args)
{
    MyMath mm = new MyMath( );Console.WriteLine("Calling DoFunc(7). Result: {0}",
                      mm.DoFunc1(7));

    // get the member information and use it to
    // retrieve the custom attributes
    System.Reflection.MemberInfo inf = typeof(MyMath);
    object[] attributes;
    attributes = inf.GetCustomAttributes(
                     typeof(BugFixAttribute), false);

    // iterate through the attributes, retrieving the
    // properties
    foreach (Object attribute in attributes)
    {
        BugFixAttribute bfa = (BugFixAttribute)attribute;
        Console.WriteLine("
BugID: {0}", bfa.BugID);
        Console.WriteLine("Programmer: {0}", bfa.Programmer);
        Console.WriteLine("Date: {0}", bfa.Date);
        Console.WriteLine("Comment: {0}", bfa.Comment);
    }
}

Output:
Calling DoFunc(7). Result: 9.3333333333333333

BugID: 121
Programmer: Jesse Liberty
Date: 01/03/08
Comment:

BugID: 107
Programmer: Jesse Liberty
Date: 01/04/08
Comment: Fixed off by one errors

When you put this replacement code into Example 20-1 and run it, you can see the metadata printed as you’d expect.

Type Discovery

You can use reflection to explore and examine the contents of an assembly. You can find the types associated with a module; the methods, fields, properties, and events associated with a type, as well as the signatures of each of the type’s methods; the interfaces supported by the type; and the type’s base class.

To start, you load an assembly dynamically with the Assembly.Load( ) static method. The Assembly class encapsulates the actual assembly itself, for purposes of reflection. One signature for the Load method is:

public static Assembly.Load(AssemblyName)

For the next example, pass in the core library to the Load( ) method. Mscorlib.dll has the core classes of the .NET Framework:

Assembly a = Assembly.Load("Mscorlib");

Once the assembly is loaded, you can call GetTypes( ) to return an array of Type objects. The Type object is the heart of reflection. Type represents type declarations (classes, interfaces, arrays, values, and enumerations):

Type[] types = a.GetTypes( );

The assembly returns an array of types that you can display in a foreach loop, as shown in Example 20-3. Because this example uses the Type class, you will want to add a using directive for the System.Reflection namespace.

Example 20-3. Reflecting on an assembly
using System;
using System.Reflection;

namespace ReflectingAnAssembly
{
    public class Tester
    {
        public static void Main( )
        {
            // what is in the assembly
            Assembly a = Assembly.Load("Mscorlib");
            Type[] types = a.GetTypes( );
            foreach (Type t in types)
            {
                Console.WriteLine("Type is {0}", t);
            }
            Console.WriteLine(
                "{0} types found", types.Length);
        }
    }
}

The output from this would fill many pages. Here is a short excerpt:

Type is System.Object
Type is ThisAssembly
Type is AssemblyRef
Type is System.ICloneable
Type is System.Collections.IEnumerable
Type is System.Collections.ICollection
Type is System.Collections.IList
Type is System.Array
2373 types found

This example obtained an array filled with the types from the core library and printed them one by one. The array contained 2,373 entries on my machine.

Tip

In version 1.1, I found 1,426 entries on my machine. Microsoft has been busy!

Reflecting on a Type

You can reflect on a single type in the Mscorlib assembly as well. To do so, you extract a type from the assembly with either typeOf or the GetType( ) method, as shown in Example 20-4.

Example 20-4. Reflecting on a type
using System;

namespace ReflectingOnAType
{
    public class Tester
    {
        public static void Main( )
        {
            // examine a type
            Type theType = Type.GetType("System.Reflection.Assembly");
            Console.WriteLine("
Single Type is {0}
", theType);
        }
    }
}

Output:
Single Type is System.Reflection.Assembly

Finding all type members

You can ask the Assembly type for all its members using the GetMembers( ) method of the Type class, which lists all the methods, properties, and fields, as shown in Example 20-5.

Example 20-5. Reflecting on the members of a type
using System;
using System.Reflection;

namespace ReflectingOnMembersOfAType
{
    public class Tester
    {
        public static void Main( )
        {
            // examine a single object
            Type theType = Type.GetType("System.Reflection.Assembly");
            Console.WriteLine("
Single Type is {0}
", theType);

            // get all the members
            MemberInfo[] mbrInfoArray = theType.GetMembers( );
            foreach (MemberInfo mbrInfo in mbrInfoArray)
            {
                Console.WriteLine("{0} is a {1}",
                        mbrInfo, mbrInfo.MemberType);
            }
        }
    }
}

Once again, the output is quite lengthy, but within the output you see fields, methods, constructors, and properties, as shown in this excerpt:

System.Type GetType(System.String, Boolean, Boolean) is a Method
System.Type[] GetExportedTypes( ) is a Method
System.Reflection.Module GetModule(System.String) is a Method
System.String get_FullName( ) is a Method

Finding type methods

You might want to focus on methods only, excluding the fields, properties, and so forth. To do so, remove the call to GetMembers( ):

MemberInfo[] mbrInfoArray =
    theType.GetMembers( );

and add a call to GetMethods( ):

mbrInfoArray = theType.GetMethods( );

The output now contains nothing but the methods:

Output (excerpt):
Boolean Equals(System.Object) is a Method
System.String ToString( ) is a Method
System.String CreateQualifiedName(
System.String, System.String) is a Method
Boolean get_GlobalAssemblyCache( ) is a Method

Finding particular type members

Finally, to narrow it down even further, you can use the FindMembers method to find particular members of the type. For example, you can narrow your search to methods whose names begin with “Get.”

To narrow the search, use the FindMembers method, which takes four parameters:

MemberTypes

A MemberTypes object that indicates the type of the member to search for. These include All, Constructor, Custom, Event, Field, Method, Nestedtype, Property, and TypeInfo. You will also use the MemberTypes.Method to find a method.

BindingFlags

An enumeration that controls the way searches are conducted by reflection. There are a great many BindingFlags values, including IgnoreCase, Instance, Public, Static, and so forth.

MemberFilter

A delegate (see Chapter 12) that filters the list of members in the MemberInfo array of objects. You use a Type.FilterName filter, which is a field of the Type class that filters on a name.

Object

A string value used by the filter. In this case, you pass in Get* to match only those methods that begin with “Get.”

The complete listing for filtering on these methods is shown in Example 20-6.

Example 20-6. Finding particular members
using System;
using System.Reflection;

namespace FindingParticularMembers
{
    public class Tester
    {
        public static void Main( )
        {
            // examine a single object
            Type theType = Type.GetType("System.Reflection.Assembly");

            // just members which are methods beginning with Get
            MemberInfo[] mbrInfoArray = theType.FindMembers(
                                MemberTypes.Method,
                                BindingFlags.Public |
                                BindingFlags.Static |
                                BindingFlags.NonPublic |
                                BindingFlags.Instance |
                                BindingFlags.DeclaredOnly,
                                Type.FilterName, "Get*");
            foreach (MemberInfo mbrInfo in mbrInfoArray)
            {
                Console.WriteLine("{0} is a {1}",
                                mbrInfo, mbrInfo.MemberType);
            }
        }
    }
}


Output (excerpt):
System.Type GetType(System.String, Boolean, Boolean) is a Method
System.Type[] GetExportedTypes( ) is a Method
System.Reflection.Module GetModule(System.String) is a Method
System.Reflection.AssemblyName[] GetReferencedAssemblies( ) is a Method
Int64 GetHostContext( ) is a Method
System.String GetLocation( ) is a Method
System.String GetFullName( ) is a Method

Late Binding

Once you find a method, you can invoke it using reflection. For example, you might like to invoke the Cos( ) method of System.Math, which returns the cosine of an angle.

Tip

You can, of course, call Cos( ) in the normal course of your code, but reflection allows you to bind to that method at runtime. This is called late binding, and offers the flexibility of choosing at runtime which object to bind to and invoking it programmatically. This can be useful when creating a custom script to be run by the user or when working with objects that might not be available at compile time. For example, by using late binding, your program can interact with the spellchecker or other components of a running commercial word processing program such as Microsoft Word.

To invoke Cos( ), first get the Type information for the System.Math class:

Type theMathType = Type.GetType("System.Math");

With that type information, you can dynamically load an instance of a class using a static method of the Activator class. Because Cos( ) is static, you don’t need to construct an instance of System.Math (and you can’t because System.Math has no public constructor).

The Activator class contains four methods, all static, which you can use to create objects locally or remotely, or to obtain references to existing objects. The four methods are as follows:

CreateComInstanceFrom

Creates instances of COM objects.

CreateInstanceFrom

Creates a reference to an object from a particular assembly and type name.

GetObject

Used when marshaling objects.

CreateInstance

Creates local or remote instances of an object. For example:

Object theObj = Activator.CreateInstance(someType);

Back to the Cos( ) example. You now have one object in hand: a Type object named theMathType, which you created by calling GetType.

Before you can invoke a method on the object, you must get the method you need from the Type object, theMathType. To do so, you’ll call GetMethod( ), and you’ll pass in the signature of the Cos method.

The signature, you will remember, is the name of the method (Cos) and its parameter types. In the case of Cos( ), there is only one parameter: a double. However, Type.GetMethod takes two parameters. The first represents the name of the method you want, and the second represents the parameters. The name is passed as a string; the parameters are passed as an array of types:

MethodInfo CosineInfo =
 theMathType.GetMethod("Cos",paramTypes);

Before calling GetMethod( ), you must prepare the array of types:

Type[] paramTypes = new Type[1];
paramTypes[0]= Type.GetType("System.Double");

This code declares the array of Type objects, and then fills the first element (paramTypes[0]) with a type representing a double. Obtain the type representing a double by calling the static method Type.GetType( ), and passing in the string System.Double.

You now have an object of type MethodInfo on which you can invoke the method. To do so, you must pass in the object on which to invoke the method and the actual value of the parameters, again in an array. Because this is a static method, pass in theMathType (if Cos( ) were an instance method, you could use theObj instead of theMathType):

Object[] parameters = new Object[1];
parameters[0] = 45 * (Math.PI/180); // 45 degrees in radians
Object returnVal = CosineInfo.Invoke(theMathType,parameters);

Tip

Note that you’ve created two arrays. The first, paramTypes, holds the type of the parameters. The second, parameters, holds the actual value. If the method had taken two arguments, you’d have declared these arrays to hold two values. If the method didn’t take any values, you can still create the array, but you give it a size of zero:

Type[] paramTypes = new Type[0];

Odd as this looks, it is correct.

Example 20-7 illustrates dynamically calling the Cos( ) method.

Example 20-7. Dynamically invoking a method
using System;
using System.Reflection;

namespace DynamicallyInvokingAMethod
{
    public class Tester
    {
        public static void Main( )
        {
            Type theMathType = Type.GetType("System.Math");
            // Since System.Math has no public constructor, this
            // would throw an exception.
            // Object theObj =
            //     Activator.CreateInstance(theMathType);

            // array with one member
            Type[] paramTypes = new Type[1];
            paramTypes[0] = Type.GetType("System.Double");

            // Get method info for Cos( )
            MethodInfo CosineInfo =
                theMathType.GetMethod("Cos", paramTypes);

            // fill an array with the actual parameters
            Object[] parameters = new Object[1];
            parameters[0] = 45 * (Math.PI / 180); // 45 degrees in radians
            Object returnVal =
                CosineInfo.Invoke(theMathType, parameters);
            Console.WriteLine(
                "The cosine of a 45 degree angle {0}",
                returnVal);
        }
    }
}

Output:
The cosine of a 45 degree angle 0.707106781186548

That was a lot of work just to invoke a single method. The power, however, is that you can use reflection to discover an assembly on the user’s machine, to query what methods are available, and to invoke one of those members dynamically.

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

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