18

Reflection, Attributes, and Dynamic Programming

Attributes are a means of inserting additional metadata into an assembly and associating the metadata with a programming construct such as a class, method, or property. This chapter investigates the details surrounding attributes that are built into the framework and describes how to define custom attributes. To take advantage of custom attributes, it is necessary to identify them. This is handled through reflection.

This chapter begins with a look at reflection, including how you can use it to dynamically bind at execution time based on member invocation by name (or metadata) at compile time. Reflection is frequently leveraged within tools such as a code generator. In addition, reflection is used at execution time when the call target is unknown.

The chapter ends with a discussion of dynamic programming,1 a feature that greatly simplifies working with data that is dynamic and requires execution-time rather than compile-time binding.

Reflection

Using reflection, it is possible to do the following:

Access the metadata for types within an assembly. This includes constructs such as the full type name, member names, and any attributes decorating the construct.
Dynamically invoke a type’s members at runtime using the metadata, rather than a compile-time–defined binding.

Reflection is the process of examining the metadata within an assembly. Traditionally, when code compiles down to a machine language, all the metadata (such as type and method names) about the code is discarded. In contrast, when C# compiles into the Common Intermediate Language (CIL), it maintains most of the metadata about the code. Furthermore, using reflection, it is possible to enumerate through all the types within an assembly and search for those that match certain criteria. You access a type’s metadata through instances of System.Type, and this object includes methods for enumerating the type instance’s members. Additionally, it is possible to invoke those members on objects that are of the examined type.

The facility for reflection enables a host of new paradigms that otherwise are unavailable. For example, reflection enables you to enumerate over all the types within an assembly, along with their members, and in the process create stubs for documentation of the assembly API. To create the API documentation, you can then combine the metadata retrieved from reflection with the XML document created from XML comments (using the /doc switch). Similarly, programmers can use reflection metadata to generate code for persisting (serializing) business objects into a database. It can also be used in a list control that displays a collection of objects. Given the collection, a list control could use reflection to iterate over all the properties of an object in the collection, defining a column within the list for each property. Furthermore, by invoking each property on each object, the list control could populate each row and column with the data contained in the object, even though the data type of the object is unknown at compile time.

XmlSerializer, ValueType, and the Microsoft .NET Framework’s DataBinder are a few of the classes in the framework that use reflection for portions of their implementation as well.

Accessing Metadata Using System.Type

The key to reading a type’s metadata is to obtain an instance of System.Type that represents the target type instance. System.Type provides all the methods for retrieving the information about a type. You can use it to answer questions such as the following:

What is the type’s name (Type.Name)?
Is the type public (Type.IsPublic)?
What is the type’s base type (Type.BaseType)?
Does the type support any interfaces (Type.GetInterfaces())?
Which assembly is the type defined in (Type.Assembly)?
What are a type’s properties, methods, fields, and so on (Type.GetProperties(), Type.GetMethods(), Type.GetFields(), and so on)?
Which attributes decorate a type (Type.GetCustomAttributes())?

There are more such members, but all of them provide information about a particular type. The key is to obtain a reference to a type’s Type object, and the two primary ways to do so are through object.GetType() and typeof().

Note that the GetMethods() call does not return extension methods. These methods are available only as static members on the implementing type.

GetType()

object includes a GetType() member, so all types necessarily include this function. You call GetType() to retrieve an instance of System.Type corresponding to the original object. Listing 18.1 demonstrates this process, using a Type instance from DateTime. Output 18.1 shows the results.

Listing 18.1: Using Type.GetProperties() to Obtain an Object’s Public Properties
DateTime dateTime = new();
 
Type type = dateTime.GetType();
foreach(
    System.Reflection.PropertyInfo property in
        type.GetProperties())
{
    Console.WriteLine(property.Name);
}
Output 18.1
Date
Day
DayOfWeek
DayOfYear
Hour
Kind
Millisecond
Minute
Month
Now
UtcNow
Second
Ticks
TimeOfDay
Today
Year

After calling GetType(), you iterate over each System.Reflection.PropertyInfo instance returned from Type.GetProperties() and display the property names. The key to calling GetType() is that you must have an object instance. However, sometimes no such instance is available. Static classes, for example, cannot be instantiated, so there is no way to call GetType() with them.

typeof()

Another way to retrieve a Type object is with the typeof expression. typeof binds at compile time to a particular Type instance, and it takes a type directly as a parameter. The exception is for the type parameter on a generic type, as it isn’t determined until runtime. Listing 18.2 demonstrates the use of typeof with Enum.Parse().

