Pro C#10 CHAPTER 17 Type Reflection, Late Binding, Attribute, and Dynamic Types

CHAPTER 17

Type Reflection, Late Binding, Attribute, and Dynamic Types

As shown in Chapter 16, assemblies are the basic unit of deployment in the .NET universe. Using the integrated Object Browser of Visual Studio (and numerous other IDEs), you can examine the types within a project’s referenced set of assemblies. Furthermore, external tools such as ildasm.exe allow you to peek into the underlying CIL code, type metadata, and assembly manifest for a given .NET binary. In addition to this design-time investigation of .NET assemblies, you are also able to programmatically obtain this same information using the System.Reflection namespace. To this end, the first task of this chapter is to define the role of reflection and the necessity of .NET metadata.
The next sections of the chapter examine several closely related topics, which hinge upon reflection services. For example, you will learn how a .NET client may employ dynamic loading and late binding to activate types it has no compile-time knowledge of. You will also learn how to insert custom metadata into your .NET assemblies using system-supplied and custom attributes. To put all of these (seemingly esoteric) topics into perspective, the chapter closes by demonstrating how to build several “snap-in objects” that you can plug into an extendable console application.
In this chapter, you will also be introduced to the C# dynamic keyword and understand how loosely typed calls are mapped to the correct in-memory object using the Dynamic Language Runtime (DLR). After you understand the services provided by the DLR, you will see examples of using dynamic types to streamline how you can perform late-bound method calls (via reflection services) and to easily communicate with legacy COM libraries.

■ Note Don’t confuse the C# dynamic keyword with the concept of a dynamic assembly (see Chapter 18). While you could use the dynamic keyword when building a dynamic assembly, these are ultimately two independent concepts.

The Necessity of Type Metadata
The ability to fully describe types (classes, interfaces, structures, enumerations, and delegates) using metadata is a key element of the .NET platform. Many .NET technologies, such as object serialization, require the ability to discover the format of types at runtime. Furthermore, cross-language interoperability, numerous compiler services, and an IDE’s IntelliSense capabilities all rely on a concrete description of type.

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_17

661

Recall that the ildasm.exe utility allows you to view an assembly’s type metadata. In the generated CarLibrary.il file (from Chapter 16), navigate to the METAINFO section to see all the CarLibrary’s metadata. A small snippet is included here:

// ==== M E T A I N F O ===

// ===========================================================
// ScopeName : CarLibrary.dll
// MVID : {DF92DBD2-2C47-4CF8-B25E-7319F1351625}
// ===========================================================
// Global functions
//
//
// Global fields
//
//
// Global MemberRefs
//
//
// TypeDef #1
//
// TypDefName: CarLibrary.Car
// Flags : [Public] [AutoLayout] [Class] [Abstract] [AnsiClass] [BeforeFieldInit]
// Extends : [TypeRef] System.Object
// Field #1
//
// Field Name: k BackingField
// Flags : [Private]
// CallCnvntn : [FIELD]
// Field type : String

As you can see, the .NET type metadata is verbose (the actual binary format is much more compact). In fact, if I were to list the entire metadata description representing the CarLibrary.dll assembly, it would span several pages. Given that this act would be a woeful waste of paper, let’s just glimpse into some key metadata descriptions of the CarLibrary.dll assembly.

■ Note Don’t be too concerned with the exact syntax of every piece of .NET metadata in the next few sections. The bigger point to absorb is that .NET metadata is very descriptive and lists each internally defined (and externally referenced) type found within a given code base.

Viewing (Partial) Metadata for the EngineStateEnum Enumeration
Each type defined within the current assembly is documented using a TypeDef #n token (where TypeDef is short for type definition). If the type being described uses a type defined within a separate .NET assembly, the referenced type is documented using a TypeRef #n token (where TypeRef is short for type reference).
A TypeRef token is a pointer (if you will) to the referenced type’s full metadata definition in an external assembly. In a nutshell, .NET metadata is a set of tables that clearly mark all type definitions (TypeDefs) and referenced types (TypeRefs), all of which can be examined using ildasm.exe.

