Reflection and Generics

In .NET Framework 2.0, Reflection was extended to accommodate open and closed generic types and methods. The Type class is the focal point of changes that extend reflection to accommodate generic types, while MethodInfo has been enhanced to reflect generic methods. (Generics were introduced in Chapter 7.) Open constructed types are generic types with unbound type parameters, whereas closed constructed types have bound type arguments. With Reflection, you can browse bound and unbound parameters, create instances of generic types, and invoke generic methods at run time.

IsGeneric and IsGenericTypeDefinition

With reflection, you can query the status of a type or method. Is a type or method generic? If generic, is the type or method open or closed?

Type.IsGeneric is a Boolean property that confirms whether a type is generic; Type.IsGenericTypeDefinition, another Boolean property, indicates whether the generic type is open or closed. For methods, the MethodInfo.IsGenericMethod property confirms whether a method is a generic method. The MethodInfo.IsGenericTypeDefinition property indicates whether the generic method is open or closed. The following program demonstrates the four properties:

using System;
using System.Reflection;

public class ZClass<T, V> {
    public T membera;
}

public class XClass {
    public void MethodA<T>() {
    }
}

namespace Donis.CSharpBook {

    class Starter {

        static void Main() {
            Type[] types = { typeof(ZClass<,>), typeof(ZClass<int,int>) };
            bool[,] bresp = { { types[0].IsGenericType,
                                types[0].IsGenericTypeDefinition },
                              { types[1].IsGenericType,
                                types[1].IsGenericTypeDefinition } };
            Console.WriteLine("Is ZClass<,> a generic type? " + bresp[0,0]);
            Console.WriteLine("Is ZClass<,> open? " + bresp[0,1]);
            Console.WriteLine("Is ZClass<int,int> a generic type? " + bresp[1,0]);
            Console.WriteLine("Is ZClass<int,int> open? " + bresp[1,1]);

            Type tObj = typeof(XClass);
            MethodInfo method = tObj.GetMethod("MethodA");
            bool[] bMethod = { method.IsGenericMethod,
                               method.IsGenericMethodDefinition };
            Console.WriteLine("Is XClass.MethodA<T> a generic method? " + bMethod[0]);
            Console.WriteLine("Is XClass.MethodA<T> open? " + bMethod[1]);
        }
    }
}

typeof

The typeof operator, demonstrated several times already in this chapter, also can be used with generic types. The Type object has a single parameter, which identifies the type. For a generic type, connote an open constructed type using empty type parameters. For example, ZClass<T> would be indicated as ZClass<>. Multiple generic type parameters (n parameters) are indicated with n-1 commas. For the typeof operator, ZClass<,> connotes the open constructed type for ZClass<K,V>. Indicate a closed constructed type by including the actual argument types, such as ZClass<int, int>. In addition to the typeof operator, Type.GetType and Type.GetGenericTypeDefinition methods can return Type objects for generic types.

GetType

Type.GetType is available in two flavors: an instance and a static method. Type.GetType, as an instance method, returns the type from an object, which can be an instance of a generic type. The method has no parameters. The Type.GetType static method is overloaded to return a Type object of a type. The pivotal parameter of the static GetType method is a string naming the type. This parameter can be used to indicate a generic type. To specify an open constructed type, the string is the name of the generic with the number of parameters affixed. The suffix is preceded with a grave accent character (`). For example, the string "NamespaceA.XClass`2" would represent XClass<K,V>. XClass has two type parameters. For a closed constructed type, you need to add the bound type arguments. After specifying the number of type arguments, list the bound type arguments. The bound type arguments are contained in square brackets. For example, the string "NamespaceB.ZClass`3[System.Int32, System.Int32, System.Decimal]" identifies ZClass<int, int, decimal>. Here is the general format for an open constructed type:

GenericType`NumberOfParameters

Here is the general format for a closed constructed type:

GenericType`NumberOfParameters[parameterList]

The following sample code demonstrates both the instance and static GetType methods:

using System;

namespace Donis.CSharpBook {

    class ZClass<K,V> {
        public void FunctionA(K argk, V argv) {
        }
    }

    class XClass<T> {
        public void FunctionB(T argt) {
        }
    }

    class Starter {

        public static void Main() {
            ZClass<int, decimal> obj = new ZClass<int, decimal>();
            Type typeClosed = obj.GetType();
            Console.WriteLine(typeClosed.ToString());

            Type typeOpen = Type.GetType("Donis.CSharpBook.XClass`1");
            Console.WriteLine(typeOpen.ToString());
            Type typeClosed2 = Type.GetType(
                "Donis.CSharpBook.ZClass`2[System.Int32, System.Decimal]");
            Console.WriteLine(typeClosed2.ToString());
        }
    }
}

