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. 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)?
– 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.
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 process, using a Type
instance from DateTime
. Output 17.1 shows the results.
DateTime dateTime = new DateTime();
Type type = dateTime.GetType();
foreach (
System.Reflection.PropertyInfo property in
type.GetProperties())
{
Console.WriteLine(property.Name);
}
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()
.
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 7.3 used 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.
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 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 17.3 demonstrates this process.
using System;
using System.Diagnostics;
public partial class Program
{
public static void Main(string[] args)
{
string errorMessage;
CommandLineInfo commandLine = new CommandLineInfo();
if (!CommandLineHandler.TryParse(
args, commandLine, out errorMessage))
{
Console.WriteLine(errorMessage);
DisplayHelp();
}
if (commandLine.Help)
{
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");
}
}
using System;
using System.Diagnostics;
public partial class Program
{
private class CommandLineInfo
{
public bool Help { get; set; }
public string Out { get; set; }
public ProcessPriorityClass Priority { get; set; }
= ProcessPriorityClass.Normal;
}
}
using System;
using System.Diagnostics;
using System.Reflection;
public class CommandLineHandler
{
public static void Parse(string[] args, object commandLine)
{
string errorMessage;
if (!TryParse(args, commandLine, out errorMessage))
{
throw new ApplicationException(errorMessage);
}
}
public static bool TryParse(string[] args, object 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, true, null);
success = true;
}
else if (
property.PropertyType == typeof(string))
{
property.SetValue(
commandLine, optionParts[1], null);
success = true;
}
else if (property.PropertyType.IsEnum)
{
try
{
property.SetValue(commandLine,
Enum.Parse(
typeof(ProcessPriorityClass),
optionParts[1], true),
null);
success = true;
}
catch (ArgumentException )
{
success = false;
errorMessage =
errorMessage =
$@"The option '{
optionParts[1]
}' is invalid for '{
option }'";
}
}
else
{
success = false;
errorMessage =
$@"Data type '{
property.PropertyType.ToString()
}' on {
commandLine.GetType().ToString()
} is not supported."
}
}
else
{
success = false;
errorMessage =
$"Option '{ option }' is not supported.";
}
}
}
return success;
}
}
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 its 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
(albeit 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. 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.
Begin 2.0
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
, so 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.
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.
In the System.Type
class for the version 2.0 release of 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 17.5.
using System;
public class Program
{
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 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.
using System;
using System.Collections.Generic;
public partial class Program
{
public static void Main()
{
Stack<int> s = new Stack<int>();
Type t = s.GetType();
foreach(Type type in t.GetGenericArguments())
{
System.Console.WriteLine(
"Type parameter: " + type.FullName);
}
// ...
}
}
Type parameter: System.Int32
End 2.0
Begin 6.0
We briefly touched on the nameof
operator in Chapter 10, where it was used to provide the name of a parameter in an argument exception:
throw new ArgumentException(
"The argument did not represent a digit", nameof(textDigit));
Introduced in C# 6.0, this contextual keyword produces a constant string containing the unqualified name of whatever program element is specified as an argument. In this case, textDigit
is a parameter to the method, so nameof(textDigit)
returns “textDigit.” (Given that this activity happens at compile time, nameof
is not technically reflection. We include it here because ultimately it receives data about the assembly and it structure.)
One might ask what advantage is gained by using nameof(textDigit)
over simply "textDigit"
(especially given that the latter might even seem easier to use to some programmers). The advantages are twofold:
• The C# compiler ensures that the argument to the nameof
operator is, in fact, a valid program element. This helps prevent errors when a program element name is changed, helps prevent misspellings, and so on.
• IDE tools work better with the nameof
operator than with literal strings. For example, the “find all references” tool will find program elements mentioned in a nameof
expression, but not in a literal string. The automatic renaming refactoring also works better, and so on.
In the snippet given earlier, nameof(textDigit)
produces the name of a parameter. However, the nameof
operator works with any program element. For example, Listing 17.7 uses nameof
to pass the property name to INotifyPropertyChanged.PropertyChanged
.
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Person(string name)
{
Name = name;
}
private string _Name;
public string Name
{
get { return _Name; }
set
{
if (_Name != value)
{
_Name = value;
// Using C# 6.0 conditional null reference.
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(
nameof(Name)));
}
}
}
// ...
}
Notice that whether only the unqualified “Name” is provided (because it’s in scope) or the fully (or partially) qualified name like Person.Name
is used, the result is only the final identifier (the last element in a dotted name).
You can still use C# 5.0’s CallerMemberName
parameter attribute to obtain a property’s name; see http://itl.tc/CallerMemberName for an example.
End 6.0
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.8.
class CommandLineInfo
{
[CommandLineSwitchAlias("?")]
public bool Help { get; set; }
[CommandLineSwitchRequired]
public string Out { get; set; }
public System.Diagnostics.ProcessPriorityClass Priority
{ get; set; } =
System.Diagnostics.ProcessPriorityClass.Normal;
}
In Listing 17.8, 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 check that all required switches were specified.
There are two ways to combine attributes on the same construct. First, you can separate the attributes with commas within the same square brackets. Alternatively, you can place each attribute within its own square brackets. Listing 17.9 provides examples.
[CommandLineSwitchRequired]
[CommandLineSwitchAlias("FileName")]
public string Out { get; set; }
[CommandLineSwitchRequired,
CommandLineSwitchAlias("FileName")]
public string Out { get; set; }
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 cases, applying an attribute involves the same square bracket syntax shown in Listing 17.9. However, this syntax doesn’t work for return values, assemblies, and modules.
Assembly attributes are used to add 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.10 is an example of such a file.
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General information about an assembly is controlled
// through the following set of attributes. Change these
// attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("CompressionLibrary")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("IntelliTect")]
[assembly: AssemblyProduct("Compression Library")]
[assembly: AssemblyCopyright("Copyright© IntelliTect 2006-2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this
// assembly not visible to COM components. If you need to
// access a type in this assembly from COM, set the ComVisible
// attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib
// if this project is exposed to COM.
[assembly: Guid("417a9609-24ae-4323-b1d6-cef0f87a42c3")]
// Version information for an assembly consists
// of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can
// default the Revision and Build Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
The assembly
attributes define things such as the 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 must appear after the using
directive but before any namespace or class declarations. The attributes in Listing 17.10 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.11, appear before a method declaration but use the same type of syntax structure.
[return: Description(
"Returns true if the object is in a valid state.")]
public bool IsValid()
{
// ...
return true;
}
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, respectively. 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 calls for Attribute
to appear 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; rather, it appears only when defining one or using the attribute inline (such as typeof(DescriptionAttribute)
).
Guidelines
DO apply AssemblyVersionAttribute
to assemblies with public types.
CONSIDER applying the AssemblyFileVersionAttribute
and AssemblyCopyrightAttribute
to provide additional information about the assembly.
DO apply the following information assembly attributes: System.Reflection.AssemblyTitleAttribute
, System.Reflection.AssemblyCompanyAttribute
, System.Reflection.AssemblyProductAttribute
, System.Reflection.AssemblyDescriptionAttribute
, System.Reflection.AssemblyFileVersionAttribute
, and System.Reflection.AssemblyCopyrightAttribute
.
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.12.
public class CommandLineSwitchRequiredAttribute : Attribute
{
}
With that simple definition, you now can use the attribute as demonstrated in Listing 17.8. So far, no code responds to the attribute; therefore, the Out
property that includes the attribute will have no effect on command-line parsing.
Guidelines
DO name custom attribute classes with the suffix “Attribute”.
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.13 defines a method to return a list of required switches that are missing from the command line.
using System;
using System.Collections.Specialized;
using System.Reflection;
public class CommandLineSwitchRequiredAttribute : Attribute
{
public static string[] GetMissingRequiredOptions(
object commandLine)
{
List<string> missingOptions = new List<string>();
PropertyInfo[] properties =
commandLine.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
Attribute[] attributes =
(Attribute[])property.GetCustomAttributes(
typeof(CommandLineSwitchRequiredAttribute),
false);
if ((attributes.Length > 0) &&
(property.GetValue(commandLine, null) == null))
{
missingOptions.Add(property.Name);
}
}
return missingOptions.ToArray();
}
}
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, then indicate 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 can be cast to an Attribute
array. In our example, because 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.14 defines a CommandLineAliasAttribute
attribute—a custom attribute that 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 the command should traverse all the subdirectories.
To support this functionality, you need to provide a constructor for 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.)
public class CommandLineSwitchAliasAttribute : Attribute
{
public CommandLineSwitchAliasAttribute(string alias)
{
Alias = alias;
}
public string Alias { get; private set; }
}
class CommandLineInfo
{
[CommandLineSwitchAlias("?")]
public bool Help { get; set; }
// ...
}
When applying an attribute to a construct, only constant values and typeof()
expressions are allowed as arguments. This constraint is intended to enable their serialization into the resultant CIL. It implies that an attribute constructor should require parameters of the appropriate types; creating a constructor that takes arguments of type System.DateTime
would be of little value, as there are no System.DateTime
constants in C#.
The objects returned from PropertyInfo.GetCustomAttributes()
will be initialized with the specified constructor arguments, as demonstrated in Listing 17.15.
PropertyInfo property =
typeof(CommandLineInfo).GetProperty("Help");
CommandLineSwitchAliasAttribute attribute =
(CommandLineSwitchAliasAttribute)
property.GetCustomAttributes(
typeof(CommandLineSwitchAliasAttribute), false)[0];
if(attribute.Alias == "?")
{
Console.WriteLine("Help(?)");
};
Furthermore, as Listing 17.16 and Listing 17.17 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.
using System;
using System.Reflection;
using System.Collections.Generic;
public class CommandLineSwitchAliasAttribute : Attribute
{
public CommandLineSwitchAliasAttribute(string alias)
{
Alias = alias;
}
public string Alias { get; set; }
public static Dictionary<string, PropertyInfo> GetSwitches(
object commandLine)
{
PropertyInfo[] properties = null;
Dictionary<string, PropertyInfo> options =
new Dictionary<string, PropertyInfo>();
properties = commandLine.GetType().GetProperties(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance);
foreach (PropertyInfo property in properties)
{
options.Add(property.Name.ToLower(), property);
foreach (CommandLineSwitchAliasAttribute attribute in
property.GetCustomAttributes(
typeof(CommandLineSwitchAliasAttribute), false))
{
options.Add(attribute.Alias.ToLower(), property);
}
}
return options;
}
}
using System;
using System.Reflection;
using System.Collections.Generic;
public class CommandLineHandler
{
// ...
public static bool TryParse(
string[] args, object commandLine,
out string errorMessage)
{
bool success = false;
errorMessage = null;
Dictionary<string, PropertyInfo> options =
CommandLineSwitchAliasAttribute.GetSwitches(
commandLine);
foreach (string arg in args)
{
PropertyInfo property;
string option;
if (arg[0] == '/' || arg[0] == '-')
{
string[] optionParts = arg.Split(
new char[] { ':' }, 2);
option = optionParts[0].Remove(0, 1).ToLower();
if (options.TryGetValue(option, out property))
{
success = SetOption(
commandLine, property,
optionParts, ref errorMessage);
}
else
{
success = false;
errorMessage =
$"Option '{ option }' is not supported.";
}
}
}
return success;
}
private static bool SetOption(
object commandLine, PropertyInfo property,
string[] optionParts, ref string errorMessage)
{
bool success;
if (property.PropertyType == typeof(bool))
{
// Last parameters for handling indexers.
property.SetValue(
commandLine, true, null);
success = true;
}
else
{
if ((optionParts.Length < 2)
|| optionParts[1] == ""
|| optionParts[1] == ":")
{
// No setting was provided for the switch.
success = false;
errorMessage = string.Format(
"You must specify the value for the {0} option.",
property.Name);
}
else if (
property.PropertyType == typeof(string))
{
property.SetValue(
commandLine, optionParts[1], null);
success = true;
}
else if (property.PropertyType.IsEnum)
{
success = TryParseEnumSwitch(
commandLine, optionParts,
property, ref errorMessage);
}
else
{
success = false;
errorMessage = string.Format(
"Data type '{0}' on {1} is not supported.",
property.PropertyType.ToString(),
commandLine.GetType().ToString());
}
}
return success;
}
}
Guidelines
DO provide get-only properties (with private setters) on attributes with required property values.
DO provide constructor parameters to initialize properties on attributes with required properties. Each parameter should have the same name (albeit with different casing) as the corresponding property.
AVOID providing constructor parameters to initialize attribute properties corresponding to the optional arguments (and therefore, avoid overloading custom attribute constructors).
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.18 (for CommandLineOptionAttribute
) demonstrates how to do this.
[AttributeUsage(AttributeTargets.Property)]
public class CommandLineSwitchAliasAttribute : Attribute
{
// ...
}
If the attribute is used inappropriately, as it is in Listing 17.19, it will cause a compile-time error, as Output 17.5 demonstrates.
// ERROR: The attribute usage is restricted to properties
[CommandLineSwitchAlias("?")]
class CommandLineInfo
{
}
...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 AttributeTargets
flag. This enum provides a list of all 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.20.
// Restrict the attribute to properties and methods
[AttributeUsage(
AttributeTargets.Field | AttributeTargets.Property)]
public class CommandLineSwitchAliasAttribute : Attribute
{
// ...
}
Guidelines
DO apply the AttributeUsageAttribute
class to custom attributes.
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.21.
[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public class CommandLineSwitchAliasAttribute : Attribute
{
// ...
}
This 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. Given that 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 haven’t seen yet in the custom attributes you have created 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 the runtime and compiler also behave differently to facilitate these attributes’ functionality. Attributes such as AttributeUsageAttribute
, FlagsAttribute
, ObsoleteAttribute
, and ConditionalAttribute
are examples of predefined attributes. They implement 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.22 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.23 demonstrates the concept, and Output 17.7 shows the results.
#define CONDITION_A
using System;
using System.Diagnostics;
public class Program
{
public static void Main()
{
Console.WriteLine("Begin...");
MethodA();
MethodB();
Console.WriteLine("End...");
}
[Conditional("CONDITION_A")]
static void MethodA()
{
Console.WriteLine("MethodA() executing...");
}
[Conditional("CONDITION_B")]
static void MethodB()
{
Console.WriteLine("MethodB() executing...");
}
}
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.
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. ConditionalAttibute
, however, 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 potentially 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 Framework 2.0) and AttributeTargets.Method
, which 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 Framework 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. Its purpose 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.24 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.
class Program
{
public static void Main()
{
ObsoleteMethod();
}
[Obsolete]
public static void ObsoleteMethod()
{
}
}
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 constructor 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 continue 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. For an object to be serializable, the only requirement is that it include a System.SerializableAttribute
. Given the attribute, a formatter class reflects over the serializable object and copies it into a stream (see Listing 17.25).
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
class Program
{
public static void Main()
{
Stream stream;
Document documentBefore = new Document();
documentBefore.Title =
"A cacophony of ramblings from my potpourri of notes";
Document documentAfter;
using (stream = File.Open(
documentBefore.Title + ".bin", FileMode.Create))
{
BinaryFormatter formatter =
new BinaryFormatter();
formatter.Serialize(stream, documentBefore);
}
using (stream = File.Open(
documentBefore.Title + ".bin", FileMode.Open))
{
BinaryFormatter formatter =
new BinaryFormatter();
documentAfter = (Document)formatter.Deserialize(
stream);
}
Console.WriteLine(documentAfter.Title);
}
}
// Serializable classes use SerializableAttribute.
[Serializable]
class Document
{
public string Title = null;
public string Data = null;
[NonSerialized]
public long _WindowHandle = 0;
class Image
{
}
[NonSerialized]
private Image Picture = new Image();
}
Output 17.9 shows the results of Listing 17.25.
A cacophony of ramblings from my potpourri of notes
Listing 17.25 serializes and deserializes a Document
object. Serialization involves instantiating a formatter (System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
, in this example) and calling Serialization()
with the appropriate stream object. Deserializing the object simply involves calling the formatter’s Deserialize()
method, specifying the stream that contains the serialized object as an argument. However, given that 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 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, which 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 be readily accessed. Consider the Notepad view of the serialized document in Figure 17.2.
Listing 17.25 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. To support deserialization as well, it is necessary to provide a constructor that takes parameters of type System.Runtime.Serialization.SerializationInfo
and System.Runtime.Serialization.StreamingContext
(see Listing 17.26).
using System;
using System.Runtime.Serialization;
[Serializable]
class EncryptableDocument :
ISerializable
{
public EncryptableDocument(){ }
enum Field
{
Title,
Data
}
public string Title;
public string Data;
public static string Encrypt(string data)
{
string encryptedData = data;
// Key-based encryption ...
return encryptedData;
}
public static string Decrypt(string encryptedData)
{
string data = encryptedData;
// Key-based decryption...
return data;
}
#region ISerializable Members
public void GetObjectData(
SerializationInfo info, StreamingContext context)
{
info.AddValue(
Field.Title.ToString(), Title);
info.AddValue(
Field.Data.ToString(), Encrypt(Data));
}
public EncryptableDocument(
SerializationInfo info, StreamingContext context)
{
Title = info.GetString(
Field.Title.ToString());
Data = Decrypt(info.GetString(
Field.Data.ToString()));
}
#endregion
}
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; the reverse may also occur. If the programmer is not paying sufficient attention, however, version incompatibilities can easily be introduced in this process, 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.
Begin 2.0
To avoid this problem, .Net Framework 2.0 and later include a System.Runtime.Serialization.OptionalFieldAttribute
. When backward compatibility is required, you must decorate serialized fields—even private ones—with OptionalFieldAttribute
(unless, of course, a latter version begins to require it).
End 2.0
Begin 4.0
The introduction of dynamic objects in C# 4.0 simplified a host of programming scenarios and enabled 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? Many times, 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
that makes available a DynamicMetaObject
3. Calling through the IUnknown
and IDispatch
interfaces of COM
4. Calling a type defined by dynamic languages such as IronPython
Of these four approaches, we will delve into the first two. The principles underlying them 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. To reiterate, this restriction states that at compile time we need to know the member name along with the signature (the number of parameters and whether the specified parameters will be type-compatible with the signature). Listing 17.30 (with Output 17.10) provides an example.
using System;
// ...
dynamic data =
"Hello! My name is Inigo Montoya";
Console.WriteLine(data);
data = (double)data.Length;
data = data * 3.5 + 28.6;
if(data == 2.4 + 112 + 26.2)
{
Console.WriteLine(
$"{ data } makes for a long triathlon.");
}
else
{
data.NonExistentMethodCallStillCompiles();
}
// ...
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 which 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 really a corresponding member.
However, type safety is not abandoned altogether. For standard CLR types (such as those used in Listing 17.30), 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 no such member is available, the call will result in a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
.
Note that this capability is not nearly as flexible as the reflection described 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 (as we did when parsing the command-line arguments).
Listing 17.30 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 Topic 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 process is more complicated than simply copying the “pointer” (address) from one location to the next.
• Successful conversion from dynamic
to an alternative 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 an 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 step is left entirely to the work of the runtime when the code executes. Moreover, if the code never executes, even though surrounding code does (as 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 the member call is truly 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 can be successfully converted to dynamic
, and that 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 that we invoke dynamically. You might consider using dynamic invocation to retrieve the values of an XML element, for example. Rather than using the strongly typed syntax of Listing 17.31, using dynamic invocation we could call person.FirstName
and person.LastName
.
using System;
using System.Xml.Linq;
// ...
XElement person = XElement.Parse(
@"<Person>
<FirstName>Inigo</FirstName>
<LastName>Montoya</LastName>
</Person>");
Console.WriteLine("{0} {1}",
person.Descendants("FirstName").FirstOrDefault().Value,
person.Descendants("LastName").FirstOrDefault().Value);
// ...
Although the code in Listing 17.31 is not overly complex, compare it to Listing 17.32—an alternative approach that uses a dynamically typed object.
using System;
// ...
dynamic person = DynamicXml.Parse(
@"<Person>
<FirstName>Inigo</FirstName>
<LastName>Montoya</LastName>
</Person>");
Console.WriteLine(
$"{ person.FirstName } { person.LastName }");
// ...
The advantages are clear, but does that mean dynamic programming is preferable to static compilation?
In Listing 17.32, we have the same functionality as in Listing 17.31, albeit with one very important difference: Listing 17.31 is entirely statically typed. Thus, at compile time, all types and their member signatures are verified with this approach. 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.32 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, then, is such a possibility even available in C#—a functionality that was added in C# 4.0, in fact?
To understand this apparent paradox, let’s reexamine Listing 17.32. Notice the call to retrieve the "FirstName"
element: Element.Descendants("LastName").FirstOrDefault().Value
. The listing uses a string
("LastName"
) to identify the element name, but 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 does not attempt to verify that the "FirstName"
element even exists; if it doesn’t, we would also get the NullReferenceException
message. In other words, in spite of all the type-safety advantages, type safety doesn’t offer many benefits when you’re accessing the dynamic data stored within the XML element.
Listing 17.32 is no better than Listing 17.31 when it comes to compile-time verification of the element retrieval. If a case mismatch occurs 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.32 (person.FirstName
) with the call in Listing 17.31. The call in the latter listing is undoubtedly significantly simpler.
1. You cannot use a space in the FirstName
property call, but neither does XML support spaces in element names, so let’s ignore this fact.
In summary, there are situations in which type safety doesn’t—and likely can’t—make certain checks. In such cases, code that makes a dynamic call that is verified only at runtime, rather than also being verified at compile time, 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 programmers to write simpler code rather than emphasizing the purity of type safety.
Listing 17.32 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 fostering compile-time-based implementation of the XML elements. In other words, DynamicXml
does not use reflection for accessing its members, but rather 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 implementations for a host of members and allows you to override the ones that don’t fit. Listing 17.33 shows the full implementation.
using System;
using System.Dynamic;
using System.Xml.Linq;
public class DynamicXml : DynamicObject
{
private XElement Element { get; set; }
public DynamicXml(System.Xml.Linq.XElement element)
{
Element = element;
}
public static DynamicXml Parse(string text)
{
return new DynamicXml(XElement.Parse(text));
}
public override bool TryGetMember(
GetMemberBinder binder, out object result)
{
bool success = false;
result = null;
XElement firstDescendant =
Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant != null)
{
if (firstDescendant.Descendants().Count() > 0)
{
result = new DynamicXml(firstDescendant);
}
else
{
result = firstDescendant.Value;
}
success = true;
}
return success;
}
public override bool TrySetMember(
SetMemberBinder binder, object value)
{
bool success = false;
XElement firstDescendant =
Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant != null)
{
if (value.GetType() == typeof(XElement))
{
firstDescendant.ReplaceWith(value);
}
else
{
firstDescendant.Value = value.ToString();
}
success = true;
}
return success;
}
}
The key dynamic implementation methods for this use case are TryGetMember()
and TrySetMember()
(assuming you 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. A return value of false
will immediately 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 more dynamic invocations are required. Listing 17.34 produces a list of all overridable members.
using System.Dynamic;
public class DynamicObject : IDynamicMetaObjectProvider
{
protected DynamicObject();
public virtual IEnumerable<string> GetDynamicMemberNames();
public virtual DynamicMetaObject GetMetaObject(
Expression parameter);
public virtual bool TryBinaryOperation(
BinaryOperationBinder binder, object arg,
out object result);
public virtual bool TryConvert(
ConvertBinder binder, out object result);
public virtual bool TryCreateInstance(
CreateInstanceBinder binder, object[] args,
out object result);
public virtual bool TryDeleteIndex(
DeleteIndexBinder binder, object[] indexes);
public virtual bool TryDeleteMember(
DeleteMemberBinder binder);
public virtual bool TryGetIndex(
GetIndexBinder binder, object[] indexes,
out object result);
public virtual bool TryGetMember(
GetMemberBinder binder, out object result);
public virtual bool TryInvoke(
InvokeBinder binder, object[] args, out object result);
public virtual bool TryInvokeMember(
InvokeMemberBinder binder, object[] args,
out object result);
public virtual bool TrySetIndex(
SetIndexBinder binder, object[] indexes, object value);
public virtual bool TrySetMember(
SetMemberBinder binder, object value);
public virtual bool TryUnaryOperation(
UnaryOperationBinder binder, out object result);
}
As Listing 17.34 shows, there are member implementations for everything—from casts and various operations, to index invocations. In addition, there is a method for retrieving all the possible member names: GetDynamicMemberNames()
.
End 4.0
This chapter discussed how to use reflection to read the metadata that is compiled into the CIL. Using reflection, it is possible 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 executes considerably more slowly than statically linked (compile-time), defined code. This tends to make it more prevalent and useful in development tools when performance is potentially not as critical.
Reflection also enables the retrieval of additional metadata decorating various constructs in the form of attributes. Typically, custom attributes are sought using reflection. You can define your own custom attributes that insert additional metadata of your own choosing into the CIL. At runtime, you can then retrieve this metadata and use it within the programming logic.
Many programmers 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 creating a significant risk to the stability of the language.
Finally, this chapter included a feature introduced in C# 4.0—dynamic programming using the new type dynamic
. This coverage 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.