Listing 18.2: Using typeof() to create a System.Type instance
using System.Diagnostics;
// ...
        ThreadPriorityLevel priority;
        priority = (ThreadPriorityLevel)Enum.Parse(
                typeof(ThreadPriorityLevel), "Idle");
        //...

In this listing, Enum.Parse() takes a Type object identifying an enum and then converts a string to the specific enum value. In this case, it converts "Idle" to System.Diagnostics.ThreadPriorityLevel.Idle.

Similarly, Listing 18.3 in the next section uses the typeof expression inside the CompareTo(object obj) method to verify that the type of the obj parameter was indeed what was expected:

if(obj.GetType() != typeof(Contact)) { ... }

The typeof expression is resolved at compile time such that a type comparison—perhaps comparing the type returned from a call to GetType()—can determine if an object is of a specific type.

Member Invocation

The possibilities with reflection don’t stop with retrieving the metadata. You can also take the metadata and dynamically invoke the members it identifies. Consider the possibility of defining a class to represent an application’s command line.2 The difficulty with a CommandLineInfo class such as this relates to populating the class with the actual command-line data that started the application. However, using reflection, you can map the command-line options to property names and then dynamically set the properties at runtime. Listing 18.3 demonstrates this process.

Listing 18.3: Dynamically Invoking a Member
using System.Diagnostics;
using System.IO;
using System.Reflection;
 
public partial class Program
{
    public static void Main(string[] args)
    {
        CommandLineInfo commandLine = new();
        if(!CommandLineHandler.TryParse(
            args, commandLine, out string? errorMessage))
        {
            Console.WriteLine(errorMessage);
            DisplayHelp();
        } 
        else if (commandLine.Help || string.IsNullOrWhiteSpace(commandLine.Out))
        {
            DisplayHelp();
        }
        else
        {
            if(commandLine.Priority !=
                ProcessPriorityClass.Normal)
            {
                // Change thread priority
            }
            // ...
        }
    }
 
    private static void DisplayHelp()
    {
        // Display the command-line help.
        Console.WriteLine(
            "Compress.exe /Out:< file name > /Help "
            + "/Priority:RealTime | High | "
            + "AboveNormal | Normal | BelowNormal | Idle");
 
    }
}
 
public partial class Program
{
    private class CommandLineInfo
    {
        public bool Help { getset; }
 
        public string? Out { getset; }
 
        public ProcessPriorityClass Priority { getset; }
            = ProcessPriorityClass.Normal;
    }
 
}
 
public class CommandLineHandler
{
    public static void Parse(string[] argsobject commandLine)
    {
        if (!TryParse(args, commandLine, out string? errorMessage))
        {
            throw new InvalidOperationException(errorMessage);
        }
    }
 
    public static bool TryParse(string[] argsobject commandLine,
        out string? errorMessage)
    {
        bool success = false;
        errorMessage = null;
        foreach(string arg in args)
        {
            string option;
            if(arg[0] == '/' || arg[0] == '-')
            {
                string[] optionParts = arg.Split(
                    new char[] { ':' }, 2);
 
                // Remove the slash|dash
                option = optionParts[0].Remove(0, 1);
                PropertyInfo? property =
                    commandLine.GetType().GetProperty(option,
                        BindingFlags.IgnoreCase |
                        BindingFlags.Instance |
                        BindingFlags.Public);
                if(property != null)
                {
                    if(property.PropertyType == typeof(bool))
                    {
                        // Last parameters for handling indexers
                        property.SetValue(
                            commandLine, truenull);
                        success = true;
                    }
                    else if(
                        property.PropertyType == typeof(string))
                    {
                        property.SetValue(
                            commandLine, optionParts[1], null);
                        success = true;
                    }
                    else if (
                        // property.PropertyType.IsEnum also available
                        property.PropertyType ==
                            typeof(ProcessPriorityClass))
                    {
                        try
                        {
                            property.SetValue(commandLine,
                                Enum.Parse(
                                    typeof(ProcessPriorityClass),
                                    optionParts[1], true),
                                null);
                            success = true;
                        }
                        catch (ArgumentException)
                        {
                            success = false;
                            errorMessage =
                                $@"The option '{
                                    optionParts[1]
                                    }' is invalid for '
                                    option }'";
                        }
                    }
                    else
                    {
                        success = false;
                        errorMessage = 
                            $@"Data type '{
                                property.PropertyType }' on {
                                commandLine.GetType() } is not supported.";
                    }
                }
                else
                {
                    success = false;
                    errorMessage = 
                       $"Option '{ option }' is not supported.";
                }
            }
        }
        return success;
    }
}