GetGenericTypeDefinition

Closed constructed types are created from open constructed types. Type.GetGenericTypeDefinition returns the Type object for the underlying type (the open constructed type) used to build a closed constructed type.

Here is the Type.GetGenericTypeDefinition signature:

Type GetGenericTypeDefinition()

The following code highlights the GetGenericTypeDefinition method:

using System;

namespace Donis.CSharpBook {

    class ZClass<K,V> {
        public void FunctionA(K argk, V argv) {
        }
    }

    class Starter {

        public static void Main() {
            ZClass<int, decimal> obj = new ZClass<int, decimal>();
            ZClass<string, float> obj2 = new ZClass<string, float>();

            Type closedType = obj.GetType();
            Type openType = closedType.GetGenericTypeDefinition();

            Type closedType2 = obj2.GetType();
            Type openType2 = closedType2.GetGenericTypeDefinition();

            Console.WriteLine(openType.ToString());
            Console.WriteLine(openType2.ToString());
        }
    }
}

The preceding code displays identical strings in the console window. Why? The underlying open constructed type of ZClass<int, decimal> and ZClass<string, float> is the same, namely ZClass<K, V>.

MethodInfo.GetGenericMethodDefinition is comparable to Type.GetGenericTypeDefinition, but pertains to methods rather than types. Similarly, the Type.GetMethod method is comparable to Type.GetType but pertains to methods.

GetGenericArguments

You can now extract a Type object for a generic type and a MethodInfo object for a generic method. Determining the number of unbound or bound parameters is a natural next step. For example, if bound, looking at the type of each parameter is an appropriate error-checking step. GetGenericArguments is the universal method for enumerating parameters of a generic type or method. GetGenericArguments enumerates unbound and bound parameters.

Here is the Type.GetGenericArguments signature:

Type[] GetGenericArguments()

The MethodInfo.GetGenericArguments signature is identical:

Type[] GetGenericArguments()

The following code demonstrates both Type.GetGenericArguments and MethodInfo.GetGenericArguments:

using System;

namespace Donis.CSharpBook {

    class ZClass<K, V>{
        public void FunctionA(K argk, V argv) {
        }
    }

    class Starter {
        public static void Main() {
            int count=0;
            ZClass<int, decimal> obj = new ZClass<int, decimal>();
            Type zObj = obj.GetType();
            object[] arguments = zObj.GetGenericArguments();
            foreach (object argument in arguments) {
                Console.WriteLine("zObj argument {0}: {1}",
                    (++count).ToString(), argument.ToString());
            }
            count = 0;
            Type zType = typeof(ZClass<,>);
            object[] parameters = zType.GetGenericArguments();
            foreach (object parameter in parameters) {
                Console.WriteLine("ZClass parameters {0}: {1}",
                    (++count).ToString(), parameter.ToString());
            }
        }
    }
}

Creating Generic Types

Generic types can be created at run time using reflection. First, determine the open constructed type of the generic. This chapter has already shown several ways to accomplish this, including using GetType and GetGenericTypeDefinition. Next, bind the type arguments of the open constructed type. The result will be a closed constructed type. Finally, create an instance of the closed constructed type in the customary manner. (Activator.CreateInstance works well.)

The Type.MakeGenericType method binds type arguments to a generic open constructed type. MakeGenericType has a single parameter, which is an array of Type objects. Each element of the array is a type argument. If the generic has three parameters, the array passed to MakeGenericType will have three elements, each representing a type argument.

Here is the signature of the MakeGenericType method:

Type.MakeGenericType syntax:
void MakeGenericType(Type[] genericArguments)

