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, as well as how to define custom attributes. In order 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 runtime and call a member using its name at compile time. This is frequently performed 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, a feature added in C# 4.0 that greatly simplifies working with data that is dynamic and requires execution-time rather than compile-time binding.
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 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 particular 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. You can then combine the metadata retrieved from reflection with the XML document created from XML comments (using the /doc
switch) to create the API documentation. Similarly, programmers use reflection metadata to generate code for persisting (serializing) business objects into a database. It could 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 DataBinder
are a few of the classes in the framework that use reflection for portions of their implementation as well.
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)?
• What attributes decorate a type (Type.GetCustomAttributes()
)?
There are more such members, but in summary, they all 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 this are through object.GetType()
and typeof()
.
Note that the GetMethods()
call does not return extension methods. They are available only as static members on the implementing type.
object
includes a GetType()
member, and therefore, all types include this function. You call GetType()
to retrieve an instance of System.Type
corresponding to the original object. Listing 17.1 demonstrates this, using a Type
instance from DateTime
. Output 17.1 shows the results.
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()
.
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. Listing 17.2 demonstrates the use of typeof
with Enum.Parse()
.
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
.
The possibilities with reflection don’t stop with retrieving the metadata. The next step is to take the metadata and dynamically invoke the members it references. Consider the possibility of defining a class to represent an application’s command line. The difficulty with a CommandLineInfo
class such as this has to do with 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 17.3 demonstrates this example.
Although Listing 17.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 17.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 (Help
or Out
, for example). Once the name is determined, the code reflects on the CommandLineInfo
object, looking for an instance property with the same name. If the property is found, 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 property the text’s enum equivalent. Assuming the TryParse()
call was successful, the method exits and the CommandLineInfo
object is initialized with the data from the command line.
Interestingly, in spite of the fact that CommandLineInfo
is a private class nested within Program
, CommandLineHandler
has no trouble reflecting over it and even invoking its members. In other words, reflection is able to circumvent accessibility rules as long as appropriate code access security (CAS; see Chapter 21) permissions are established. If, for example, Out
was private, it would still be possible for the TryParse()
method to 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 their own CommandLineInfo
class.
In this particular 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
(although indirectly), as shown in Figure 17.1.
The CAS permissions are set up to allow private member invocation in this case because the program runs from the local computer, and by default, locally installed programs are part of the trusted zone and have appropriate permissions granted. Programs run from a remote location will need to be explicitly granted such a right.
The introduction of generic types in version 2.0 of the 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.
In the same way that you can use a typeof
operator with nongeneric types to retrieve an instance of System.Type
, you can use the typeof
operator on type parameters in a generic type or generic method. Listing 17.4 applies the typeof
operator to the type parameter in the Add
method of a Stack
class.
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.
In the System.Type
class for the version 2.0 release of the CLR, a handful of methods were added to 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 17.5.
Output 17.3 shows the results of Listing 17.5.
True
True
True
True
Type.IsGenericType
is a Boolean property that evaluates whether a type is generic.
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 are declared as type parameters of the generic class. Listing 17.6 reflects into a generic type and obtains each type parameter. Output 17.4 shows the results.
Type parameter: System.Int32
Before delving into details on how to program attributes, we should consider a use case that demonstrates their utility. In the CommandLineHandler
example in Listing 17.3, you dynamically set a class’s properties based on the command-line option matching the property name. This approach is insufficient, however, when the command-line option is an invalid property name. /?
, for example, cannot be supported. Furthermore, this mechanism doesn’t provide any way of identifying which options are required versus which are optional.
Instead of relying on an exact match between the option name and the property name, attributes provide a way of identifying additional metadata about the decorated construct—in this case, the option that the attribute decorates. With attributes, you can decorate a property as Required
and provide a /?
option alias. In other words, attributes are a means of associating additional data with a property (and other constructs).
Attributes appear within square brackets preceding the construct they decorate. For example, you can modify the CommandLineInfo
class to include attributes, as shown in Listing 17.7.
In Listing 17.7, the Help
and Out
properties are decorated with attributes. The purpose of these attributes is to allow an alias of /?
for /Help
, and to indicate that /Out
is a required parameter. The idea is that from within the CommandLineHandler.TryParse()
method, you enable support for option aliases and, assuming the parsing was successful, you can check that all the required switches were specified.
There are two ways to combine attributes on the same construct. You can either separate the attributes with commas within the same square brackets, or place each attribute within its own square brackets, as shown in Listing 17.8.
In addition to decorating properties, developers can use attributes to decorate classes, interfaces, structs, enums, delegates, events, methods, constructors, fields, parameters, return values, assemblies, type parameters, and modules. For the majority of these, applying an attribute involves the same square bracket syntax shown in Listing 17.8. However, this syntax doesn’t work for return values, assemblies, and modules.
Assembly attributes are used to add additional metadata about the assembly. Visual Studio’s Project Wizard, for example, generates an AssemblyInfo.cs
file that includes numerous attributes about the assembly. Listing 17.9 is an example of such a file.
The assembly
attributes define things such as company, product, and assembly version number. Similar to assembly
, identifying an attribute usage as module
requires prefixing it with module:
. The restriction on assembly
and module
attributes is that they appear after the using
directive but before any namespace or class declarations. The attributes listed above are generated by the Visual Studio Project Wizard and should be included in all projects to mark the resultant binaries with information about the contents of the executable or DLL.
Return attributes, such as the one shown in Listing 17.10, appear before a method declaration but use the same type of syntax structure.
In addition to assembly:
and return:
, C# allows for explicit target identifications of module:
, class:
, and method:
, corresponding to attributes that decorate the module, class, and method. class:
and method:
, however, are optional, as demonstrated earlier.
One of the conveniences of using attributes is that the language takes into consideration the attribute naming convention, which is to place Attribute
at the end of the name. However, in all the attribute uses in the preceding listings, no such suffix appears, despite the fact that each attribute used follows the naming convention. This is because although the full name (DescriptionAttribute
, AssemblyVersionAttribute
, and so on) is allowed when applying an attribute, C# makes the suffix optional. Generally, no such suffix appears when applying an attribute; it appears only when defining one or using the attribute inline (such as typeof(DescriptionAttribute)
).
Defining a custom attribute is relatively trivial. Attributes are objects; therefore, to define an attribute, you need to define a class. The characteristic that turns a general class into an attribute is that it derives from System.Attribute
. Consequently, you can create a CommandLineSwitchRequiredAttribute
class, as shown in Listing 17.11.
With that simple definition, you now can use the attribute as demonstrated in Listing 17.7. So far, no code responds to the attribute; therefore, the Out
property that includes the attribute will have no effect on command-line parsing.
In addition to providing properties for reflecting on a type’s members, Type
includes methods to retrieve the Attribute
s decorating that type. Similarly, all the reflection types (PropertyInfo
and MethodInfo
, for example) include members for retrieving a list of attributes that decorate a type. Listing 17.12 defines a method to return a list of required switches that are missing from the command line.
The code that checks for an attribute is relatively simple. Given a PropertyInfo
object (obtained via reflection), you call GetCustomAttributes()
and specify the attribute sought, followed by whether to check any overloaded methods. (Alternatively, you can call the GetCustomAttributes()
method without the attribute type to return all of the attributes.)
Although it is possible to place code for finding the CommandLineSwitchRequiredAttribute
attribute within the CommandLineHandler
’s code directly, it makes for better object encapsulation to place the code within the CommandLineSwitchRequiredAttribute
class itself. This is frequently the pattern for custom attributes. What better location to place code for finding an attribute than in a static method on the attribute class?
The call to GetCustomAttributes()
returns an array of objects that will successfully cast to an Attribute
array. But since the attribute in this example didn’t have any instance members, the only metadata information that it provided in the returned attribute was whether it appeared. Attributes can also encapsulate data, however. Listing 17.13 defines a CommandLineAliasAttribute
attribute. This is another custom attribute, and it provides alias command-line options. For example, you can provide command-line support for /Help
or /?
as an abbreviation. Similarly, /S
could provide an alias to /Subfolders
that indicates that the command should traverse all the subdirectories.
To support this, you need to provide a constructor on the attribute. Specifically, for the alias you need a constructor that takes a string argument. (Similarly, if you want to allow multiple aliases, you need to define an attribute that has a params string
array for a parameter.)
The only restriction on the constructor is that when applying an attribute to a construct, only literal values and types (such as typeof(int)
) are allowed as arguments. This is to enable their serialization into the resultant CIL. Therefore, it is not possible to call a static method when applying an attribute. In addition, providing a constructor that takes arguments of type System.DateTime
would be of little value, since there is no System.DateTime
literal.
Given the constructor call, the objects returned from PropertyInfo.GetCustomAttributes()
will be initialized with the specified constructor arguments, as demonstrated in Listing 17.14.
Furthermore, as Listing 17.15 and Listing 17.16 demonstrate, you can use similar code in a GetSwitches()
method on CommandLineAliasAttribute
that returns a dictionary collection of all the switches, including those from the property names, and associate each name with the corresponding attribute on the command-line object.
Most attributes are intended to decorate only particular constructs. For example, it makes no sense to allow CommandLineOptionAttribute
to decorate a class or an assembly. The attribute in those contexts would be meaningless. To avoid inappropriate use of an attribute, custom attributes can be decorated with System.AttributeUsageAttribute
. Listing 17.17 (for CommandLineOptionAttribute
) demonstrates how to do this.
If the attribute is used inappropriately, as it is in Listing 17.18, it will cause a compile-time error, as Output 17.5 demonstrates.
...Program+CommandLineInfo.cs(24,17): error CS0592: Attribute
'CommandLineSwitchAlias' is not valid on this declaration type. It is
valid on 'property, indexer' declarations only.
AttributeUsageAttribute
’s constructor takes an AttributesTargets
flag. This enum provides a list of all the possible targets that the runtime allows an attribute to decorate. For example, if you also allowed CommandLineSwitchAliasAttribute
on a field, you would update the AttributeUsageAttribute
class as shown in Listing 17.19.
In addition to restricting what an attribute can decorate, AttributeUsageAttribute
provides a mechanism for allowing duplicates of the same attribute on a single construct. The syntax appears in Listing 17.20.
The syntax is different from the constructor initialization syntax discussed earlier. The AllowMultiple
parameter is a named parameter, similar to the named parameter syntax used for optional method parameters (added in C# 4.0). Named parameters provide a mechanism for setting specific public properties and fields within the attribute constructor call, even though the constructor includes no corresponding parameters. The named attributes are optional designations, but they provide a means of setting additional instance data on the attribute without providing a constructor parameter for the purpose. In this case, AttributeUsageAttribute
includes a public member called AllowMultiple
. Therefore, you can set this member using a named parameter assignment when you use the attribute. Assigning named parameters must occur as the last portion of a constructor, following any explicitly declared constructor parameters.
Named parameters allow for assigning attribute data without providing constructors for every conceivable combination of which attribute properties are specified and which are not. Since many of an attribute’s properties may be optional, this is a useful construct in many cases.
The AttributeUsageAttribute
attribute has a special characteristic that you didn’t see in the custom attributes you have created thus far in this book. This attribute affects the behavior of the compiler, causing the compiler to sometimes report an error. Unlike the reflection code you wrote earlier for retrieving CommandLineRequiredAttribute
and CommandLineSwitchAliasAttribute
, AttributeUsageAttribute
has no runtime code; instead, it has built-in compiler support.
AttributeUsageAttribute
is a predefined attribute. Not only do such attributes provide additional metadata about the constructs they decorate, but also the runtime and compiler behave differently in order to facilitate these attributes’ functionality. Attributes such as AttributeUsageAttribute
, FlagsAttribute
, ObsoleteAttribute
, and ConditionalAttribute
are examples of predefined attributes. They include special behavior that only the CLI provider or compiler can offer because there are no extension points for additional noncustom attributes. In contrast, custom attributes are entirely passive. Listing 17.21 includes a couple of predefined attributes; Chapter 18 includes a few more.
Within a single assembly, the System.Diagnostics.ConditionalAttribute
attribute behaves a little like the #if
/#endif
preprocessor identifier. However, instead of eliminating the CIL code from the assembly, System.Diagnostics.ConditionalAttribute
will optionally cause the call to behave like a no-op, an instruction that does nothing. Listing 17.22 demonstrates the concept, and Output 17.7 shows the results.
Begin...
MethodA() executing...
End...
This example defined CONDITION_A
, so MethodA()
executed normally. CONDITION_B
, however, was not defined either through #define
or by using the csc.exe /Define
option. As a result, all calls to Program.MethodB()
from within this assembly will do nothing.
Functionally, ConditionalAttribute
is similar to placing an #if
/#endif
around the method invocation. The syntax is cleaner, however, because developers create the effect by adding the ConditionalAttribute
attribute to the target method without making any changes to the caller itself.
Note that the C# compiler notices the attribute on a called method during compilation, and assuming the preprocessor identifier exists, it eliminates any calls to the method. Note also that ConditionalAttibute
does not affect the compiled CIL code on the target method itself (besides the addition of the attribute metadata). Instead, it affects the call site during compilation by removing the calls. This further distinguishes ConditionalAttribute
from #if
/#endif
when calling across assemblies. Because the decorated method is still compiled and included in the target assembly, the determination of whether to call a method is based not on the preprocessor identifier in the callee’s assembly, but rather on the caller’s assembly. In other words, if you create a second assembly that defines CONDITION_B
, any calls to Program.MethodB()
from the second assembly will execute. This is a useful characteristic in many tracing and testing scenarios. In fact, calls to System.Diagnostics.Trace
and System.Diagnostics.Debug
use this trait with ConditionalAttribute
s on TRACE
and DEBUG
preprocessor identifiers.
Because methods don’t execute whenever the preprocessor identifier is not defined, ConditionalAttribute
may not be used on methods that include an out
parameter or specify a return other than void
. Doing so causes a compile-time error. This makes sense because possibly none of the code within the decorated method will execute, so it is unknown what to return to the caller. Similarly, properties cannot be decorated with ConditionalAttribute
. The AttributeUsage
(see the section titled System.AttributeUsageAttribute
, earlier in this chapter) for ConditionalAttribute
is AttributeTargets.Class
(starting in .NET 2.0) and AttributeTargets.Method
. This allows the attribute to be used on either a method or a class. However, the class usage is special because ConditionalAttribute
is allowed only on System.Attribute
-derived classes.
When ConditionalAttribute
decorates a custom attribute, a feature started in .NET 2.0, the latter can be retrieved via reflection only if the conditional string is defined in the calling assembly. Without such a conditional string, reflection that looks for the custom attribute will fail to find it.
As mentioned earlier, predefined attributes affect the compiler’s and/or the runtime’s behavior. ObsoleteAttribute
provides another example of attributes affecting the compiler’s behavior. The purpose of ObsoleteAttribute
is to help with the versioning of code, providing a means of indicating to callers that a particular member or type is no longer current. Listing 17.23 is an example of ObsoleteAttribute
usage. As Output 17.8 shows, any callers that compile code that invokes a member marked with ObsoleteAttribute
will cause a compile-time warning, optionally an error.
c:SampleCodeObsoleteAttributeTest.cs(24,17): warning CS0612:
Program.ObsoleteMethod()' is obsolete
In this case, ObsoleteAttribute
simply displays a warning. However, there are two additional constructors on the attribute. One of them, ObsoleteAttribute(string message)
, appends the additional message argument to the compiler’s obsolete message. The best practice for this message is to provide direction on what replaces the obsolete code. The second, however, is a bool error
parameter that forces the warning to be recorded as an error instead.
ObsoleteAttribute
allows third parties to notify developers of deprecated APIs. The warning (not an error) allows the original API to work until the developer is able to update the calling code.
Using predefined attributes, the framework supports the capacity to serialize objects onto a stream so that they can be deserialized back into objects at a later time. This provides a means of easily saving a document type object to disk before shutting down an application. Later on, the document may be deserialized so that the user can continue to work on it.
In spite of the fact that an object can be relatively complex and can include links to many other types of objects that also need to be serialized, the serialization framework is easy to use. In order for an object to be serializable, the only requirement is that it includes a System.SerializableAttribute
. Given the attribute, a formatter class reflects over the serializable object and copies it into a stream (see Listing 17.24).
Output 17.9 shows the results of Listing 17.24.
A cacophony of ramblings from my potpourri of notes
Listing 17.24 serializes and deserializes a Document
object. Serialization involves instantiating a formatter (this example uses System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
) and calling Serialization()
with the appropriate stream object. Deserializing the object simply involves a call to the formatter’s Deserialize()
method, specifying the stream that contains the serialized object as an argument. However, since the return from Deserialize()
is of type object
, you also need to cast it specifically to the type that was serialized.
Notice that serialization occurs for the entire object graph (all the items associated with the serialized object [Document
] via a field). Therefore, all fields in the object graph also must be serializable.
Fields that are not serializable should be decorated with the System.NonSerializable
attribute. This tells the serialization framework to ignore them. The same attribute should appear on fields that should not be persisted for use case reasons. Passwords and Windows handles are good examples of fields that should not be serialized: Windows handles because they change each time a window is re-created, and passwords because data serialized into a stream is not encrypted and can easily be accessed. Consider the Notepad view of the serialized document in Figure 17.2.
Listing 17.24 set the Title
field, and the resultant *.BIN
file includes the text in plain view.
One way to add encryption is to provide custom serialization. Ignoring the complexities of encrypting and decrypting, this requires implementing the ISerializable
interface in addition to using SerializableAttribute
. The interface requires only the GetObjectData()
method to be implemented. However, this is sufficient only for serialization. In order to also support deserialization, it is necessary to provide a constructor that takes parameters of type System.Runtime.Serialization.SerializationInfo
and System.Runtime.Serialization.StreamingContext
(see Listing 17.25).
Essentially, the System.Runtime.Serialization.SerializationInfo
object is a collection of name/value pairs. When serializing, the GetObject()
implementation calls AddValue()
. To reverse the process, you call one of the Get*()
members. In this case, you encrypt and decrypt prior to serialization and deserialization, respectively.
One more serialization point deserves mention: versioning. Objects such as documents may be serialized using one version of an assembly and deserialized using a newer version, sometimes the reverse. Without paying attention, however, version incompatibilities can easily be introduced, sometimes unexpectedly. Consider the scenario shown in Table 17.1.
Surprisingly, even though all you did was to add a new field, deserializing the original file throws a System.Runtime.Serialization.SerializationException
. This is because the formatter looks for data corresponding to the new field within the stream. Failure to locate such data throws an exception.
To avoid this, the 2.0 framework and later includes a System.Runtime.Serialization.OptionalFieldAttribute
. When you require backward compatibility, you must decorate serialized fields—even private ones—with OptionalFieldAttribute
(unless, of course, a latter version begins to require it).
Unfortunately, System.Runtime.Serialization.OptionalFieldAttribute
is not supported in the earlier framework version. Instead, it is necessary to implement ISerializable
, just as you did for encryption, saving and retrieving only the fields that are available. Assuming the addition of the Author
field, for example, the implementation shown in Listing 17.26 is required for backward-compatibility support prior to the 2.0 framework.
Serializing in GetObjectData()
simply involves serializing all fields (assume here that version 1 does not need to open documents from version 2). On deserialization, however, you can’t simply call GetString("Author")
because if no such entry exists, it will throw an exception. Instead, iterate through all the entries that are in info
and retrieve them individually.
The introduction of dynamic objects in C# 4.0 simplifies a host of programming scenarios and enables several new ones previously not available. At its core, programming with dynamic objects enables developers to code operations using a dynamic dispatch mechanism that the runtime will resolve at execution time, rather than the compiler verifying and binding to it at compile time.
Why? Because there are many times when objects are inherently not statically typed. Examples include loading data from an XML/CSV file, a database table, the Internet Explorer DOM, or COM’s IDispatch
interface, or calling code in a dynamic language such as an IronPython object. C# 4.0’s Dynamic object support provides a common solution for talking to runtime environments that don’t necessarily have a compile-time-defined structure. In the initial implementation of dynamic objects in C# 4.0, four binding methods are available:
1. Using reflection against an underlying CLR type
2. Invoking a custom IDynamicMetaObjectProvider
which makes available a DynamicMetaObject
3. Calling through the IUnknown
and IDispatch
interfaces of COM
4. Calling type defined by dynamic languages such as IronPython
Of these, we are going to delve into the first two. The principles found there translate seamlessly to the remaining cases—COM interoperability and dynamic language interoperability.
One of the key features of reflection is the ability to dynamically find and invoke a member on a particular type based on an execution time identification of the member name or some other quality, such as an attribute (see Listing 17.3). However, C# 4.0’s addition of dynamic objects provides a simpler way of invoking a member by reflection, assuming compile-time knowledge of the member signature. Again: The restriction is that at compile time we need to know the member name along with the signature (number of parameters and whether the specified parameters will be type-compatible with the signature). Listing 17.29 (with Output 17.10) provides an example.
Hello! My name is Inigo Montoya
140.6 makes for a long triathlon.
In this example, there is no explicit code for determining the object type, finding a particular MemberInfo
instance, and then invoking it. Instead, data
is declared as type dynamic
and methods are called against it directly. At compile time, there is no check as to whether the members specified are available, or even a check regarding what type underlies the dynamic
object. Hence, it is possible at compile time to make any call so long as the syntax is valid. At compile time, it is irrelevant whether there is a corresponding member or not.
However, type safety is not abandoned altogether. For standard CLR types (such as those used in Listing 17.29), the same type checker normally used at compile time for non-dynamic
types is instead invoked at execution time for the dynamic
type. Therefore, at execution time, if in fact no such member is available, the call will result in a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
.
Note again that this is not nearly as flexible as the reflection earlier in the chapter, although the API is undoubtedly simpler. The key difference when using a dynamic object is that it is necessary to identify the signature at compile time, rather than determine things such as the member name at runtime (like we did when parsing the command-line arguments).
Listing 17.29 and the accompanying text reveal several characteristics of the dynamic
data type.
• dynamic
is a directive to the compiler to generate code.
dynamic
involves an interception mechanism so that when a dynamic call is encountered by the runtime, it can compile the request to CIL and then invoke the newly compiled call. (See the Advanced Block titled dynamic
Uncovered, later in this chapter, for more details.)
The principle at work when a type is assigned to dynamic
is to conceptually “wrap” the original type so that no compile-time validation occurs. Additionally, when a member is invoked at runtime, the “wrapper” intercepts the call and dispatches it appropriately (or rejects it). Calling GetType()
on the dynamic
object reveals the type underlying the dynamic instance—it does not return dynamic
as a type.
• Any type that converts to object
will convert to dynamic
.
In Listing 17.29, we successfully cast both a value type (double
) and a reference type (string
) to dynamic
. In fact, all types can successfully be converted into a dynamic
object. There is an implicit conversion from any reference type to dynamic
. Similarly, there is an implicit conversion (a boxing conversion) from a value type to dynamic
. In addition, there is an implicit conversion from dynamic
to dynamic
. This is perhaps obvious, but with dynamic
this is more complicated than simply copying the “pointer” (address) from one location to the next.
• Successful conversion from dynamic
to an alternate type depends on support in the underlying type.
Conversion from a dynamic
object to a standard CLR type is an explicit cast (for example, (double)data.Length
). Not surprisingly, if the target type is a value type, an unboxing conversion is required. If the underlying type supports the conversion to the target type, the conversion from dynamic
will also succeed.
• The type underlying the dynamic type can change from one assignment to the next.
Unlike the implicitly typed variable (var
) which cannot be reassigned to a different type, dynamic
involves an interception mechanism for compilation before the underlying type’s code is executed. Therefore, it is possible to successfully swap out the underlying type instance to an entirely different type. This will result in another interception call site that will need to be compiled before invocation.
• Verification that the specified signature exists on the underlying type doesn’t occur until runtime—but it does occur.
As the method call to person.NonExistentMethodCallStillCompiles()
demonstrates, the compiler makes almost no verification of operations on a dynamic
type. This is left entirely to the work of the runtime when the code executes. And if the code never executes, even though surrounding code does (as in the case with person.NonExistentMethodCallStillCompiles()
), no verification and binding to the member will ever occur.
• The result of any dynamic
member invocation is of compile-time type dynamic
.
A call to any member on a dynamic
object will return a dynamic
object. Therefore, calls such as data.ToString()
will return a dynamic
object rather than the underlying string
type. However, at execution time, when GetType()
is called on the dynamic
object, an object representing the runtime type is returned.
• If the member specified does not exist at runtime, the runtime will throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
exception.
If an attempt to invoke a member at execution time does occur, the runtime will verify that in fact the member call is valid (that the signatures are type-compatible in the case of reflection, for example). If the method signatures are not compatible, the runtime will throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
.
• dynamic
with reflection does not support extension methods.
Just like with reflection using System.Type
, reflection using dynamic
does not support extension methods. Invocation of extension methods is still available on the implementing type (System.Linq.Enumerable
, for example), just not on the extended type directly.
• At its core, dynamic
is a System.Object
.
Given that any object will successfully convert to dynamic
and dynamic
may be explicitly converted to a different object type, dynamic
behaves like System.Object
. Like System.Object
, it even returns null
for its default value (default(dynamic)
), indicating it is a reference type. The special dynamic behavior of dynamic
that distinguishes it from a System.Object
appears only at compile time.
In addition to reflection, we can define custom types to invoke dynamically. Consider using dynamic invocation to retrieve the values of an XML element, for example. Rather than using the strongly typed syntax of Listing 17.30, using dynamic invocation we could call person.FirstName
and person.LastName
.
Although the code in Listing 17.30 is not overly complex, compare it to Listing 17.31—an alternative approach that uses a dynamically typed object.
The advantages are clear, but does that mean dynamic programming is preferable to static compilation?
In Listing 17.31, we have the same functionality as in Listing 17.30, but there is one very important difference. Listing 17.30 is entirely statically typed. This means that at compile time, all types and their member signatures are verified. Method names are required to match, and all parameters are checked for type compatibility. This is a key feature of C# and something we have highlighted throughout the book.
In contrast, Listing 17.31 has virtually no statically typed code; the variable person
is instead dynamic
. As a result, there is no compile-time verification that person
has a FirstName
or LastName
property, or any other members, for that matter. Furthermore, when coding within an IDE, there is no IntelliSense identifying any members on person
.
The loss of typing would seem to result in a significant decrease in functionality. Why is such a possibility even available in C#—a functionality that was added in C# 4.0, in fact? Let’s examine Listing 17.31 again. Notice the call to retrieve the "FirstName"
element: Element.Descendants("LastName").FirstOrDefault().Value
. The listing uses a string
("LastName"
) to identify the element name. However, there is no compile-time verification that the string is correct. If the casing was inconsistent with the element name or if there was a space, the compile would still succeed, even though a NullReferenceException
would occur with the call to the Value
property. Furthermore, the compiler makes no verification that the "FirstName"
element even exists, and if it doesn’t, we would also get the NullReferenceException
. In other words, in spite of all the type-safety advantages, type safety doesn’t offer much advantage to accessing the dynamic data stored within the XML element.
Listing 17.31 is no better than Listing 17.30 when it comes to compile-time verification of the element retrieval. If there is a case mismatch or if the FirstName
element didn’t exist, there would still be an exception.1 However, compare the call to access the first name in Listing 17.31 (person.FirstName
) with the call in Listing 17.30. The call in the latter listing is undoubtedly significantly simpler. In summary, there are situations where type safety doesn’t—and likely can’t—make certain checks. And in such cases, being able to make a dynamic call that is only runtime-verified rather than also compile-time-verified is significantly more readable and succinct. Obviously, if compile-time verification is possible, statically typed programming is preferred because readable and succinct APIs can accompany it. However, in the cases where it isn’t effective, C# 4.0 enables simpler code rather than the purity of type safety.
1. You cannot use a space in the FirstName
property call, but if XML supported spaces in element names, this would be a potential disadvantage, so let’s ignore this fact.
Listing 17.31 included a method call to DynamicXml.Parse(...)
that was essentially a factory method call for DynamicXml
—a custom type rather than one built into the CLR Framework. However, DynamicXml
doesn’t implement a FirstName
or LastName
property. To do so would break the dynamic support for retrieving data from the XML file at execution time, rather than compile-time-based implementation of the XML elements. In other words, DynamicXml
does not use reflection for accessing its members, but rather it dynamically binds to the values based on the XML content.
The key to defining a custom dynamic type is implementation of the System.Dynamic.IDynamicMetaObjectProvider
interface. Rather than implementing the interface from scratch, however, the preferred approach is to derive the custom dynamic type from System.Dynamic.DynamicObject
. This provides default implementation for a host of members and allows you to override the ones that don’t fit. Listing 17.32 shows the full implementation.
The key dynamic implementation methods for this use case are TryGetMember()
and TrySetMember()
(assuming you also want to assign the elements as well). Only these two method implementations are necessary to support the invocation of the dynamic getter and setter properties. Furthermore, the implementations are straightforward. First, they examine the contained XElement
, looking for an element with the same name as the binder.Name
—the name of the member invoked. If a corresponding XML element exists, the value is retrieved (or set). The return value is set to true
if the element exists and false
if it doesn’t. Automatically, a return value of false
will cause the runtime to throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
at the call site of the dynamic member invocation.
System.Dynamic.DynamicObject
supports additional virtual methods if additional dynamic invocations are required. Listing 17.33 shows the list of all the overridable members.
As Listing 17.33 shows, there are member implementations for everything—from casts and various operations, through to index invocations. In addition, there is a method for retrieving all the possible member names: GetDynamicMemberNames()
.
This chapter discussed how to use reflection to read the metadata that is compiled into the CIL. Using reflection, you saw how to provide a late binding in which the code to call is defined at execution time rather than at compile time. Although reflection is entirely feasible for deploying a dynamic system, it is considerably slower than statically linked (compile-time), defined code. This tends to make it more prevalent and useful in development tools.
Reflection also enables the retrieval of additional metadata decorating various constructs in the form of attributes. Typically, custom attributes are sought using reflection. It is possible to define your own custom attributes that insert additional metadata of your own choosing into the CIL. At runtime, it is then possible to retrieve this metadata and use it within the programming logic.
Many view attributes as a precursor to a concept known as aspect-oriented programming, in which you add functionality through constructs such as attributes instead of manually implementing the functionality wherever it is needed. It will take some time before you see true aspects within C# (if ever); however, attributes provide a clear steppingstone in that direction, without forcing a significant risk to the stability of the language.
Finally, the chapter included a C# 4.0 introduced feature—dynamic programming using the new type dynamic
. This section included a discussion of why static binding, although preferred when the API is strongly typed, has limitations when working with dynamic data.
The next chapter looks at multithreading, where attributes are used for synchronization.
18.224.34.205