As far as CarLibrary.dll goes, one TypeDef is the metadata description of the CarLibrary.
EngineStateEnum enumeration (your number may differ; TypeDef numbering is based on the order in which the C# compiler processes the file).

// TypeDef #2
//
// TypDefName: CarLibrary.EngineStateEnum
// Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
// Extends : [TypeRef] System.Enum
// Field #1
//
// Field Name: value
// Flags : [Public] [SpecialName] [RTSpecialName]
// CallCnvntn: [FIELD]
// Field type: I4
//
// Field #2
//
// Field Name: EngineAlive
// Flags : [Public] [Static] [Literal] [HasDefault]
// DefltValue: (I4) 0
// CallCnvntn: [FIELD]
// Field type: ValueClass CarLibrary.EngineStateEnum
//
// Field #3 (04000007)
//
// Field Name: EngineDead (04000007)
// Flags : [Public] [Static] [Literal] [HasDefault] (00008056)
// DefltValue: (I4) 1
// CallCnvntn: [FIELD]
// Field type: ValueClass CarLibrary.EngineStateEnum

Here, the TypDefName token is used to establish the name of the given type, which in this case is the custom CarLibrary.EngineStateEnum enum. The Extends metadata token is used to document the base type of a given .NET type (in this case, the referenced type, System.Enum). Each field of an enumeration is marked using the Field #n token.

■ Note While they look like typos, TypDefName does not have the e and DefltValue does not have the au
one would expect.

Viewing (Partial) Metadata for the Car Type
Here is a partial dump of the Car class that illustrates the following:
• How fields are defined in terms of .NET metadata
• How methods are documented via .NET metadata
• How an automatic property is represented in .NET metadata

// TypeDef #1
//
// TypDefName: CarLibrary.Car
// Flags : [Public] [AutoLayout] [Class] [Abstract] [AnsiClass] [BeforeFieldInit]
// Extends : [TypeRef] System.Object
// Field #1
//
// Field Name: k BackingField
// Flags : [Private]
// CallCnvntn: [FIELD]
// Field type: String

// Method #1
//
// MethodName: get_PetName
// Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName]
// RVA : 0x00002050
// ImplFlags : [IL] [Managed]
// CallCnvntn: [DEFAULT]
// hasThis
// ReturnType: String
// No arguments.

// Method #2
//
// MethodName: set_PetName
// Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName]
// RVA : 0x00002058
// ImplFlags : [IL] [Managed]
// CallCnvntn: [DEFAULT]
// hasThis
// ReturnType: Void
// 1 Arguments
// Argument #1: String
// 1 Parameters
// (1) ParamToken : Name : value flags: [none]

// Property #1
//
// Prop.Name : PetName
// Flags : [none]
// CallCnvntn: [PROPERTY]
// hasThis
// ReturnType: String
// No arguments.
// DefltValue:
// Setter : set_PetName
// Getter : get_PetName
// 0 Others

First, note that the Car class metadata marks the type’s base class (System.Object) and includes various flags that describe how this type was constructed (e.g., [Public], [Abstract], and whatnot). Methods (such as the Car’s constructor) are described by their parameters, return value, and name.
Note how an automatic property results in a compiler-generated private backing field (which was named k BackingField) and two compiler-generated methods (in the case of a read-write property) named, in this example, get_PetName() and set_PetName(). Finally, the actual property is mapped to the internal get/set methods using the .NET metadata Getter/Setter tokens.

Examining a TypeRef
Recall that an assembly’s metadata will describe not only the set of internal types (Car, EngineStateEnum, etc.) but also any external types the internal types reference. For example, given that CarLibrary.dll has defined two enumerations, you find a TypeRef block for the System.Enum type, as follows:

// TypeRef #19
//
// Token: 0x01000013
// ResolutionScope: 0x23000001
// TypeRefName: System.Enum

Documenting the Defining Assembly
The CarLibrary.il file also allows you to view the .NET metadata that describes the assembly itself using the Assembly token. The following is a partial dump of the manifest of CarLibrary.dll. Note that the version matches the assembly version set for the library.

// Assembly
//
// Token: 0x20000001
// Name : CarLibrary
// Public Key :
// Hash Algorithm : 0x00008004
// Version: 1.0.0.1
// Major Version: 0x00000001
// Minor Version: 0x00000000
// Build Number: 0x00000000
// Revision Number: 0x00000001
// Locale:
// Flags : [none] (00000000)

Documenting Referenced Assemblies
In addition to the Assembly token and the set of TypeDef and TypeRef blocks, .NET metadata also makes use of AssemblyRef #n tokens to document each external assembly. Given that each .NET assembly references the System.Runtime base class library assembly, you will find an AssemblyRef for the System.Runtime assembly, as shown in the following code:

// AssemblyRef #1
//
// Token: 0x23000001
// Public Key or Token: b0 3f 5f 7f 11 d5 0a 3a
// Name: System.Runtime
// Version: 6.0.0.0
// Major Version: 0x00000006
// Minor Version: 0x00000000
// Build Number: 0x00000000
// Revision Number: 0x00000000
// Locale:
// HashValue Blob:
// Flags: [none] (00000000)

Documenting String Literals
The final point of interest regarding .NET metadata is the fact that every string literal in your code base is documented under the User Strings token.

// User Strings
//
// 70000001 : (23) L"CarLibrary Version 2.0!"
// 70000031 : (13) L"Quiet time…"
// 7000004d : (11) L"Jamming {0}"
// 70000065 : (32) L"Eek! Your engine block exploded!"
// 700000a7 : (34) L"Ramming speed! Faster is better…"

■ Note as illustrated in the previous metadata listing, always be aware that all strings are clearly documented in the assembly metadata. This could have huge security consequences if you were to use string literals to represent passwords, credit card numbers, or other sensitive information.

The next question on your mind may be (in the best-case scenario) “How can I leverage this information in my applications?” or (in the worst-case scenario) “Why should I care about metadata?” To address both points of view, allow me to introduce .NET reflection services. Be aware that the usefulness of the topics presented over the pages that follow may be a bit of a head-scratcher until this chapter’s endgame. So, hang tight.

■ Note you will also find a number of CustomAttribute tokens displayed by the METAINFO section, which documents the attributes applied within the code base. you will learn about the role of .NET attributes later in this chapter.

Understanding Reflection
In the .NET universe, reflection is the process of runtime type discovery. Using reflection services, you can programmatically obtain the same metadata information generated by ildasm.exe using a friendly object model. For example, through reflection, you can obtain a list of all types contained within a given .dll or
.exe assembly, including the methods, fields, properties, and events defined by a given type. You can also dynamically discover the set of interfaces supported by a given type, the parameters of a method, and other related details (base classes, namespace information, manifest data, etc.).
Like any namespace, System.Reflection (which is defined in System.Runtime.dll) contains several related types. Table 17-1 lists some of the core items you should be familiar with.

Table 17-1. A Sampling of Members of the System.Reflection Namespace

Type Meaning in Life
Assembly This abstract class contains members that allow you to load, investigate, and manipulate an assembly.
AssemblyName This class allows you to discover numerous details behind an assembly’s identity (version information, culture information, etc.).
EventInfo This abstract class holds information for a given event.
FieldInfo This abstract class holds information for a given field.
MemberInfo This is the abstract base class that defines common behaviors for the EventInfo, FieldInfo, MethodInfo, and PropertyInfo types.
MethodInfo This abstract class contains information for a given method.
Module This abstract class allows you to access a given module within a multifile assembly.
ParameterInfo This class holds information for a given parameter.
PropertyInfo This abstract class holds information for a given property.

To understand how to leverage the System.Reflection namespace to programmatically read .NET metadata, you need to first come to terms with the System.Type class.

The System.Type Class
The System.Type class defines members that can be used to examine a type’s metadata, a great number of which return types from the System.Reflection namespace. For example, Type.GetMethods() returns an array of MethodInfo objects, Type.GetFields() returns an array of FieldInfo objects, and so on. The complete set of members exposed by System.Type is quite expansive; however, Table 17-2 offers a partial snapshot of the members supported by System.Type (see the .NET documentation for full details).

Table 17-2. Select Members of System.Type

Member Meaning in Life
IsAbstract IsArray IsClass IsCOMObject IsEnum
IsGenericTypeDefinition IsGenericParameter IsInterface
IsPrimitive IsNestedPrivate IsNestedPublic IsSealed IsValueType These properties (among others) allow you to discover a number of basic traits about the Type you are referring to (e.g., if it is an abstract entity, an array, a nested class, etc.).
GetConstructors() GetEvents() GetFields() GetInterfaces() GetMembers() GetMethods() GetNestedTypes() GetProperties() These methods (among others) allow you to obtain an array representing the items (interface, method, property, etc.) you are interested in. Each method returns a related array (e.g., GetFields() returns a FieldInfo array, GetMethods() returns a MethodInfo array, etc.). Be aware that each of these methods has a singular form (e.g., GetMethod(), GetProperty(), etc.) that allows you to retrieve a specific item by name, rather than an array of all related items.
FindMembers() This method returns a MemberInfo array based on search criteria.
GetType() This static method returns a Type instance given a string name.
InvokeMember() This method allows “late binding” for a given item. You’ll learn about late binding later in this chapter.

Obtaining a Type Reference Using System.Object.GetType()
You can obtain an instance of the Type class in a variety of ways. However, the one thing you cannot do is directly create a Type object using the new keyword, as Type is an abstract class. Recall that System.
Object defines a method named GetType(), which returns an instance of the Type class that represents the metadata for the current object.

// Obtain type information using a SportsCar instance. SportsCar sc = new SportsCar();
Type t = sc.GetType();

Obviously, this approach will work only if you have compile-time knowledge of the type you want to reflect over (SportsCar in this case) and currently have an instance of the type in memory. Given this
restriction, it should make sense that tools such as ildasm.exe do not obtain type information by directly calling System.Object.GetType() for each type since that ildasm.exe was not compiled against your custom assemblies.

Obtaining a Type Reference Using typeof()
The next way to obtain type information is using the C# typeof operator, like so:

// Get the type using typeof. Type t = typeof(SportsCar);

Unlike System.Object.GetType(), the typeof operator is helpful, in that you do not need to first create an object instance to extract type information. However, your code base must still have compile- time knowledge of the type you are interested in examining, as typeof expects the strongly typed name of the type.

Obtaining a Type Reference Using System.Type.GetType()
To obtain type information in a more flexible manner, you may call the static GetType() member of the System.Type class and specify the fully qualified string name of the type you are interested in examining. Using this approach, you do not need to have compile-time knowledge of the type you are extracting metadata from, given that Type.GetType() takes an instance of the omnipresent System.String.

■ Note When i say you do not need compile-time knowledge when calling Type.GetType(), i am referring to the fact that this method can take any string value whatsoever (rather than a strongly typed variable). of course, you would still need to know the name of the type in a “stringified” format!

The Type.GetType() method has been overloaded to allow you to specify two Boolean parameters, one of which controls whether an exception should be thrown if the type cannot be found, and the other of which establishes the case sensitivity of the string. To illustrate, ponder the following:

// Obtain type information using the static Type.GetType() method
// (don’t throw an exception if SportsCar cannot be found and ignore case).
Type t = Type.GetType("CarLibrary.SportsCar", false, true);

In the previous example, notice that the string you are passing into GetType() makes no mention of the assembly containing the type. In this case, the assumption is that the type is defined within the currently executing assembly. However, when you want to obtain metadata for a type within an external assembly, the string parameter is formatted using the type’s fully qualified name, followed by a comma, followed by the friendly name (the assembly name without any version information) of the assembly containing the type, like so:

// Obtain type information for a type within an external assembly. Type t = Type.GetType("CarLibrary.SportsCar, CarLibrary");

As well, do know that the string passed into Type.GetType() may specify a plus token (+) to denote a nested type. Assume you want to obtain type information for an enumeration (SpyOptions) nested within a class named JamesBondCar. To do so, you would write the following:

// Obtain type information for a nested enumeration
// within the current assembly.
Type t = Type.GetType("CarLibrary.JamesBondCar+SpyOptions");

Building a Custom Metadata Viewer
To illustrate the basic process of reflection (and the usefulness of System.Type), let’s create a Console Application project named MyTypeViewer. This program will display details of the methods, properties, fields, and supported interfaces (in addition to some other points of interest) for any type within System. Runtime.dll (recall all .NET applications have automatic access to this framework class library) or a type within MyTypeViewer itself. Once the application has been created, be sure to import the System. Reflection namespace.

// Need to import this namespace to do any reflection! using System.Reflection;

Reflecting on Methods
Several static methods will be added to the Program.cs file, each of which takes a single System.Type parameter and returns void. First you have ListMethods(), which (as you might guess) prints the name of each method defined by the incoming type. Notice how Type.GetMethods() returns an array of System. Reflection.MethodInfo objects, which can be enumerated with a standard foreach loop, as follows:

// Display method names of type. static void ListMethods(Type t)
{
Console.WriteLine(" Methods "); MethodInfo[] mi = t.GetMethods(); foreach(MethodInfo m in mi)
{
Console.WriteLine("->{0}", m.Name);
}
Console.WriteLine();
}

Here, you are simply printing the name of the method using the MethodInfo.Name property. As you might guess, MethodInfo has many additional members that allow you to determine whether the method is static, virtual, generic, or abstract. As well, the MethodInfo type allows you to obtain the method’s return value and parameter set. You’ll spruce up the implementation of ListMethods() in just a bit.
If you wanted, you could also build a fitting LINQ query to enumerate the names of each method.
Recall from Chapter 13, LINQ to Objects allows you to build strongly typed queries that can be applied to in-memory object collections. As a good rule of thumb, whenever you find blocks of looping or decision programming logic, you could make use of a related LINQ query. For example, you could rewrite the previous method with LINQ like this:

static void ListMethods(Type t)
{
Console.WriteLine(" Methods ");
var methodNames = from n in t.GetMethods() orderby n.Name select n.Name;
// Using LINQ extensions:
// var methodNames = t.GetMethods().OrderBy(m=>m.Name).Select(m=>m.Name); foreach (var name in methodNames)
{

Console.WriteLine("->{0}", name);
}
Console.WriteLine();
}

Reflecting on Fields and Properties
The implementation of ListFields() is similar. The only notable difference is the call to Type.GetFields() and the resulting FieldInfo array. Again, to keep things simple, you are printing out only the name of each field using a LINQ query.

// Display field names of type. static void ListFields(Type t)
{
Console.WriteLine(" Fields ");
// var fieldNames = from f in t.GetFields() orderby f.Name select f.Name; var fieldNames = t.GetFields().OrderBy(m=>m.Name).Select(x=>x.Name);
foreach (var name in fieldNames)
{
Console.WriteLine("->{0}", name);
}
Console.WriteLine();
}

The logic to display a type’s properties is similar.

// Display property names of type. static void ListProps(Type t)
{
Console.WriteLine(" Properties ");
var propNames = from p in t.GetProperties() orderby p.Name select p.Name;
//var propNames = t.GetProperties().Select(p=>p.Name); foreach (var name in propNames)
{
Console.WriteLine("->{0}", name);
}
Console.WriteLine();
}

Reflecting on Implemented Interfaces
Next, you will author a method named ListInterfaces() that will print the names of any interfaces supported on the incoming type. The only point of interest here is that the call to GetInterfaces() returns an array of System.Types! This should make sense given that interfaces are, indeed, types.

// Display implemented interfaces. static void ListInterfaces(Type t)
{
Console.WriteLine(" Interfaces ");

var ifaces = from i in t.GetInterfaces() orderby i.Name select i;
//var ifaces = t.GetInterfaces().OrderBy(i=>i.Name); foreach(Type i in ifaces)
{
Console.WriteLine("->{0}", i.Name);
}
}

■ Note Be aware that a majority of the “get” methods of System.Type (GetMethods(), GetInterfaces(), etc.) have been overloaded to allow you to specify values from the BindingFlags enumeration. This provides a greater level of control on exactly what should be searched for (e.g., only static members, only public members, include private members, etc.). Consult the documentation for details.

Displaying Various Odds and Ends
Last but not least, you have one final helper method that will simply display various statistics (indicating whether the type is generic, what the base class is, whether the type is sealed, etc.) regarding the incoming type.

// Just for good measure.
static void ListVariousStats(Type t)
{
Console.WriteLine(" Various Statistics "); Console.WriteLine("Base class is: {0}", t.BaseType); Console.WriteLine("Is type abstract? {0}", t.IsAbstract); Console.WriteLine("Is type sealed? {0}", t.IsSealed); Console.WriteLine("Is type generic? {0}", t.IsGenericTypeDefinition); Console.WriteLine("Is type a class type? {0}", t.IsClass); Console.WriteLine();
}

Adding the Top-Level Statements
The top-level statements of the Program.cs file prompts the user for the fully qualified name of a type. Once you obtain this string data, you pass it into the Type.GetType() method and send the extracted System.Type into each of your helper methods. This process repeats until the user presses Q to terminate the application.

Console.WriteLine(" Welcome to MyTypeViewer "); string typeName = "";

do
{
Console.WriteLine("\nEnter a type name to evaluate"); Console.Write("or enter Q to quit: ");

// Get name of type.
typeName = Console.ReadLine();

// Does user want to quit?
if (typeName.Equals("Q",StringComparison.OrdinalIgnoreCase))
{
break;
}

// Try to display type. try
{
Type t = Type.GetType(typeName); Console.WriteLine(""); ListVariousStats(t); ListFields(t);
ListProps(t);
ListMethods(t);
ListInterfaces(t);
}
catch
{
Console.WriteLine("Sorry, can’t find type");
}
} while (true);

At this point, MyTypeViewer.exe is ready to take a test-drive. For example, run your application and enter the following fully qualified names (be aware that Type.GetType() requires case-sensitive string names):
•System.Int32
•System.Collections.ArrayList
•System.Threading.Thread
•System.Void
•System.Math
For example, here is some partial output when specifying System.Math:

Welcome to MyTypeViewer Enter a type name to evaluate
or enter Q to quit: System.Math

Various Statistics Base class is: System.Object Is type abstract? True
Is type sealed? True Is type generic? False
Is type a class type? True

Fields
->E
->PI
->Tau

Properties

Methods
->Abs
->Abs

->Acos
->Asin
->Atan
->Atan2
->Ceiling
->Cos

Notice the repeated listing for Abs. This is because there will be at least one overload for the Abs()
method. The code will be expanded to show the parameters and return types shortly.

Reflecting on Static Types
If you enter System.Console in the previous method, an exception will be thrown in the first helper method because the value for t will be null. Static types cannot be loaded using the Type.GetType(typeName) method. Instead, you must use another mechanism, the typeof function from System.Type. Update the program to handle the System.Console special case like this:

Type t = Type.GetType(typeName);
if (t == null && typeName.Equals("System.Console", StringComparison.OrdinalIgnoreCase))
{
t = typeof(System.Console);
}

Reflecting on Generic Types
When you call Type.GetType() to obtain metadata descriptions of generic types, you must make use of a special syntax involving a “backtick” character (`) followed by a numerical value that represents the number of type parameters the type supports. For example, if you want to print out the metadata description of System.Collections.Generic.List, you need to pass the following string into your application:

System.Collections.Generic.List`1

Here, you are using the numerical value of 1, given that List has only one type parameter. However, if you want to reflect over Dictionary<TKey, TValue>, supply the value 2, like so:

System.Collections.Generic.Dictionary`2

Reflecting on Method Parameters and Return Values
So far, so good! Next, we will make a minor enhancement to the current application. Specifically, you will update the ListMethods() helper function to list not only the name of a given method but also the return type and incoming parameter types. The MethodInfo type provides the ReturnType property and GetParameters()

method for these tasks. In the following modified code, notice that you are building a string that contains the type and name of each parameter using a nested foreach loop (without the use of LINQ):

static void ListMethods(Type t)
{
Console.WriteLine(" Methods ");
MethodInfo[] mi = t.GetMethods().OrderBy(m=>m.Name).ToArray(); foreach (MethodInfo m in mi)
{
// Get return type.
string retVal = m.ReturnType.FullName; string paramInfo = "( ";
// Get params.
foreach (ParameterInfo pi in m.GetParameters())
{
paramInfo += string.Format("{0} {1} ", pi.ParameterType, pi.Name);
}
paramInfo += " )";

// Now display the basic method sig.
Console.WriteLine("->{0} {1} {2}", retVal, m.Name, paramInfo);
}
Console.WriteLine();
}

If you now run this updated application, you will find that the methods of a given type are much more detailed and the mystery of the repeated methods is solved. If you enter System.Math into the program, both of the Abs() methods (and all other methods) will display the return type and the parameter(s).

Methods
->System.Double Abs ( System.Double value )
->System.Single Abs ( System.Single value )

The current implementation of ListMethods() is helpful, in that you can directly investigate each parameter and method return type using the System.Reflection object model. As an extreme shortcut, be aware that all of the XXXInfo types (MethodInfo, PropertyInfo, EventInfo, etc.) have overridden ToString() to display the signature of the item requested. Thus, you could also implement ListMethods()
as follows (once again using LINQ, where you simply select all MethodInfo objects, rather than only the Name
values):

static void ListMethods(Type t)
{
Console.WriteLine(" Methods ");
var methods = t.GetMethods().OrderBy(m=>m.Name); foreach (var m in methods)
{
Console.WriteLine("->{0}", m);
}
Console.WriteLine();
}

Interesting stuff, huh? Clearly, the System.Reflection namespace and System.Type class allow you to reflect over many other aspects of a type beyond what MyTypeViewer is currently displaying. As you would hope, you can obtain a type’s events, get the list of any generic parameters for a given member, and glean dozens of other details.
Nevertheless, at this point you have created a (somewhat capable) object browser. The major limitation with this specific example is that you have no way to reflect beyond the current assembly (MyTypeViewer)
or assemblies in the base class libraries that are always referenced. This begs the question “How can I build applications that can load (and reflect over) assemblies not referenced at compile time?” Glad you asked.

Dynamically Loading Assemblies
There will be times when you need to load assemblies on the fly programmatically, even if there is no record of said assembly in the manifest. Formally speaking, the act of loading external assemblies on demand is known as a dynamic load.
System.Reflection defines a class named Assembly. Using this class, you can dynamically load an assembly, as well as discover properties about the assembly itself. In essence, the Assembly class provides methods that allow you to programmatically load assemblies off disk.
To illustrate dynamic loading, create a new Console Application project named ExternalAssemblyReflector. Your task is to construct code that prompts for the name of an assembly (minus any extension) to load dynamically. You will pass the Assembly reference into a helper method named DisplayTypes(), which will simply print the names of each class, interface, structure, enumeration, and delegate it contains. The code is refreshingly simple.

using System.Reflection;

Console.WriteLine(" External Assembly Viewer "); string asmName = "";
Assembly asm = null; do
{
Console.WriteLine("\nEnter an assembly to evaluate"); Console.Write("or enter Q to quit: ");
// Get name of assembly. asmName = Console.ReadLine();
// Does user want to quit?
if (asmName.Equals("Q",StringComparison.OrdinalIgnoreCase))
{
break;
}

// Try to load assembly. try
{
asm = Assembly.LoadFrom(asmName); DisplayTypesInAsm(asm);
}
catch
{
Console.WriteLine("Sorry, can’t find assembly.");
}

} while (true);

static void DisplayTypesInAsm(Assembly asm)
{
Console.WriteLine("\n Types in Assembly "); Console.WriteLine("->{0}", asm.FullName);
Type[] types = asm.GetTypes(); foreach (Type t in types)
{
Console.WriteLine("Type: {0}", t);
}
Console.WriteLine("");
}

If you want to reflect over CarLibrary.dll, you will need to copy the CarLibrary.dll binary (from the previous chapter) to the project directory (if using Visual Studio Code) or to the \bin\Debug\net6.0 directory (if using Visual Studio) of the ExternalAssemblyReflector application to run this program. Enter CarLibrary (the extension is optional) when prompted, and the output will look like this:

External Assembly Viewer Enter an assembly to evaluate
or enter Q to quit: CarLibrary

Types in Assembly
->CarLibrary, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null Type: CarLibrary.Car
Type: CarLibrary.EngineStateEnum Type: CarLibrary.MiniVan
Type: CarLibrary.MusicMediaEnum Type: CarLibrary.MyInternalClass Type: CarLibrary.SportsCar

The LoadFrom method can also take an absolute path to the assembly you want to view (e.g., C:\MyApp\ MyAsm.dll). With this method, you can pass in a full path to your Console Application project. Thus, if CarLibrary.dll was located under C:\MyCode, you could enter C:\MyCode\CarLibrary (the extension is still optional).

Reflecting on Framework Assemblies
The Assembly.Load() method has several overloads. One variation allows you to specify a culture value (for localized assemblies), as well as a version number and public key token value (for framework assemblies).
Collectively speaking, the set of items identifying an assembly is termed the display name. The format of a display name is a comma-delimited string of name-value pairs that begins with the friendly name of the assembly, followed by optional qualifiers (that may appear in any order). Here is the template to follow (optional items appear in parentheses):

Name (,Version = major.minor.build.revision) (,Culture = culture token) (,PublicKeyToken= public key token)

When you are crafting a display name, the convention PublicKeyToken=null indicates that binding and matching against a nonstrongly named assembly is required. Additionally, Culture="" indicates matching against the default culture of the target machine. Here is an example:

// Load version 1.0.0.1 of CarLibrary using the default culture. Assembly a =
Assembly.Load("CarLibrary, Version=1.0.0.1, PublicKeyToken=null, Culture=\"\"");
// The quotes must be escaped with back slashes in C#

Also be aware that the System.Reflection namespace supplies the AssemblyName type, which allows you to represent the preceding string information in a handy object variable. Typically, this class is used in conjunction with System.Version, which is a wrapper around an assembly’s version number. Once you have established the display name, it can then be passed into the overloaded Assembly.Load() method, like so:

// Make use of AssemblyName to define the display name. AssemblyName asmName;
asmName = new AssemblyName(); asmName.Name = "CarLibrary"; Version v = new Version("1.0.0.1"); asmName.Version = v;
Assembly a = Assembly.Load(asmName);

To load a .NET Framework assembly (not .NET), the Assembly.Load() parameter should specify a PublicKeyToken value. With .NET, it’s not required, due to the diminished use of strong naming. For
example, assume you have a new Console Application project named FrameworkAssemblyViewer that has a reference to the Microsoft.EntityFrameworkCore package. As a reminder, this can all be done with the
.NET command-line interface (CLI).

dotnet new console -lang c# -n FrameworkAssemblyViewer -o .\FrameworkAssemblyViewer -f net6.0 dotnet sln .\Chapter17_AllProjects.sln add .\FrameworkAssemblyViewer
dotnet add .\FrameworkAssemblyViewer package Microsoft.EntityFrameworkCore -v 6.0.0

Recall that when you reference another assembly, a copy of that assembly is copied into the output directory of the referencing project. Build the project using the CLI.

dotnet build

With the project created, EntityFrameworkCode referenced, and the project built, you can now load it and inspect it. Given that the number of types in this assembly is quite large, the following application prints out only the names of public enums, using a simple LINQ query:

using System.Reflection;

Console.WriteLine(" The Framework Assembly Reflector App \n");

// Load Microsoft.EntityFrameworkCore.dll var displayName =
"Microsoft.EntityFrameworkCore, Version=6.0.0.0, Culture=neutral, PublicKeyToken=a db9793829ddae60";
Assembly asm = Assembly.Load(displayName); DisplayInfo(asm); Console.WriteLine("Done!");

Console.ReadLine();

static void DisplayInfo(Assembly a)
{
AssemblyName asmNameInfo = a.GetName(); Console.WriteLine(" Info about Assembly "); Console.WriteLine($"Asm Name: {asmNameInfo.Name}"); Console.WriteLine($"Asm Version: {asmNameInfo.Version}");
Console.WriteLine($"Asm Culture: {asmNameInfo.CultureInfo.DisplayName}");

Console.WriteLine("\nHere are the public enums:");
// Use a LINQ query to find the public enums.
var publicEnums = a.GetTypes().Where(p=>p.IsEnum && p.IsPublic); foreach (var pe in publicEnums)
{
Console.WriteLine(pe);
}
}

At this point, you should understand how to use some of the core members of the System.Reflection namespace to discover metadata at runtime. Of course, you likely will not need to build custom object browsers often (if ever). However, reflection services are the foundation for many common programming activities, including late binding.

Understanding Late Binding
Simply put, late binding is a technique in which you can create an instance of a given type and invoke its members at runtime without having hard-coded compile-time knowledge of its existence. When you are building an application that binds late to a type in an external assembly, you have no reason to set a reference to the assembly; therefore, the caller’s manifest has no direct listing of the assembly.
At first glance, it is not easy to see the value of late binding. It is true that if you can “bind early” to an object (e.g., add an assembly reference and allocate the type using the C# new keyword), you should opt to do so. For one reason, early binding allows you to determine errors at compile time, rather than at runtime. Nevertheless, late binding does have a critical role in any extendable application you may be building. You will have a chance to build such an “extendable” program later in this chapter, in the section “Building an Extendable Application.” Until then, let’s examine the role of the Activator class.

The System.Activator Class
The System.Activator class is the key to the .NET late-binding process. For the next example, you are interested only in the Activator.CreateInstance() method, which is used to create an instance of a type through late binding. This method has been overloaded numerous times to provide a good deal of flexibility. The simplest variation of the CreateInstance() member takes a valid Type object that describes the entity you want to allocate into memory on the fly.
Create a new Console Application project named LateBindingApp and update the Program.cs file as follows:

using System.Reflection;

// This program will load an external library,
// and create an object using late binding.

Console.WriteLine(" Fun with Late Binding ");
// Try to load a local copy of CarLibrary.
Assembly a = null; try
{
a = Assembly.LoadFrom("CarLibrary");
}
catch(FileNotFoundException ex)
{
Console.WriteLine(ex.Message); return;
}
if(a != null)
{
CreateUsingLateBinding(a);
}
Console.ReadLine();

static void CreateUsingLateBinding(Assembly asm)
{
try
{
// Get metadata for the Minivan type.
Type miniVan = asm.GetType("CarLibrary.MiniVan");

// Create a Minivan instance on the fly.
object obj = Activator.CreateInstance(miniVan); Console.WriteLine("Created a {0} using late binding!", obj);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}

Now, before you run this application, you will need to manually place a copy of CarLibrary.dll into the project file folder (or bin\Debug\net6.0 folder if you are using Visual Studio) of this new application.

■ Note Don’t add a reference to CarLibrary.dll for this example! The whole point of late binding is that you are trying to create an object that is not known at compile time.

Notice that the Activator.CreateInstance() method returns a System.Object rather than a strongly typed MiniVan. Therefore, if you apply the dot operator on the obj variable, you will fail to see any members of the MiniVan class. At first glance, you might assume you can remedy this problem with an explicit cast, like so:

// Cast to get access to the members of MiniVan?
// Nope! Compiler error!
object obj = (MiniVan)Activator.CreateInstance(minivan);

However, because your program has not added a reference to CarLibrary.dll, you cannot use the C# using keyword to import the CarLibrary namespace, and therefore, you cannot use a MiniVan type during the casting operation! Remember that the whole point of late binding is to create instances of objects for which there is no compile-time knowledge. Given this, how can you invoke the underlying methods of the MiniVan object stored in the System.Object reference? The answer, of course, is by using reflection.

Invoking Methods with No Parameters
Assume you want to invoke the TurboBoost() method of the MiniVan. As you recall, this method will set the state of the engine to “dead” and display an informational message. The first step is to obtain a MethodInfo object for the TurboBoost() method using Type.GetMethod(). From the resulting MethodInfo, you are then able to call MiniVan.TurboBoost using Invoke(). MethodInfo.Invoke() requires you to send in all parameters that are to be given to the method represented by MethodInfo. These parameters are represented by an array of System.Object types (as the parameters for a given method could be any number of various entities).
Given that TurboBoost() does not require any parameters, you can simply pass null (meaning “this method has no parameters”). Update your CreateUsingLateBinding() method as follows (updates in bold):

static void CreateUsingLateBinding(Assembly asm)
{
try
{
// Get metadata for the Minivan type.
Type miniVan = asm.GetType("CarLibrary.MiniVan");

// Create the Minivan on the fly.
object obj = Activator.CreateInstance(miniVan); Console.WriteLine($"Created a {obj} using late binding!");

// Get info for TurboBoost.
MethodInfo mi = miniVan.GetMethod("TurboBoost");

// Invoke method (‘null’ for no parameters). mi.Invoke(obj, null);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}

At this point, you will see the message in the console that your engine exploded.

Invoking Methods with Parameters
When you want to use late binding to invoke a method requiring parameters, you should package up the arguments as a loosely typed array of objects. The version of the Car class that has a radio and has the following method:

public void TurnOnRadio(bool musicOn, MusicMediaEnum mm)
=> MessageBox.Show(musicOn ? $"Jamming {mm}" : "Quiet time…");

This method takes two parameters: a Boolean representing if the automobile’s music system should be turned on or off and an enum representing the type of music player. Recall this enum was structured as so:

public enum MusicMediaEnum
{
musicCd, // 0
musicTape, // 1
musicRadio, // 2
musicMp3 // 3
}

Here is a new method of the Program.cs file, which invokes TurnOnRadio(). Notice that you are using the underlying numerical values of the MusicMediaEnum enumeration to specify a “radio” media player.

static void InvokeMethodWithArgsUsingLateBinding(Assembly asm)
{
try
{
// First, get a metadata description of the sports car. Type sport = asm.GetType("CarLibrary.SportsCar");

// Now, create the sports car.
object obj = Activator.CreateInstance(sport);
// Invoke TurnOnRadio() with arguments. MethodInfo mi = sport.GetMethod("TurnOnRadio"); mi.Invoke(obj, new object[] { true, 2 });
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

Ideally, at this point, you can see the relationships among reflection, dynamic loading, and late binding. To be sure, the reflection API provides many additional features beyond what has been covered here, but you should be in good shape to dig into more details if you are interested.
Again, you still might wonder exactly when you should use these techniques in your own applications.
The extendable app later in this chapter should shed light on this issue; however, the next topic under investigation is the role of .NET attributes.

Understanding the Role of .NET Attributes
As illustrated at the beginning of this chapter, one role of a .NET compiler is to generate metadata descriptions for all defined and referenced types. In addition to this standard metadata contained within any assembly, the .NET platform provides a way for programmers to embed additional metadata into
an assembly using attributes. In a nutshell, attributes are nothing more than code annotations that can be applied to a given type (class, interface, structure, etc.), member (property, method, etc.), assembly, or module.
.NET attributes are class types that extend the abstract System.Attribute base class. As you explore the .NET namespaces, you will find many predefined attributes that you are able to use in your applications. Furthermore, you are free to build custom attributes to further qualify the behavior of your types by creating

a new type deriving from Attribute. The .NET base class library provides attributes in various namespaces. Table 17-3 gives a snapshot of some—but by absolutely no means all—predefined attributes.

Table 17-3. A Tiny Sampling of Predefined Attributes

Attribute Meaning in Life

[CLSCompliant] Enforces the annotated item to conform to the rules of the Common Language Specification (CLS). Recall that CLS-compliant types are guaranteed to be used seamlessly across all .NET programming languages.
[DllImport] Allows .NET code to make calls to any unmanaged C- or C++-based code library, including the API of the underlying operating system.
[Obsolete] Marks a deprecated type or member. If other programmers attempt to use such an item, they will receive a compiler warning describing the error of their ways.

Understand that when you apply attributes in your code, the embedded metadata is essentially useless until another piece of software explicitly reflects over the information. If this is not the case, the blurb of metadata embedded within the assembly is ignored and completely harmless.

Attribute Consumers
As you would guess, the .NET Framework ships with numerous utilities that are indeed on the lookout for various attributes. The C# compiler (csc.exe) itself has been preprogrammed to discover the presence
of various attributes during the compilation cycle. For example, if the C# compiler encounters the [CLSCompliant] attribute, it will automatically check the attributed item to ensure it is exposing only CLS- compliant constructs. By way of another example, if the C# compiler discovers an item attributed with the [Obsolete] attribute, it will display a compiler warning.
In addition to development tools, numerous methods in the .NET base class libraries are preprogrammed to reflect over specific attributes. Chapter 19 introduces XML and JSON serialization, both of which use attributes to control the serialization process.
Finally, you are free to build applications that are programmed to reflect over your own custom attributes, as well as any attribute in the .NET base class libraries. By doing so, you are essentially able to create a set of “keywords” that are understood by a specific set of assemblies.

Applying Attributes in C#
To illustrate the process of applying attributes in C#, create a new Console Application project named ApplyingAttributes and add a reference to the System.Text.Json NuGet package. Update the Program.cs file to include the following global using statements:

global using System.Text.Json.Serialization; global using System.Xml.Serialization;

Assume you want to build a class named Motorcycle that can be persisted to JSON format. If you have a field that should not be exported to JSON, you can apply the [JsonIgnore] attribute.

namespace ApplyingAttributes; public class Motorcycle
{

[JsonIgnore]
public float weightOfCurrentPassengers;
// These fields are still serializable. public bool hasRadioSystem;
public bool hasHeadSet; public bool hasSissyBar;
}

■ Note an attribute applies to the “very next” item.

At this point, do not concern yourself with the actual process of object serialization (again, Chapter 19 examines the details). Just notice that when you want to apply an attribute, the name of the attribute is sandwiched between square brackets.
As you might guess, a single item can be attributed with multiple attributes. Assume you have a legacy C# class type (HorseAndBuggy) that was attributed to have a custom XML namespace. The code base has changed over time, and the class is now considered obsolete for current development. Rather than deleting the class definition from your code base (and risk breaking existing software), you can mark the class with the [Obsolete] attribute. To apply multiple attributes to a single item, simply use a comma-delimited list, like so:

namespace ApplyingAttributes;
[XmlRoot(Namespace = "http://www.MyCompany.com"), Obsolete("Use another vehicle!")] public class HorseAndBuggy
{
// …
}

As an alternative, you can also apply multiple attributes on a single item by stacking each attribute as follows:

[XmlRoot(Namespace = "http://www.MyCompany.com")] [Obsolete("Use another vehicle!")]
public class HorseAndBuggy
{
// …
}

C# Attribute Shorthand Notation
If you were consulting the .NET documentation, you might have noticed that the actual class name of the [Obsolete] attribute is ObsoleteAttribute, not Obsolete. As a naming convention, all .NET attributes (including custom attributes you may create yourself) are suffixed with the Attribute token. However, to simplify the process of applying attributes, the C# language does not require you to type in the Attribute suffix. Given this, the following iteration of the HorseAndBuggy type is identical to the previous example (it just involves a few more keystrokes):

[XmlRootAttribute(Namespace = "http://www.MyCompany.com")] [ObsoleteAttribute("Use another vehicle!")]
public class HorseAndBuggy

{
// …
}

Be aware that this is a courtesy provided by C#. Not all .NET languages support this shorthand attribute syntax.

Specifying Constructor Parameters for Attributes
Notice that the [Obsolete] attribute can accept what appears to be a constructor parameter. If you view the formal definition of the [Obsolete] attribute, you will find that this class indeed provides a constructor receiving a System.String.

public sealed class ObsoleteAttribute : Attribute
{
public ObsoleteAttribute();
public ObsoleteAttribute(string? message);
public ObsoleteAttribute(string? message, bool error); public string? Message { get; }
public bool IsError { get; }
public string DiagnosticId { get; set; } public string UrlFormat { get; set; }
}

Understand that when you supply constructor parameters to an attribute, the attribute is not allocated into memory until the parameters are reflected upon by another type or an external tool. The string data defined at the attribute level is simply stored within the assembly as a blurb of metadata.

The Obsolete Attribute in Action
Now that HorseAndBuggy has been marked as obsolete, if you were to allocate an instance of this type:

using ApplyingAttributes;

Console.WriteLine("Hello World!"); HorseAndBuggy mule = new HorseAndBuggy();

you would find that a compiler warning is issued. The warning is specifically CS0618, and the message includes the information passed into the attribute.

‘HorseAndBuggy’ is obsolete: ‘Use another vehicle!’

Visual Studio and Visual Studio Code also help with IntelliSense, which gets their information through reflection. Figure 17-1 shows the results of the Obsolete attribute in Visual Studio through IntelliSense, and Figure 17-2 shows the more detailed messaging in the Visual Studio code editor. Note that both use the term deprecated instead of obsolete.

Figure 17-1. Attributes in action in Visual Studio

Figure 17-2. Hovering over Obsolete types in the Visual Studio editor window

Figure 17-3 and Figure 17-4 show the results of the Obsolete attribute in Visual Studio Code.

Figure 17-3. Attributes in action in Visual Studio Code

Figure 17-4. Hovering over Obsolete types in the Visual Studio Code editor

Ideally, at this point, you should understand the following key points regarding .NET attributes:
•Attributes are classes that derive from System.Attribute.
•Attributes result in embedded metadata.
•Attributes are basically useless until another agent (including IDEs) reflects upon them.
•Attributes are applied in C# using square brackets.

Next up, let’s examine how you can build your own custom attributes and a piece of custom software that reflects over the embedded metadata.

Building Custom Attributes
The first step in building a custom attribute is to create a new class deriving from System.Attribute. Keeping in step with the automobile theme used throughout this book, assume you have created a new C# Class Library project named AttributedCarLibrary.
This assembly will define a handful of vehicles, each of which is described using a custom attribute named VehicleDescriptionAttribute, as follows:

namespace AttributedCarLibrary;
// A custom attribute.
public sealed class VehicleDescriptionAttribute :Attribute
{
public string Description { get; set; }

public VehicleDescriptionAttribute(string description)
=> Description = description;
public VehicleDescriptionAttribute(){ }
}

As you can see, VehicleDescriptionAttribute maintains a piece of string data using an automatic property (Description). Beyond the fact that this class derived from System.Attribute, there is nothing unique to this class definition.

■ Note for security reasons, it is considered a .NET best practice to design all custom attributes as sealed. in fact, both Visual studio and Visual studio Code provide a code snippet named Attribute that will scaffold a new System.Attribute-derived class into your code window. you can expand any snippet by typing its name and pressing the Tab key.

Applying Custom Attributes
Given that VehicleDescriptionAttribute is derived from System.Attribute, you are now able to annotate your vehicles as you see fit. For testing purposes, add the following classes to your new class library:

//Motorcycle.cs
namespace AttributedCarLibrary;
// Assign description using a "named property." [VehicleDescription(Description = "My rocking Harley")] public class Motorcycle
{
}

//HorseAndBuggy.cs
namespace AttributedCarLibrary; [Obsolete ("Use another vehicle!")]

[VehicleDescription("The old gray mare, she ain’t what she used to be…")] public class HorseAndBuggy
{
}

//Winnebago.cs
namespace AttributedCarLibrary;
[VehicleDescription("A very long, slow, but feature-rich auto")] public class Winnebago
{
}

Named Property Syntax
Notice that the description of Motorcycle is assigned a description using a new bit of attribute syntax termed a named property. In the constructor of the first [VehicleDescription] attribute, you set the underlying string data by using the Description property. If this attribute is reflected upon by an external agent, the value is fed into the Description property (named property syntax is legal only if the attribute supplies a writable .NET property).
In contrast, the HorseAndBuggy and Winnebago types are not using named property syntax and are simply passing the string data via the custom constructor. In any case, once you compile the
AttributedCarLibrary assembly, you can use ildasm.exe to view the injected metadata descriptions for your type. For example, the following shows the embedded description of the Winnebago class:

// CustomAttribute #1
//
// CustomAttribute Type: 06000005
// CustomAttributeName: AttributedCarLibrary.VehicleDescriptionAttribute :: instance void
.ctor(class System.String)
// Length: 45
// Value : 01 00 28 41 20 76 65 72 79 20 6c 6f 6e 67 2c 20 > (A very long, <
// : 73 6c 6f 77 2c 20 62 75 74 20 66 65 61 74 75 72 >slow, but feature<
// : 65 2d 72 69 63 68 20 61 75 74 6f 00 00 >e-rich auto <
// ctor args: ("A very long, slow, but feature-rich auto")

Restricting Attribute Usage
By default, custom attributes can be applied to just about any aspect of your code (methods, classes, properties, etc.). Thus, if it made sense to do so, you could use VehicleDescription to qualify methods, properties, or fields (among other things).

[VehicleDescription("A very long, slow, but feature-rich auto")] public class Winnebago
{
[VehicleDescription("My rocking CD player")] public void PlayMusic(bool On)
{

}
}

In some cases, this is exactly the behavior you require. Other times, however, you may want to build a custom attribute that can be applied only to select code elements. If you want to constrain the scope of a custom attribute, you will need to apply the [AttributeUsage] attribute on the definition of your custom attribute. The [AttributeUsage] attribute allows you to supply any combination of values (via an OR operation) from the AttributeTargets enumeration, like so:

// This enumeration defines the possible targets of an attribute. public enum AttributeTargets
{
All, Assembly, Class, Constructor,
Delegate, Enum, Event, Field, GenericParameter, Interface, Method, Module, Parameter,
Property, ReturnValue, Struct
}

Furthermore, [AttributeUsage] also allows you to optionally set a named property (AllowMultiple) that specifies whether the attribute can be applied more than once on the same item (the default is false). As well, [AttributeUsage] allows you to establish whether the attribute should be inherited by derived classes using the Inherited named property (the default is true).
To establish that the [VehicleDescription] attribute can be applied only once on a class or structure, you can update the VehicleDescriptionAttribute definition as follows:

// This time, we are using the AttributeUsage attribute
// to annotate our custom attribute.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] public sealed class VehicleDescriptionAttribute : System.Attribute
{

}

With this, if a developer attempted to apply the [VehicleDescription] attribute on anything other than a class or structure, they are issued a compile-time error.

Assembly-Level Attributes
It is also possible to apply attributes on all types within a given assembly using the [assembly:] tag. For example, assume you want to ensure that every public member of every public type defined within your assembly is CLS compliant. To do so, simply add the following assembly-level attribute at the top of any C# source code file, like this (outside of any namespace declarations):

[assembly: CLSCompliant] namespace AttributedCarLibrary;

■ Note all assembly-level or module-level attributes must be listed outside the scope of any namespace scope.

Using a Separate File for Assembly Attributes
Another approach is to add a new file to your project named similar to AssemblyAttributes.cs (any name will work, but that name conveys the purpose of the file) and place your assembly-level attributes in that file. In the .NET Framework, it was recommended to use AssemblyInfo.cs; however, with .NET (Core), that file can’t be used. Placing the attributes in a separate file will make it easier for other developers to find the attributes applied to the project.

■ Note There are two significant changes in .NET. The first is that the AssemblyInfo.cs file is now autogenerated from the project properties and is not meant for developer use. The second (and related) change is that many of the prior assembly-level attributes (Version, Company, etc.) have been replaced with properties in the project file.

Create a new file named AssemblyAttributes.cs and update it to match the following:

// List "using" statements first.

// Now list any assembly- or module-level attributes.
// Enforce CLS compliance for all public types in this
// assembly.
[assembly: CLSCompliant(true)]

If you now add a bit of code that falls outside the CLS specification (such as an exposed point of unsigned data), you will be issued a compiler warning.

// Ulong types don’t jibe with the CLS. public class Winnebago
{
public ulong notCompliant;
}

Using the Project File for Assembly Attributes
As shown in Chapter 16 with InternalsVisibleToAttribute, assembly attributes can also be added to the project file. There is a catch, in that only single-string parameter attributes can be used this way. This is true for the properties that can be set on the Package tab in the project properties.

■ Note at the time of this writing, there is an active discussion on the msBuild github repo regarding adding capability for nonstring parameters support. This would enable the CLSCompliant attribute to be added using the project file instead of a *.cs file.

Go ahead and set some of the properties (such as Authors, Description) by right-clicking the project in Solution Explorer, selecting Properties, and then clicking Package. Also, add InternalsVisibleToAttribute as you did in Chapter 16. Your project file will now look something like this:


net6.0
enable
disable
Philip Japikse
Apress
This is a simple car library with attributes

After you compile your project, navigate to the \obj\Debug\net6.0 directory, and look for the AttributedCarLibrary.AssemblyInfo.cs file. Open that, and you will see those properties as attributes (unfortunately, not very readable in this format).

using System;
using System.Reflection;

[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("CSharpCarClient")] [assembly: System.Reflection.AssemblyCompanyAttribute("Philip Japikse")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyDescriptionAttribute("This is a sample car library with attributes")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] [assembly: System.Reflection.AssemblyProductAttribute("AttributedCarLibrary")] [assembly: System.Reflection.AssemblyTitleAttribute("AttributedCarLibrary")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

As a final closing remark on assembly attributes, you can turn off the generation of the AssemblyInfo. cs class if you want to manage the process yourself.

Reflecting on Attributes Using Early Binding
Remember that an attribute is quite useless until another piece of software reflects over its values. Once a given attribute has been discovered, that piece of software can take whatever course of action necessary. Now, like any application, this “other piece of software” could discover the presence of a custom attribute using either early binding or late binding. If you want to make use of early binding, you’ll require the client application to have a compile-time definition of the attribute in question (VehicleDescriptionAttribute, in this case). Given that the AttributedCarLibrary assembly has defined this custom attribute as a public class, early binding is the best option.

To illustrate the process of reflecting on custom attributes, add a new C# Console Application project named VehicleDescriptionAttributeReader to the solution. Next, add a reference to the AttributedCarLibrary project. Using the CLI, execute these commands (each command must be on its own line):

dotnet new console -lang c# -n VehicleDescriptionAttributeReader -o .\ VehicleDescriptionAttributeReader -f net6.0
dotnet sln .\Chapter17_AllProjects.sln add .\VehicleDescriptionAttributeReader dotnet add VehicleDescriptionAttributeReader reference .\AttributedCarLibrary

Update the Program.cs file with the following code:

using AttributedCarLibrary;

Console.WriteLine(" Value of VehicleDescriptionAttribute \n"); ReflectOnAttributesUsingEarlyBinding();
Console.ReadLine();

static void ReflectOnAttributesUsingEarlyBinding()
{
// Get a Type representing the Winnebago. Type t = typeof(Winnebago);

// Get all attributes on the Winnebago.
object[] customAtts = t.GetCustomAttributes(false);

// Print the description.
foreach (VehicleDescriptionAttribute v in customAtts)
{
Console.WriteLine("-> {0}\n", v.Description);
}
}

The Type.GetCustomAttributes() method returns an object array that represents all the attributes applied to the member represented by the Type (the Boolean parameter controls whether the search should extend up the inheritance chain). Once you have obtained the list of attributes, iterate over each VehicleDescriptionAttribute class, and print out the value obtained by the Description property.

Reflecting on Attributes Using Late Binding
The previous example used early binding to print out the vehicle description data for the Winnebago type. This was possible because the VehicleDescriptionAttribute class type was defined as a public member in the AttributedCarLibrary assembly. It is also possible to make use of dynamic loading and late binding to reflect over attributes.
Add a new project called VehicleDescriptionAttributeReaderLateBinding to the solution, set it as the startup project, and copy AttributedCarLibrary.dll to the project’s folder (or \bin\Debug\net6.0 if using Visual Studio). Now, update your Program.cs file as follows:

using System.Reflection;

Console.WriteLine(" Value of VehicleDescriptionAttribute \n");

ReflectAttributesUsingLateBinding(); Console.ReadLine();

static void ReflectAttributesUsingLateBinding()
{
try
{
// Load the local copy of AttributedCarLibrary.
Assembly asm = Assembly.LoadFrom("AttributedCarLibrary");

// Get type info of VehicleDescriptionAttribute.
Type vehicleDesc = asm.GetType("AttributedCarLibrary.VehicleDescriptionAttribute");

// Get type info of the Description property.
PropertyInfo propDesc = vehicleDesc.GetProperty("Description");

// Get all types in the assembly.
Type[] types = asm.GetTypes();

// Iterate over each type and obtain any VehicleDescriptionAttributes.
foreach (Type t in types)
{
object[] objs = t.GetCustomAttributes(vehicleDesc, false);

// Iterate over each VehicleDescriptionAttribute and print
// the description using late binding.
foreach (object o in objs)
{
Console.WriteLine("-> {0}: {1}\n", t.Name, propDesc.GetValue(o, null));
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

If you were able to follow along with the examples in this chapter, this code should be (more or less) self-explanatory. The only point of interest is the use of the PropertyInfo.GetValue() method, which is used to trigger the property’s accessor. Here is the output of the current example:

Value of VehicleDescriptionAttribute
-> Motorcycle: My rocking Harley

-> HorseAndBuggy: The old gray mare, she ain’t what she used to be…

-> Winnebago: A very long, slow, but feature-rich auto

Putting Reflection, Late Binding, and Custom Attributes in Perspective
Even though you have seen numerous examples of these techniques in action, you may still be wondering when to make use of reflection, dynamic loading, late binding, and custom attributes in your programs. To be sure, these topics can seem a bit on the academic side of programming (which may or may not be a bad thing, depending on your point of view). To help map these topics to a real-world situation, you need a solid example. Assume for the moment that you are on a programming team that is building an application with the following requirement:
The product must be extendable using additional third-party tools.
What exactly is meant by extendable? Well, consider the Visual Studio or Visual Studio Code IDEs. When this application was developed, various “hooks” were inserted into the code base to allow other software vendors to “snap” (or plug in) custom modules into the IDE. Obviously, the Visual Studio/Visual Studio Code development teams had no way to set references to external .NET assemblies that had not been developed yet (thus, no early binding), so how exactly would an application provide the required hooks? Here is one possible way to solve this problem:
1.First, an extendable application must provide some input mechanism to allow the user to specify the module to plug in (such as a dialog box or command-line flag). This requires dynamic loading.
2.Second, an extendable application must be able to determine whether the module supports the correct functionality (such as a set of required interfaces) to be plugged into the environment. This requires reflection.
3.Finally, an extendable application must obtain a reference to the required infrastructure (such as a set of interface types) and invoke the members to trigger the underlying functionality. This may require late binding.
Simply put, if the extendable application has been preprogrammed to query for specific interfaces, it is able to determine at runtime whether the type can be activated. Once this verification test has been passed, the type in question may support additional interfaces that provide a polymorphic fabric to their functionality. This is the exact approach taken by the Visual Studio team and, despite what you might be thinking, is not at all difficult!

Building an Extendable Application
In the sections that follow, I will take you through an example that illustrates the process of building an application that can be augmented by the functionality of external assemblies. To serve as a road map, the extendable application entails the following assemblies:
•CommonSnappableTypes.dll: This assembly contains type definitions that will be used by each snap-in object and will be directly referenced by the Windows Forms application.
•CSharpSnapIn.dll: A snap-in written in C#, which leverages the types of
CommonSnappableTypes.dll.
•VBSnapIn.dll: A snap-in written in Visual Basic, which leverages the types of
CommonSnappableTypes.dll.
•MyExtendableApp.exe: A console application that may be extended by the functionality of each snap-in.

This application will use dynamic loading, reflection, and late binding to dynamically gain the functionality of assemblies it has no prior knowledge of.

■ Note you might be thinking to yourself, “my boss has never asked me to build a console application,” and you are probably correct! line-of-business applications built with C# usually fall into the category of smart client (Winforms or Wpf), web applications/rEsTful services (asp.NET Core), or headless processes (azure functions, Windows services, etc.). We are using console applications to focus on the specific concepts of the example, in this case dynamic loading, reflection, and late binding. later in this book, you will explore “real” user-facing applications using Wpf and asp.NET Core.

Building the Multiproject ExtendableApp Solution
Up to this point in this book, most of the applications have been stand-alone projects, with a few exceptions (like the previous one). This was done to keep the examples simple and focused. However, in real-world development, you typically work with multiple projects together in a solution.

Creating the Solution and Projects with the CLI
To get started using the CLI, enter the following commands to create a new solution, the class libraries and console application, and the project references:

dotnet new sln -n Chapter17_ExtendableApp

dotnet new classlib -lang c# -n CommonSnappableTypes -o .\CommonSnappableTypes -f net6.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\CommonSnappableTypes

dotnet new classlib -lang c# -n CSharpSnapIn -o .\CSharpSnapIn -f net6.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\CSharpSnapIn
dotnet add CSharpSnapin reference CommonSnappableTypes

dotnet new classlib -lang vb -n VBSnapIn -o .\VBSnapIn -f net6.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\VBSnapIn
dotnet add VBSnapIn reference CommonSnappableTypes

dotnet new console -lang c# -n MyExtendableApp -o .\MyExtendableApp -f net6.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\MyExtendableApp
dotnet add MyExtendableApp reference CommonSnappableTypes

Adding PostBuild Events into the Project Files
When projects are built (either from Visual Studio or from the command line), there are events that can be hooked into. For example, we want to copy the two snap-in assemblies into the console app project directory (when debugging with dotnet run) and the console app output directory (when debugging with Visual Studio) after every successful build. To do this, we are going to take advantage of several built-in macros.

If you are using Visual Studio, copy this block of markup into the CSharpSnapIn.csproj and VBSnapIn.vbproj files, which copies the compiled assembly into the MyExtendableApp output directory (MyExtendableApp\bin\debug\net6.0):



If you are using Visual Studio Code, copy this block of markup into the CSharpSnapIn.csproj and
VBSnapIn.vbproj files, which copies the compiled assembly into the MyExtendableApp project directory:



Now when each project is built, its assembly is copied into the MyExtendableApp’s target directory as well.

Creating the Solution and Projects with Visual Studio
Recall that, by default, Visual Studio names the solution the same as the first project created in that solution. However, you can easily change the name of the solution, as shown in Figure 17-5.

Figure 17-5. Creating the CommonSnappableTypes project and the ExtendableApp solution

To create the ExtendableApp solution, start by selecting File ➤ New Project to load the New Project dialog. Select Class Library and enter the name CommonSnappableTypes. Before you click OK, enter the solution name ExtendableApp, as shown in Figure 17-5.
To add another project into the solution, right-click the solution name (ExtendableApp) in Solution Explorer (or click File ➤ Add ➤ New Project) and select Add ➤ New Project. When adding another project into an existing solution, the Add New Project dialog box is a little different now; the solution options are no longer there, so you will see just the project information (name and location). Name the Class Library project CSharpSnapIn and click Create.
Next, add a reference to the CommonSnappableTypes project from the CSharpSnapIn project. To do this in Visual Studio, right-click the CSharpSnapIn project and select Add ➤ Project Reference. In the
Reference Manager dialog, select Projects ➤ Solution from the left (if not already selected) and select the box
next to CommonSnappableTypes.
Repeat the process for a new Visual Basic class library (VBSnapIn) that references the CommonSnappableTypes project.
The final project to add is a .NET Console app named MyExtendableApp. Add a reference to the CommonSnappableTypes project and set the console app as the startup project for the solution. To do this, right-click the MyExtendableApp project in Solution Explorer and select Set as StartUp Project.

■ Note if you right-click the Extendableapp solution instead of one of the projects, the context menu option displayed is set startup projects. in addition to having just one project execute when you click run, you can set up multiple projects to execute. This will be demonstrated in later chapters.

Setting Project Build Dependencies
When Visual Studio is given the command to run a solution, the startup projects and all referenced projects are built if any changes are detected; however, any unreferenced projects are not built. This can be changed by setting project dependencies. To do this, right-click the solution in Solution Explorer, select Project Build Order, and, in the resulting dialog, select the Dependencies tab and change the project to MyExtendableApp.
Notice that the CommonSnappableTypes project is already selected and the check box is disabled. This is because it is referenced directly. Select the CSharpSnapIn and VBSnapIn project check boxes as well, as shown in Figure 17-6.

Figure 17-6. Accessing the Project Build Order context menu

Now, each time the MyExtendableApp project is built, the CSharpSnapIn and VBSnapIn projects build as well.

Adding PostBuild Events
Open the project properties for the CSharpSnapIn (right-click Solution Explorer and select Properties) and navigate to the Build Events page (C#). Click the Edit Post-build button and then click Macros>>. Here you can see the macros available for use, and they all refer to paths and/or filenames. The advantage of using these macros in build events is that they are machine independent and work on the relative paths. For example, I am working in a directory called c-sharp-wf\code\chapter17. You might be (and probably are) using a different directory. By using the macros, MSBuild will always use the correct path relative to the
*.csproj files.
In the PostBuild box, enter the following (over two lines):

copy $(TargetPath) $(SolutionDir)MyExtendableApp\$(OutDir)$(TargetFileName) /Y copy $(TargetPath) $(SolutionDir)MyExtendableApp\$(TargetFileName) /Y

Do the same for the VBSnapIn project, except the property page is called Compile, and from there you can click the Build Events button.
After you have added these post-build event commands, each assembly will be copied into the MyExtendableApp’s project and output directories each time they are compiled.

Building CommonSnappableTypes.dll
In the CommonSnappableTypes project, delete the default Class1.cs file, add a new interface file named
IAppFunctionality.cs, and update the file to the following:

namespace CommonSnappableTypes; public interface IAppFunctionality
{
void DoIt();
}

Add a class file named CompanyInfoAttribute.cs and update it to the following:

namespace CommonSnappableTypes; [AttributeUsage(AttributeTargets.Class)]
public sealed class CompanyInfoAttribute : System.Attribute
{
public string CompanyName { get; set; } public string CompanyUrl { get; set; }
}

The IAppFunctionality interface provides a polymorphic interface for all snap-ins that can be consumed by the extendable application. Given that this example is purely illustrative, you supply a single method named DoIt().
The CompanyInfoAttribute type is a custom attribute that can be applied on any class type that wants to be snapped into the container. As you can tell by the definition of this class, [CompanyInfo] allows the developer of the snap-in to provide some basic details about the component’s point of origin.

Building the C# Snap-In
In the CSharpSnapIn project, delete the Class1.cs file and add a new file named CSharpModule.cs. Update the code to match the following:
using CommonSnappableTypes; namespace CSharpSnapIn;
[CompanyInfo(CompanyName = "FooBar", CompanyUrl = "www.FooBar.com")] public class CSharpModule : IAppFunctionality
{
void IAppFunctionality.DoIt()
{
Console.WriteLine("You have just used the C# snap-in!");
}
}

Notice that I chose to make use of explicit interface implementation (see Chapter 8) when supporting the IAppFunctionality interface. This is not required; however, the idea is that the only part of the system that needs to directly interact with this interface type is the hosting application. By explicitly implementing this interface, the DoIt() method is not directly exposed from the CSharpModule type.

Building the Visual Basic Snap-In
Moving on to the VBSnapIn project, delete the Class1.vb file and add a new file named VBSnapIn.vb. The code is (again) intentionally simple.

Imports CommonSnappableTypes

<CompanyInfo(CompanyName:="Chucky’s Software", CompanyUrl:="www.ChuckySoft.com")> Public Class VBSnapIn
Implements IAppFunctionality

Public Sub DoIt() Implements CommonSnappableTypes.IAppFunctionality.DoIt Console.WriteLine("You have just used the VB snap in!")
End Sub End Class

Notice that applying attributes in the syntax of Visual Basic requires angle brackets (< >) rather than square brackets ([ ]). Also notice that the Implements keyword is used to implement interface types on a given class or structure.

Adding the Code for the ExtendableApp
The final project to update is the C# console application (MyExtendableApp). After adding the MyExtendableApp console application to the solution and setting it as the startup project, add a reference to the CommonSnappableTypes project but not the CSharpSnapIn.dll or VBSnapIn.dll project.
Begin by updating the using statements at the top of the Program.cs class to the following:

using System.Reflection; using CommonSnappableTypes;

The LoadExternalModule() method performs the following tasks:
• Dynamically loads the selected assembly into memory
• Determines whether the assembly contains any types implementing
IAppFunctionality
• Creates the type using late binding
If a type implementing IAppFunctionality is found, the DoIt() method is called and then sent to the
DisplayCompanyData() method to output additional information from the reflected type.

static void LoadExternalModule(string assemblyName)
{
Assembly theSnapInAsm = null; try
{
// Dynamically load the selected assembly. theSnapInAsm = Assembly.LoadFrom(assemblyName);
}
catch (Exception ex)
{

Console.WriteLine($"An error occurred loading the snapin: {ex.Message}"); return;
}

// Get all IAppFunctionality compatible classes in assembly. var theClassTypes = theSnapInAsm
.GetTypes()
.Where(t => t.IsClass && (t.GetInterface("IAppFunctionality") != null))
.ToList();
if (!theClassTypes.Any())
{
Console.WriteLine("Nothing implements IAppFunctionality!");
}

// Now, create the object and call DoIt() method. foreach (Type t in theClassTypes)
{
// Use late binding to create the type.
IAppFunctionality itfApp = (IAppFunctionality) theSnapInAsm.CreateInstance (t.FullName, true);
itfApp?.DoIt();
// Show company info.
DisplayCompanyData(t);
}
}

The final task is to display the metadata provided by the [CompanyInfo] attribute. Create the
DisplayCompanyData() method as follows. Notice this method takes a single System.Type parameter.

static void DisplayCompanyData(Type t)
{
// Get [CompanyInfo] data. var compInfo = t
.GetCustomAttributes(false)
.Where(ci => (ci is CompanyInfoAttribute));
// Show data.
foreach (CompanyInfoAttribute c in compInfo)
{
Console.WriteLine($"More info about {c.CompanyName} can be found at {c.CompanyUrl}");
}
}

Finally, update the top-level statements to the following:

Console.WriteLine(" Welcome to MyTypeViewer "); string typeName = "";
do
{
Console.WriteLine("\nEnter a snapin to load"); Console.Write("or enter Q to quit: ");

// Get name of type.
typeName = Console.ReadLine();

// Does user want to quit?
if (typeName.Equals("Q", StringComparison.OrdinalIgnoreCase))
{
break;
}
// Try to display type. try
{
LoadExternalModule(typeName);
}
catch (Exception ex)
{
Console.WriteLine("Sorry, can’t find snapin");
}
}
while (true);

That wraps up the example application. When you run the application, type in either VBSnapIn or CSharpSnapIn and see the program in action. Note that while C# is case sensitive, when using reflection, case doesn’t matter. Both CSharpSnapIn and csharpsnapin work the equally as well.
This concludes our look at late binding. Next up is exploring dynamics in C#.

The Role of the C# dynamic Keyword
In Chapter 3, you learned about the var keyword, which allows you to define local variables in such a way that the underlying data type is determined at compile time, based on the initial assignment (recall that this is termed implicit typing). Once this initial assignment has been made, you have a strongly typed variable, and any attempt to assign an incompatible value will result in a compiler error.
To begin your investigation into the C# dynamic keyword, create a new Console Application project named DynamicKeyword. Now, add the following method in your Program.cs file, and verify that the final code statement will indeed trigger a compile-time error if uncommented:

static void ImplicitlyTypedVariable()
{
// a is of type List. var a = new List {90};
// This would be a compile-time error!
// a = "Hello";
}

As you saw in Chapter 13, implicit typing is useful with LINQ, as many LINQ queries return enumerations of anonymous classes (via projections) that you cannot directly declare in your C# code. However, even in such cases, the implicitly typed variable is, in fact, strongly typed. In the previous example, the variable a is strongly typed to be List.
On a related note, as you learned in Chapter 6, System.Object is the topmost parent class in the .NET Core Framework and can represent anything at all. If you declare a variable of type object, you have a strongly typed piece of data; however, what it points to in memory can differ based on your assignment of

the reference. To gain access to the members the object reference is pointing to in memory, you need to perform an explicit cast.
Assume you have a simple class named Person that defines two automatic properties (FirstName and
LastName) both encapsulating a string:
//Person.cs
namespace DynamicKeyword; class Person
{
public string FirstName { get; set; } = ""; public string LastName { get; set; } = "";
}

Now, observe the following code:

static void UseObjectVariable()
{
// Create a new instance of the Person class
// and assign it to a variable of type System.Object
object o = new Person() { FirstName = "Mike", LastName = "Larson" };

// Must cast object as Person to gain access
// to the Person properties.
Console.WriteLine("Person’s first name is {0}", ((Person)o).FirstName);
}

Now, back to the dynamic keyword. From a high level, you can consider the dynamic keyword a specialized form of System.Object, in that any value can be assigned to a dynamic data type. At first glance, this can appear horribly confusing, as it appears you now have three ways to define data whose underlying type is not directly indicated in your code base. For example, this method

static void PrintThreeStrings()
{
var s1 = "Greetings"; object s2 = "From";
dynamic s3 = "Minneapolis";

Console.WriteLine("s1 is of type: {0}", s1.GetType()); Console.WriteLine("s2 is of type: {0}", s2.GetType()); Console.WriteLine("s3 is of type: {0}", s3.GetType());
}

would print out the following if invoked from your top-level statements:

s1 is of type: System.String s2 is of type: System.String s3 is of type: System.String

What makes a dynamic variable vastly different from a variable declared implicitly or via a System.
Object reference is that it is not strongly typed. Said another way, dynamic data is not statically typed. As far as the C# compiler is concerned, a data point declared with the dynamic keyword can be assigned any initial

value at all and can be reassigned to any new (and possibly unrelated) value during its lifetime. Consider the following method and the resulting output:

static void ChangeDynamicDataType()
{
// Declare a single dynamic data point
// named "t".
dynamic t = "Hello!";
Console.WriteLine("t is of type: {0}", t.GetType());

t = false;
Console.WriteLine("t is of type: {0}", t.GetType());

t = new List();
Console.WriteLine("t is of type: {0}", t.GetType());
}

t is of type: System.String t is of type: System.Boolean
t is of type: System.Collections.Generic.List`1[System.Int32]

At this point in your investigation, do be aware that the previous code would compile and execute identically if you were to declare the t variable as a System.Object. However, as you will soon see, the dynamic keyword offers many additional features.

Calling Members on Dynamically Declared Data
Given that a dynamic variable can take on the identity of any type on the fly (just like a variable of type System.Object), the next question on your mind might be about calling members on the dynamic variable (properties, methods, indexers, register with events, etc.). Syntactically speaking, it will look no different. Just apply the dot operator to the dynamic data variable, specify a public member, and supply any arguments (if required).
However (and this is a very big “however”), the validity of the members you specify will not be checked by the compiler! Remember, unlike a variable defined as a System.Object, dynamic data is not statically typed. It is not until runtime that you will know whether the dynamic data you invoked supports a specified member, whether you passed in the correct parameters, whether you spelled the member correctly, and so on. Thus, as strange as it might seem, the following method compiles perfectly:

static void InvokeMembersOnDynamicData()
{
dynamic textData1 = "Hello"; Console.WriteLine(textData1.ToUpper());

// You would expect compiler errors here!
// But they compile just fine. Console.WriteLine(textData1.toupper()); Console.WriteLine(textData1.Foo(10, "ee", DateTime.Now));
}

Notice the second call to WriteLine() attempts to call a method named toupper() on the dynamic data point (note the incorrect casing—it should be ToUpper()). As you can see, textData1 is of type string, and therefore, you know it does not have a method of this name in all lowercase letters. Furthermore, string certainly does not have a method named Foo() that takes int, string, and DateTime objects!
Nevertheless, the C# compiler is satisfied. However, if you invoke this method from within Main(), you will get runtime errors like the following output:

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ‘string’ does not contain a definition for ‘toupper’

Another obvious distinction between calling members on dynamic data and strongly typed data is that when you apply the dot operator to a piece of dynamic data, you will not see the expected Visual
Studio IntelliSense. The IDE will allow you to enter any member name you could dream up. This means you need to be extremely careful when you are typing C# code on such data points. Any misspelling
or incorrect capitalization of a member will throw a runtime error, specifically an instance of the
RuntimeBinderException class.
The RuntimeBinderException represents an error that will be thrown if you attempt to invoke a member on a dynamic data type that does not actually exist (as in the case of the toupper() and Foo() methods). This same error will be raised if you specify the wrong parameter data to a member that does exist.
Because dynamic data is so volatile, whenever you are invoking members on a variable declared with the C# dynamic keyword, you could wrap the calls within a proper try/catch block and handle the error in a graceful manner, like so:

static void InvokeMembersOnDynamicData()
{
dynamic textData1 = "Hello";

try
{
Console.WriteLine(textData1.ToUpper()); Console.WriteLine(textData1.toupper()); Console.WriteLine(textData1.Foo(10, "ee", DateTime.Now));
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
{
Console.WriteLine(ex.Message);
}
}

If you call this method again, you will find the call to ToUpper() (note the capital T and U) works correctly; however, you then find the error data displayed to the console.

HELLO
‘string’ does not contain a definition for ‘toupper’

Of course, the process of wrapping all dynamic method invocations in a try/catch block is rather tedious. If you watch your spelling and parameter passing, this is not required. However, catching exceptions is handy when you might not know in advance if a member will be present on the target type.

The Scope of the dynamic Keyword
Recall that implicitly typed data (declared with the var keyword) is possible only for local variables in a member scope. The var keyword can never be used as a return value, a parameter, or a member of a class/ structure. This is not the case with the dynamic keyword, however. Consider the following class definition:

namespace DynamicKeyword; class VeryDynamicClass
{
// A dynamic field.
private static dynamic _myDynamicField;

// A dynamic property.
public dynamic DynamicProperty { get; set; }

// A dynamic return type and a dynamic parameter type.
public dynamic DynamicMethod(dynamic dynamicParam)
{
// A dynamic local variable.
dynamic dynamicLocalVar = "Local variable"; int myInt = 10;
if (dynamicParam is int)
{
return dynamicLocalVar;
}
else
{
return myInt;
}
}
}

You could now invoke the public members as expected; however, as you are operating on dynamic methods and properties, you cannot be completely sure what the data type will be! While the
VeryDynamicClass definition might not be useful in a real-world application, it does illustrate the scope of where you can apply this C# keyword.

Limitations of the dynamic Keyword
While a great many things can be defined using the dynamic keyword, there are some limitations regarding its usage. While they are not showstoppers, do know that a dynamic data item cannot make use of lambda expressions or C# anonymous methods when calling a method. For example, the following code will always result in errors, even if the target method does indeed take a delegate parameter that takes a string value and returns void:

dynamic a = GetDynamicObject();

// Error! Methods on dynamic data can’t use lambdas! a.Method(arg => Console.WriteLine(arg));

To circumvent this restriction, you will need to work with the underlying delegate directly, using the techniques described in Chapter 12. Another limitation is that a dynamic point of data cannot understand any extension methods (see Chapter 11). Unfortunately, this would also include any of the extension methods that come from the LINQ APIs. Therefore, a variable declared with the dynamic keyword has limited use within LINQ to Objects and other LINQ technologies.

dynamic a = GetDynamicObject();
// Error! Dynamic data can’t find the Select() extension method! var data = from d in a select d;

Practical Uses of the dynamic Keyword
Given that dynamic data is not strongly typed, not checked at compile time, has no ability to trigger IntelliSense, and cannot be the target of a LINQ query, you are absolutely correct to assume that using the dynamic keyword just for the sake of doing so is a poor programming practice.
However, in a few circumstances, the dynamic keyword can radically reduce the amount of code you
need to author by hand. Specifically, if you are building a .NET application that makes heavy use of late binding (via reflection), the dynamic keyword can save you typing time. As well, if you are building a .NET application that needs to communicate with legacy COM libraries (such as Microsoft Office products), you can greatly simplify your code base via the dynamic keyword. By way of a final example, web applications built using ASP.NET Core frequently use the ViewBag type, which can also be accessed in a simplified manner using the dynamic keyword.

■Note Com interaction is strictly a Windows paradigm and removes the cross-platform capabilities of your application.

Like any “shortcut,” you need to weigh the pros and cons. The use of the dynamic keyword is a trade- off between brevity of code and type safety. While C# is a strongly typed language at its core, you can opt in (or opt out) of dynamic behaviors on a call-by-call basis. Always remember that you never need to use the dynamic keyword. You could always get to the same end result by authoring alternative code by hand (and typically much more of it).

The Role of the Dynamic Language Runtime
Now that you better understand what “dynamic data” is about, let’s learn how it is processed. Since the release of .NET 4.0, the Common Language Runtime (CLR) was supplemented with a complementary runtime environment named the Dynamic Language Runtime. The concept of a “dynamic runtime” is certainly not new. In fact, many programming languages such as JavaScript, LISP, Ruby, and Python have used it for years. In a nutshell, a dynamic runtime allows a dynamic language the ability to discover types completely at runtime with no compile-time checks.

■Note While a lot of the Dlr was ported to .NET Core (starting with 3.0), feature parity between the Dlr in
.NET 6 and .NET 4.8 hasn’t been achieved.

If you have a background in strongly typed languages (including C#, without dynamic types), the notion of such a runtime might seem undesirable. After all, you typically want to receive compile-time errors, not

runtime errors, wherever possible. Nevertheless, dynamic languages/runtimes do provide some interesting features, including the following:
•An extremely flexible code base. You can refactor code without making numerous changes to data types.
•A simple way to interoperate with diverse object types built in different platforms and programming languages.
•A way to add or remove members to a type, in memory, at runtime.
One role of the DLR is to enable various dynamic languages to run with the .NET Runtime and give them a way to interoperate with other .NET code. These languages live in a dynamic universe, where type is discovered solely at runtime. And yet, these languages have access to the richness of the .NET base class
libraries. Even better, their code bases can interoperate with C# (or vice versa), thanks to the inclusion of the
dynamic keyword.

■ Note This chapter will not address how the Dlr can be used to integrate with dynamic languages.

The Role of Expression Trees
The DLR makes use of expression trees to capture the meaning of a dynamic call in neutral terms. For example, take the following C# code:

dynamic d = GetSomeData(); d.SuperMethod(12);

In this example, the DLR will automatically build an expression tree that says, in effect, “Call the method named SuperMethod on object d, passing in the number 12 as an argument.” This information (formally termed the payload) is then passed to the correct runtime binder, which again could be the C# dynamic binder or even (as explained shortly) legacy COM objects.
From here, the request is mapped into the required call structure for the target object. The nice thing about these expression trees (beyond that you do not need to manually create them) is that this allows you to write a fixed C# code statement and not worry about what the underlying target actually is.

Dynamic Runtime Lookup of Expression Trees
As explained, the DLR will pass the expression trees to a target object; however, this dispatching will be influenced by a few factors. If the dynamic data type is pointing in memory to a COM object, the expression tree is sent to a low-level COM interface named IDispatch. As you might know, this interface was COM’s way of incorporating its own set of dynamic services. COM objects, however, can be used in a .NET application without the use of the DLR or C# dynamic keyword. Doing so, however (as you will see), tends to result in much more complex C# coding.
If the dynamic data is not pointing to a COM object, the expression tree may be passed to an object implementing the IDynamicObject interface. This interface is used behind the scenes to allow a language, such as IronRuby, to take a DLR expression tree and map it to Ruby specifics.
Finally, if the dynamic data is pointing to an object that is not a COM object and does not implement IDynamicObject, the object is a normal, everyday .NET object. In this case, the expression tree is dispatched to the C# runtime binder for processing. The process of mapping the expression tree to .NET specifics involves reflection services.

After the expression tree has been processed by a given binder, the dynamic data will be resolved to the real in-memory data type, after which the correct method is called with any necessary parameters. Now, let’s look at a few practical uses of the DLR, beginning with the simplification of late-bound .NET calls.

Simplifying Late-Bound Calls Using Dynamic Types
One instance where you might decide to use the dynamic keyword is when you are working with reflection services, specifically when making late-bound method calls. Earlier in this chapter, you saw a few examples of when this type of method call can be useful, most commonly when you are building some type of extensible application. At that time, you learned how to use the Activator.CreateInstance() method to create an object, for which you have no compile-time knowledge of (beyond its display name). You can then make use of the types of the System.Reflection namespace to invoke members via late binding. Recall the earlier example:

static void CreateUsingLateBinding(Assembly asm)
{
try
{
// Get metadata for the Minivan type.
Type miniVan = asm.GetType("CarLibrary.MiniVan");

// Create the Minivan on the fly.
object obj = Activator.CreateInstance(miniVan);

// Get info for TurboBoost.
MethodInfo mi = miniVan.GetMethod("TurboBoost");

// Invoke method ("null" for no parameters). mi.Invoke(obj, null);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

While this code works as expected, you might agree it is a bit clunky. You must manually make use of the MethodInfo class, manually query the metadata, and so forth. The following is a version of this same method, now using the C# dynamic keyword and the DLR:

static void InvokeMethodWithDynamicKeyword(Assembly asm)
{
try
{
// Get metadata for the Minivan type.
Type miniVan = asm.GetType("CarLibrary.MiniVan");

// Create the Minivan on the fly and call method! dynamic obj = Activator.CreateInstance(miniVan); obj.TurboBoost();
}

catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

By declaring the obj variable using the dynamic keyword, the heavy lifting of reflection is done on your behalf, courtesy of the DRL.

Leveraging the dynamic Keyword to Pass Arguments
The usefulness of the DLR becomes even more obvious when you need to make late-bound calls on methods that take parameters. When you use “longhand” reflection calls, arguments need to be packaged up as an array of objects, which are passed to the Invoke() method of MethodInfo.
To illustrate using a fresh example, begin by creating a new C# Console Application project named LateBindingWithDynamic. Next, add a Class Library project named MathLibrary. Rename the initial Class1.cs file of the MathLibrary project to SimpleMath.cs, and implement the class like so:

namespace MathLibrary; public class SimpleMath
{
public int Add(int x, int y)
{
return x + y;
}
}

If you are using Visual Studio, update the MathLibrary.csproj file with the following (to copy the compiled assembly to the LateBindingWithDynamic target directory):



If you are using Visual Studio Code, update the MathLibrary.csproj file with the following (to copy the compiled assembly to the LateBindingWithDynamic project directory):



Now, back in the LateBindingWithDynamic project and the Program.cs file, update the using
statements to the following:

using System.Reflection;
using Microsoft.CSharp.RuntimeBinder;

Next, add the following method to the Program.cs file, which invokes the Add() method using typical reflection API calls:

static void AddWithReflection()
{
Assembly asm = Assembly.LoadFrom("MathLibrary"); try
{
// Get metadata for the SimpleMath type.
Type math = asm.GetType("MathLibrary.SimpleMath");

// Create a SimpleMath on the fly.
object obj = Activator.CreateInstance(math);

// Get info for Add.
MethodInfo mi = math.GetMethod("Add");

// Invoke method (with parameters).
object[] args = { 10, 70 };
Console.WriteLine("Result is: {0}", mi.Invoke(obj, args));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

Now, consider the simplification of the previous logic with the dynamic keyword, via the following new method:

static void AddWithDynamic()
{
Assembly asm = Assembly.LoadFrom("MathLibrary");

try
{
// Get metadata for the SimpleMath type.
Type math = asm.GetType("MathLibrary.SimpleMath");

// Create a SimpleMath on the fly.
dynamic obj = Activator.CreateInstance(math);

// Note how easily we can now call Add().
Console.WriteLine("Result is: {0}", obj.Add(10, 70));
}
catch (RuntimeBinderException ex)
{
Console.WriteLine(ex.Message);
}
}

Not too shabby! If you call both methods, you will see identical output. However, when using the dynamic keyword, you saved yourself quite a bit of work. With dynamically defined data, you no longer need to manually package up arguments as an array of objects, query the assembly metadata, or set other such details. If you are building an application that makes heavy use of dynamic loading/late binding, I am sure you can see how these code savings would add up over time.

Summary
Reflection is an interesting aspect of a robust object-oriented environment. In the world of .NET, the keys to reflection services revolve around the System.Type class and the System.Reflection namespace. As you have seen, reflection is the process of placing a type under the magnifying glass at runtime to understand the who, what, where, when, why, and how of a given item.
Late binding is the process of creating an instance of a type and invoking its members without prior knowledge of the specific names of said members. Late binding is often a direct result of dynamic loading, which allows you to load a .NET assembly into memory programmatically. As shown during this chapter’s extendable application example, this is a powerful technique used by tool builders as well as tool consumers.
This chapter also examined the role of attribute-based programming. When you adorn your types with attributes, the result is the augmentation of the underlying assembly metadata.
The dynamic keyword allows you to define data whose identity is not known until runtime. When processed by the Dynamic Language Runtime, the automatically created “expression tree” will be passed to the correct dynamic language binder, where the payload will be unpackaged and sent to the correct object member.
Using dynamic data and the DLR, complex C# programming tasks can be radically simplified, especially the act of incorporating COM libraries into your .NET applications.
While these features can certainly simplify your code, always remember that dynamic data makes your C# code much less type-safe and open to runtime errors. Be sure you weigh the pros and cons of using dynamic data in your C# projects, and test accordingly!

发表评论