Generic methods, like non-generic methods, can be invoked dynamically at run time. MethodInfo.MakeGenericMethod binds type arguments to generic methods, similar to Type.MakeGenericType. The benefits and pitfalls of dynamic invocation are similar to non-generic methods. After binding parameters to a generic method, the method can be invoked using reflection. The following code creates a generic type and then invokes a generic method at run time:

using System;
using System.Reflection;


namespace Donis.CSharpBook {

    public class GenericZ <K, V, Z>
        where K: new()
        where V: new() {

        public void MethodA<A>(A argument1, Z argument2) {
            Console.WriteLine("MethodA invoked");
        }

        private K field1 = new K();
        private V field2 = new V();
    }

    class Starter {

        static void Main() {
            Type genericType = typeof(GenericZ<,,>);
            Type[] typeArguments = {typeof(int),typeof(float),
                typeof(int)};
            Type closedType = genericType.MakeGenericType(typeArguments);
            MethodInfo openMethod = closedType.GetMethod("MethodA");
            object newObject = Activator.CreateInstance(closedType);
            typeArguments = new Type[] {typeof(int)};
            MethodInfo closedMethod =
                openMethod.MakeGenericMethod(typeArguments);
            object[] methodargs = {2, 10};
            closedMethod.Invoke(newObject, methodargs);
        }
    }
}

Reflection Security

Some reflection operations, such as accessing protected and private members, require the ReflectionPermission security permission. ReflectionPermission is typically granted to local and intranet applications, but not to Internet applications. Set the appropriate ReflectionPermission flag to grant or deny specific reflection operations. Table 13-9 lists the ReflectionPermission flags.

Table 13-9. ReflectionPermission flags

Flag

Description

MemberAccess

Reflection of visible members granted.

NoFlags

Reflection and enumeration allowed for types and members. However, invocation only allowed on visible members.

ReflectionEmit

System.Reflection.Emit operations granted. Not required after .NET Framework 2.0 Service Pack 1.

TypeInformation

This flag is now deprecated.

AllFlags

Combines TypeInformation, MemberAccess, and ReflectionEmit flags.

Attributes

Attributes extend the description of a member of an assembly. Attributes fulfill the role of adjectives in .NET and can annotate assemblies, classes, methods, parameters, and other items. Attributes are commonplace in .NET and fulfill many roles, including delineating serialization, stipulating import linkage, setting class layout, indicating conditional compilation, and marking a method as deprecated.

Attributes extend the metadata of the target member. An instance of the attribute is stored alongside the metadata and sometimes alongside the MSIL code of the constituent. The attributes in the following code are shown in Figure 13-4. The Conditional attribute marks a method for conditional compilation. With this attribute, the target method and call sites are included in the compiled application if the LOG symbol is defined. If it is not defined, the method and any invocation are omitted from the compilation:

#define LOG

using System;
using System.IO;
using System.Diagnostics;

namespace Donis.CSharpBook {

    class Starter {
        static void Main() {
            LogInfo(new StreamWriter(@"c:logfile.txt"));
        }

        [Conditional("LOG")]
        private static void LogInfo(StreamWriter sr) {
            // write information to log file
        }
    }
}
The Conditional attribute in MSIL code

Figure 13-4. The Conditional attribute in MSIL code

Figure 13-4 shows the MSIL code of the LogInfo method as displayed in ILDASM. Notice the custom directive that defines the Conditional attribute. It is integrated into the MSIL code of the method.

Attributes are available in different flavors: predefined custom attributes, programmer-defined custom attributes, and pseudo-custom attributes. All three are explained next.

Predefined Custom Attributes

Predefined custom attributes, which are defined in the FCL, are the most prevalent custom attributes. Predefined custom attributes include AssemblyVersion, Debuggable, FileIOPermission, Flags, and Obsolete. Many responsibilities in .NET are fulfilled by predefined custom attributes. The following code uses the Obsolete attribute to flag a method as deprecated:

[Obsolete("Deprecated Method", false)]
public static void MethodA() {
    Console.WriteLine("Starter.MethodA");
}

Pseudo-custom Attributes