Although Listing 18.3 is long, the code is relatively simple. Main() begins by instantiating a CommandLineInfo class. This type is defined specifically to contain the command-line data for this program. Each property corresponds to a command-line option for the program, where the command line is as shown in Output 18.2.

Output 18.2
Compress.exe /Out:<file name> /Help
/Priority:RealTime|High|AboveNormal|Normal|BelowNormal|Idle

The CommandLineInfo object is passed to the CommandLineHandler’s TryParse() method. This method begins by enumerating through each option and separating out the option name (e.g., Help or Out). Once the name is determined, the code reflects on the CommandLineInfo object, looking for an instance property with the same name. If it finds such a property, it assigns the property using a call to SetValue() and specifies the data corresponding to the property type. (For arguments, this call accepts the object on which to set the value, the new value, and an additional index parameter that is null unless the property is an indexer.) This listing handles three property types: Boolean, string, and enum. In the case of enums, you parse the option value and assign the text’s enum equivalent to the property. Assuming the TryParse() call was successful, the method exits and the CommandLineInfo object is initialized with the data from the command line.

Interestingly, although CommandLineInfo is a private class nested within Program, CommandLineHandler has no trouble reflecting over it and even invoking its members. In other words, reflection can circumvent accessibility rules as long as appropriate permissions are established. For example, if Out was private, the TryParse() method could still assign it a value. Because of this, it would be possible to move CommandLineHandler into a separate assembly and share it across multiple programs, each with its own CommandLineInfo class.

In this example, you invoke a member on CommandLineInfo using PropertyInfo.SetValue(). Not surprisingly, PropertyInfo also includes a GetValue() method for retrieving data from the property. For a method, however, there is a MethodInfo class with an Invoke() member. Both MethodInfo and PropertyInfo derive from MemberInfo (albeit indirectly), as shown in Figure 18.1.

Figure 18.1: MemberInfo derived classes
Reflection on Generic Types

The introduction of generic types in version 2.0 of the Common Language Runtime (CLR) necessitated additional reflection features. Runtime reflection on generics determines whether a class or method contains a generic type and any type parameters or arguments it may include.

Determining the Type of Type Parameters

In the same way that you can use a typeof operator with nongeneric types to retrieve an instance of System.Type, so you can use the typeof operator on type parameters in a generic type or generic method. Listing 18.4 applies the typeof operator to the type parameter in the Add method of a Stack class.

Listing 18.4: Declaring the Stack<T> Class
public class Stack<T>
{
    //...
    public void Add(T i)
    {
        //...
        Type t = typeof(T);
        //...
    }
    //...
}

Once you have an instance of the Type object for the type parameter, you may then use reflection on the type parameter itself to determine its behavior and tailor the Add method to the specific type more effectively.

Determining Whether a Class or Method Supports Generics

In the System.Type class for the version 2.0 release of the CLR, a handful of methods were added that determine whether a given type supports generic parameters and arguments. A generic argument is a type parameter supplied when a generic class is instantiated. You can determine whether a class or method contains generic parameters that have not yet been set by querying the Type.ContainsGenericParameters property, as demonstrated in Listing 18.5 with Output 18.3.

Listing 18.5: Reflection with Generics
 
public class Program
{
    public static void Main()
    {
        Type type;
        type = typeof(System.Nullable<>);
        Console.WriteLine(type.ContainsGenericParameters);
        Console.WriteLine(type.IsGenericType);
 
        type = typeof(System.Nullable<DateTime>);
        Console.WriteLine(type.ContainsGenericParameters);
        Console.WriteLine(type.IsGenericType);
    }
}
Output 18.3
True
True
False
True

Type.IsGenericType is a Boolean property that evaluates whether a type is generic.

Obtaining Type Parameters for a Generic Class or Method

You can obtain a list of generic arguments, or type parameters, from a generic class by calling the GetGenericArguments() method. The result is an array of System.Type instances that corresponds to the order in which they were declared as type parameters of the generic class. Listing 18.6 reflects into a generic type and obtains each type parameter; Output 18.4 shows the results.

Listing 18.6: Using Reflection with Generic Types
 
public class Program
{
    public static void Main()
    {
        Stack<int> s = new();
 
        Type t = s.GetType();
 
        foreach(Type type in t.GetGenericArguments())
        {
            System.Console.WriteLine(
                "Type parameter: " + type.FullName);
        }
        //...
    }
}
Output 18.4
Type parameter: System.Int32

________________________________________

1. Added in C# 4.0.
2. The .NET Standard 1.6 added the CommandLineUtils NuGet package, which also provides a command-line parsing mechanism. For more information, see my MSDN article on the topic at http://itl.tc/sept2016.
{{ snackbarMessage }}
;