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.
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. COM
interface programming is discussed in detail in Chapter 22.
In Chapter 17, you saw this attribute:
[assembly: AssemblyKeyFile("c:\myStrongName.key")]
This inserts metadata into the assembly to designate the program’s strong name.[1]
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, described later in this chapter.
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 18-1.
Table 18-1. Possible attribute targets
Member name |
Usage |
---|---|
Applied to any of the following elements: assembly, class, constructor, delegate, enum, event, field, interface, method, module, parameter, property, return value, or struct | |
Applied to the assembly itself | |
Applied to a class | |
Applied to a given constructor | |
Applied to a delegate | |
Applied to an enumeration | |
Applied to an event | |
Applied to a field | |
Applied to an interface | |
Applied to a method | |
Applied to a single module | |
Applied to a parameter of a method | |
Applied to a property (both | |
Applied to a return value | |
Applied to a struct |
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")]
This can also be done by separating the attributes with commas:
[assembly: AssemblyDelaySign(false), assembly: AssemblyKeyFile(".\keyFile.snk")]
Many attributes are used for interoperating with COM, as discussed in
detail in Chapter 22. 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 19.
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 19, 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.
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/2005.
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/2005", 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.
This may be a somewhat artificial example, however, because these attributes would be compiled into the shipping code.
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.
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/05", 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
.
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 { return comment; } set { comment = value; } }
It is common to create read-only properties for the positional parameters:
public int BugID { get { return bugID; } }
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/05")] [BugFixAttribute(107,"Jesse Liberty","01/04/05", Comment="Fixed off by one errors")] public class MyMath
These attributes are stored with the metadata. Example 18-1 shows the complete program.
Example 18-1. Working with custom attributes
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion 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 { // private member data private int bugID; private string comment; private string date; private string programmer; // attribute constructor for // positional parameters public BugFixAttribute ( int bugID, string programmer, string date ) { this.bugID = bugID; this.programmer = programmer; this.date = date; } // accessor public int BugID { get { return bugID; } } // property for named parameter public string Comment { get { return comment; } set { comment = value; } } // accessor public string Date { get { return date; } } // accessor public string Programmer { get { return programmer; } } } // ********* assign the attributes to the class ******** [BugFixAttribute( 121, "Jesse Liberty", "01/03/05" )] [BugFixAttribute( 107, "Jesse Liberty", "01/04/05", 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 { public static void Main( ) { 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 18-1. You’ll see how to get at this metadata and use it in your program in the next section.
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.
This might be used by tools and utilities that wish to display metadata.
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.
This allows the programmer to invoke properties and methods on objects dynamically instantiated, based on type discovery. This is also known as dynamic invocation.
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.
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
.
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 18-2 replaces the Tester
class
from Example 18-1.
Example 18-2. Using reflection
public static void Main() { 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/05 Comment: BugID: 107 Programmer: Jesse Liberty Date: 01/04/05 Comment: Fixed off by one errors
When you put this replacement code into Example 18-1 and run it, you can see the metadata printed as you’d expect.
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, 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 18-3. Because this example uses the
Type
class, you will want to add a
using
directive for the
System.Reflection
namespace.
Example 18-3. Reflecting on an assembly
#region Using directives using System; using System.Collections.Generic; using System.Reflection; using System.Text; #endregion namespace ReflectingAnAssembly { public classTester { 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.
You
can reflect on a
single type in the Mscorlib
assembly as well. To
do so, extract a type from the assembly with either
typeOf
or the
GetType()
method, as shown in Example 18-4.
Example 18-4. Reflecting on a type
#region Using directives using System; using System.Collections.Generic; using System.Reflection; using System.Text; #endregion namespace ReflectingOnAType { public classTester { 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
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 18-5.
Example 18-5. Reflecting on the members of a type
#region Using directives using System; using System.Collections.Generic; using System.Reflection; using System.Text; #endregion namespace ReflectingOnMembersOfAType { public classTester { 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
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 is 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
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 the letters 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
the letters Get.
The complete listing for filtering on these methods is shown in Example 18-6.
Example 18-6. Finding particular members
#region Using directives using System; using System.Collections.Generic; using System.Reflection; using System.Text; #endregion namespace FindingParticularMembers { public classTester { 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
Once
you find a method,
it’s possible to invoke it using reflection. For
example, you might like to invoke the
Cos( )
method of
System.Math
, which returns the cosine of an angle.
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, that 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. Marshaling is discussed in detail in Chapter 19.
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 to invoke the method on and the
actual value of the parameters, again in an array. Since 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);
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];
Example 18-7 illustrates dynamically calling the
Cos()
method.
Example 18-7. Dynamically invoking a method
#region Using directives using System; using System.Collections.Generic; using System.Reflection; using System.Text; #endregion namespace DynamicallyInvokingAMethod { public classTester { 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.
[1] Actually the assembly attribute does more than just insert metadata. The C# compiler watches for this particular attribute (as well as several others) and triggers special behavior; in this case, it reads the key file and uses it to sign the assembly. Typically, however, attributes are just static metadata inserted in the assembly.
18.220.139.168