Pseudo-custom attributes are interpreted by the run time and modify the metadata of the assembly. Unlike predefined custom attributes, pseudo-custom attributes do not extend metadata. Examples of pseudo-custom attributes include DllImport, MarshalAs, and Serializable.

Combining Attributes

A member can be assigned more than one attribute. In certain circumstances, the same attribute can even be applied multiple times. There are two ways to combine attributes. They can be listed separately or combined. Here, the attributes are applied separately:

[Obsolete("Deprecated Method", false)]
[FileIOPermission(SecurityAction.Demand,
      Unrestricted = true)]
public static void MethodA() {
    Console.WriteLine("Starter.MethodA");
}

In the following code, two attributes are combined and applied to a method:

[Obsolete("Deprecated Method", false),
 FileIOPermission(SecurityAction.Demand,
      Unrestricted = true)]
public static void MethodA() {
    Console.WriteLine("Starter.MethodA");
}

Anatomy of an Attribute

Attributes are types derived from the System.Attribute class, which is the common base class of all attributes. System.Attribute is an abstract class defining the required services of any attribute. Managed compilers and the run time sometimes act differently based on an attribute. For example, the ObsoleteAttribute causes the compiler to generate error or warning messages when a deprecated member is used.

Here is the syntax of an attribute:

[type: attributeName(positionalParameter1, ..., positionalParametern,

    namedParameter1 = value, ..., namedParametern = value)] target

The attribute name is the class name of the attribute. By convention, attribute names have an Attribute suffix. OneWayAttribute is representative of an attribute name. You also can use the alias of an attribute, which omits the Attribute suffix. The alias for OneWayAttribute would be OneWay.

Attributes accept zero or more positional parameters. Positional parameters are ordered and must be used in a specific sequence. In addition, an attribute might have any number of named parameters, which must follow positional parameters. Named parameters are optional. Named parameters are not ordered and can be presented in any sequence.

The following code applies the UIPermissionAttribute to the Starter class:

[type: UIPermissionAttribute(SecurityAction.Demand,
    Clipboard = UIPermissionClipboard.OwnClipboard)]
class Starter {
    static void Main() {

    }
}

Here is the same attribute expressed somewhat more succinctly:

[type: UIPermissionAttribute(SecurityAction.Demand,
    Clipboard = UIPermissionClipboard.OwnClipboard)]
class Starter {
    static void Main() {

    }
}

Programmer-Defined Custom Attributes

You can create custom attributes for private consumption or to be published in a library for others. There are definitive procedures to creating a programmer-defined custom attribute. Here are the steps to follow:

  1. Select an appropriate name for the custom attribute. Attribute names should conclude with the Attribute suffix.

  2. Derive the attribute class from System.Attribute.

  3. Set potential targets with the AttributeUsage attribute.

  4. Implement class constructors, which determine the positional parameters.

  5. Implement write-accessible properties, which define the named parameters.

  6. Implement other members of the class as needed based on the role of the attribute.

I will use ClassVersionAttribute to demonstrate these steps. This attribute is a programmer-defined custom attribute that can apply a version number to types. By default, you can assign version numbers only to assemblies. The attribute assigns two version numbers to a type: a target version and a current version. The target version is the version number of the present type. The current version number is the version number of the most recent version of that type. When the type is the most recent version, the target and current version numbers are the same. In addition, the ClassVersionAttribute can request that the most recent version of the type should always be used.

The first and most important task of creating a programmer-defined attribute is selecting a name. Alas, ClassVersionAttribute is a mundane but descriptive name. As an attribute, ClassVersionAttribute must inherit System.Attribute. AttributeUsageAttribute sets the potential target of the attributes. AttributeTargets is the only positional parameter of the AttributeUsageAttribute and is a bitwise enumeration. AttributeUsageAttribute has three named parameters: AllowMultiple, Inherited, and ValidOn. AllowMultiple is a Boolean flag. When true, the attribute can be applied multiple times to the same target. The default is false. Inherited is another Boolean flag. If true, which is the default, the attribute is inheritable from a base class. ValidOn is an AttributeTargets enumeration. This is an alternate method of setting the potential target of the attribute.

Here is a list of the available AttributeTargets flags:

  • All

  • Assembly

  • Class

  • Constructor

  • Delegate

  • Enum

  • Event

  • Field

  • GenericParameter

  • Interface

  • Method

  • Module

  • Parameter

  • Property

  • ReturnValue

  • Struct

Here is the start of the ClassVersionAttribute class:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
    Inherited = false)]
public class ClassVersionAttribute : System.Attribute {
}

Instance constructors of an attribute provide the positional parameters for the attribute. When an attribute is used, the constructor runs. The positional arguments should match the signature of a constructor in the attribute class. Overloading the constructor allows different sets of positional parameters. Positional and named parameters are restricted to certain types. For example, a positional or named parameter cannot be a decimal. The following is a list of available attribute parameter types:

  • bool

  • byte

  • char

  • double

  • float

  • int

  • long

  • sbyte

  • short

  • string

  • uint

  • ulong

  • ushort

ClassVersionAttribute has two overloaded constructors. The first constructor sets the target version and the current version to a single specified value; the second constructor sets the target and current version number to different specified values:

public ClassVersionAttribute(string target)
    : this(target, target) {
}

public ClassVersionAttribute(string target,
        string current) {
    m_TargetVersion = target;
    m_CurrentVersion = current;
}

Define named parameters as write-only or read-write instance properties of the attribute class. You can duplicate positional parameters as named parameters for additional flexibility. ClassVersionAttributes offers a UseCurrentVersion and CurrentName named parameter. UseCurrentVersion is a Boolean value. If true, the most current type should be used in all circumstances. CurrentName assigns a string name to this version, which is optional. This is how the named parameters are implemented in the ClassVersionAttribute class:

private bool m_UseCurrentVersion = false;
public bool UseCurrentVersion {
    set {
        if (m_TargetVersion != m_CurrentVersion) {
            m_UseCurrentVersion = value;
        }
    }
    get {
        return m_UseCurrentVersion;
    }
}

private string m_CurrentName;
public string CurrentName {
    set {
        m_CurrentName = value;
    }
    get {
        return m_CurrentName;
    }
}

For completeness, read-only properties are added to the ClassVersionAttribute class for the target and current version. Here is the completed ClassVersionAttribute class:

using System;

namespace Donis.CSharpBook {
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
        Inherited = false)]
    public class ClassVersionAttribute : System.Attribute {

        public ClassVersionAttribute(string target)
            : this(target, target) {
        }

        public ClassVersionAttribute(string target,
                string current) {
            m_TargetVersion = target;
            m_CurrentVersion = current;
        }

        private bool m_UseCurrentVersion = false;
        public bool UseCurrentVersion {
            set {
                if (m_TargetVersion != m_CurrentVersion) {
                    m_UseCurrentVersion = value;
                }
            }
            get {
                return m_UseCurrentVersion;
            }
        }

        private string m_CurrentName;
        public string CurrentName {
            set {
                m_CurrentName = value;
            }
            get {
                return m_CurrentName;
            }
        }

        private string m_TargetVersion;
        public string TargetVersion {
            get {
                return m_TargetVersion;
            }
        }

        private string m_CurrentVersion;
        public string CurrentVersion {
            get {
                return m_CurrentVersion;
            }
        }
    }
}

The ClassVersionAttribute class is compiled and published in a DLL. The following application nominally uses the ClassVersionAttribute:

using System;

namespace Donis.CSharpBook {
    class Starter {
        static void Main() {

        }
    }
    [ClassVersion("1.1.2.1", UseCurrentVersion = false)]
    class ZClass {
    }
}

Attributes and Reflection

Programmer-defined custom attributes are sometimes valuable simply as information. However, the real fun and power lies in associating a behavior with an attribute. You can read custom attributes with reflection using the Attribute.GetCustomAttribute and Type.GetCustomAttributes methods. Both methods return an instance or instances of an attribute. You can downcast the attribute instance to a specific attribute type and use the public interface to invoke the appropriate behavior at run time.

Type.GetCustomAttributes returns attributes applied to a type. GetCustomAttributes also is available with other members to extract their attributes, including Assembly MemberInfo, and ParameterInfo. GetCustomAttributes has a single Boolean parameter. If true, the inheritance hierarchy of the type is evaluated for additional attributes.

Here is the Type.GetCustomAttributes method:

object[] GetCustomAttributes(bool inherit)
object[] GetCustomAttribute(type AttributeType, bool inherit)

Attribute.GetCustomAttribute, which is a static method, returns an instance of a specific attribute. When a specific attribute is desired, GetCustomAttribute is more efficient. GetCustomAttribute is overloaded for two and three arguments. The two-argument versions have the target and attribute type as parameters. The three-argument version has an additional parameter, which is the inherit parameter. If the inherit parameter is true, the ascendants of the target class also are searched for the attribute.

Here is a sampling of the two-argument overloaded Attribute.GetCustomAttribute method:

static Attribute GetCustomAttribute(Assembly targetAssembly, type attributeType)
static Attribute GetCustomAttribute(MemberInfo targetMember, type attributeType)
static Attribute GetCustomAttribute(Module targetModule, type attributeType)
static Attribute GetCustomAttribute(ParameterInfo targetParameter, type attributeType)

The following sample code uses GetCustomAttribute:

using System;
using System.Reflection;

namespace Donis.CSharpBook {

    class Starter {

        static void Main() {
            Type tObj = typeof(Starter);
            MethodInfo method = tObj.GetMethod("AMethod");
            Attribute attrib = Attribute.GetCustomAttribute(
                method, typeof(ObsoleteAttribute));
            ObsoleteAttribute obsolete = (ObsoleteAttribute) attrib;
            Console.WriteLine("Obsolete Message: " + obsolete.Message);
        }

        [Obsolete("Deprecated function.", false)]
        public void AMethod() {
        }
    }
}

The following sample code uses GetCustomAttributes. The application inspects the ClassVersionAttribute attribute with the GetCustomAttributes method and acts upon the results. The application contains two versions of ZClass and both are applied to the ClassVersionAttribute. Each version of ZClass shares a common interface: IZClass. The ClassVersionAttribute of Donis.CSharpBook.ZClass lists ANamespace.ZClass as the current version or most recent version. Also, UseCurrentVersion is assigned true to indicate that the newer version should replace instances of Donis.CSharpBook.ZClass. The function CreateZClass is a helper method for the ClassVersionAttribute. It accepts a ZClass as defined by the IZClass interface. Within the foreach loop, GetCustomAttributes enumerates the attributes of the ZClass. If the attribute is a ClassVersionAttribute, the attribute is saved and the foreach loop is exited. Next, the properties of the attribute are examined. If UseCurrentVersion is true and a current version is named, an instance of the new version is created and returned from the method. Otherwise, the target version is returned:

using System;
using System.Reflection;

interface IZClass {
    void AMethod();
}

namespace Donis.CSharpBook {
    class Starter {
        static void Main() {
            IZClass obj = CreateZClass(typeof(ZClass));
            obj.AMethod();
        }

        private static IZClass CreateZClass(Type tObj) {
            ClassVersionAttribute classversion = null;
            foreach (Attribute attrib in tObj.GetCustomAttributes(false)) {
                if (attrib.ToString() ==
                    typeof(ClassVersionAttribute).ToString()) {
                    classversion = (ClassVersionAttribute) attrib;
                }
                else {
                    return null;
                }
            }
            if (classversion.UseCurrentVersion &&
                (classversion.CurrentName != null)) {
                AppDomain currDomain = AppDomain.CurrentDomain;
                return (IZClass) currDomain.CreateInstanceFromAndUnwrap(
                    "client.exe", classversion.CurrentName);
            }
            else {
                return (IZClass) Activator.CreateInstance(tObj);
            }
        }
    }

    [ClassVersion("1.1.2.1", "2.0.0.0", UseCurrentVersion = true,
        CurrentName = "Donis.CSharpBook.ANamespace.ZClass")]
    public class ZClass: IZClass {
        public void AMethod() {
            Console.WriteLine("AMethod: old version");
        }
    }

    namespace ANamespace {
        [ClassVersion("2.0.0.0", UseCurrentVersion = false)]
        public class ZClass: IZClass {
            public void AMethod() {
                Console.WriteLine("AMethod: new version");
            }
        }
    }
}
..................Content has been hidden....................

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