Reflection is the mechanism provided by the .NET Framework to allow you to inspect how a program is constructed. Using reflection, you can obtain information such as the name of an assembly and what other assemblies a given assembly imports. You can even dynamically call methods on an instance of a type in a given assembly. Reflection also allows you to create code dynamically and compile it to an in-memory assembly or to build a symbol table of type entries in an assembly.
Reflection is a very powerful feature of the Framework and, as such, is guarded by the runtime. The ReflectionPermission
must be granted to assemblies that are going to access the protected or private members of a type. If you are going to access only the public members of a public type, you will not need to be granted the ReflectionPermission
. Code Access Security (CAS) has only two permission sets that give all reflection access by default: FullTrust
and Everything
. The LocalIntranet
permission set includes the ReflectionEmit
privilege, which allows for emitting metadata and creating assemblies, and the MemberAccess
privilege, which allows for performing dynamic invocation of methods on types in assemblies.
In this chapter, you will see how you can use reflection to dynamically invoke members on types, figure out all of the assemblies a given assembly is dependent on, and inspect assemblies for different types of information. Reflection is a great way to understand how things are put together in .NET, and this chapter provides a starting point.
This chapter will also cover the dynamic
keyword in C#, which is supported by the Dynamic Language Runtime (DLR) in .NET. It is used to help extend C# to identify the type of an object at runtime instead of statically at compile time, and to support dynamic behavior. To use these features, you need to reference the System.Dynamic
assembly and namespace.
The DLR was introduced to support the following use cases:
Porting other languages (like Python and Ruby) to .NET
Enabling dynamic features in static languages (like C# and Visual Basic)
Enabling more sharing of libraries between languages
Caching binding operations (like Reflection) to improve performance instead of determining everything at runtime each time
The DLR provides three main services:
Expression trees (to represent language semantics such as those used in LINQ)
Call site caching (caches the characteristics of the operation the first time it is performed)
Dynamic object interoperability (through the use of IDynamicMetaObjectProvider
, DynamicMetaObject
, DynamicObject
, and ExpandoObject
)
The three main constructs provided to do dynamic programming in C# are the dynamic
type (an object that is not bound by compile-time checking), the ExpandoObject
class (used to construct or deconstruct the members of an object at runtime), and the DynamicObject
class (a base class for adding dynamic behavior to your own objects). All three constructs are demonstrated in this chapter.
Use the Assembly.GetReferencedAssemblies
method, as shown in Example 6-1, to obtain the imported assemblies of a particular assembly.
public static void BuildDependentAssemblyList(string path, StringCollection assemblies) { // maintain a list of assemblies the original one needs if(assemblies == null) assemblies = new StringCollection(); // have we already seen this one? if(assemblies.Contains(path)==true) return; try { Assembly asm = null; // look for common path delimiters in the string // to see if it is a name or a path if ((path.IndexOf(@"", 0, path.Length, StringComparison.Ordinal) != -1) || (path.IndexOf("/", 0, path.Length, StringComparison.Ordinal) != -1)) { // load the assembly from a path asm = Assembly.LoadFrom(path); } else { // try as assembly name asm = Assembly.Load(path); } // add the assembly to the list if (asm != null) assemblies.Add(path); // get the referenced assemblies AssemblyName[] imports = asm.GetReferencedAssemblies(); // iterate foreach (AssemblyName asmName in imports) { // now recursively call this assembly to get the new modules // it references BuildDependentAssemblyList(asmName.FullName, assemblies); } } catch (FileLoadException fle) { // just let this one go... Console.WriteLine(fle); } }
This code returns a StringCollection
containing the original assembly, all imported assemblies, and the dependent assemblies of the imported assemblies.
If you ran this method against the assembly C:CSharpRecipesinDebugCSharpRecipes.exe, you’d get the following dependency tree:
Assembly C:CSharpRecipesinDebugCSharpRecipes.exe has a dependency tree of these assemblies : C:CSharpRecipesinDebugCSharpRecipes.exe mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Data.SqlXml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Security, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Numerics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Messaging, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.DirectoryServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Web.RegularExpressions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Accessibility, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Runtime.Serialization.Formatters.Soap, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Deployment, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Data.OracleClient, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Drawing.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Web.ApplicationServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 System.ComponentModel.DataAnnotations, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 System.DirectoryServices.Protocols, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Runtime.Caching, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.ServiceProcess, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Configuration.Install, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Runtime.Serialization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.ServiceModel.Internals, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 SMDiagnostics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Web.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a Microsoft.Build.Utilities.v4.0, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a Microsoft.Build.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Microsoft.Build.Tasks.v4.0, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a NorthwindLinq2Sql, Version=1.0.0.0, Culture=neutral, PublicKeyToken=fe85c3941fbcc4c5 System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Dynamic, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Data.DataSetExtensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Obtaining the imported types in an assembly is useful in determining what assemblies another assembly is using. This knowledge can greatly aid you in learning to use a new assembly. This method can also help you determine dependencies between assemblies for shipping purposes or to perform compliance management if you are restricted from using or exporting certain types of assemblies.
The GetReferencedAssemblies
method of the System.Reflection.Assembly
class obtains a list of all the imported assemblies. This method accepts no parameters and returns an array of AssemblyName
objects instead of an array of Type
s. The AssemblyName
type is made up of members that allow access to the information about an assembly, such as the name, version, culture information, public/private key pairs, and other data.
To call the BuildDependentAssemblyList
method on the current executable, run this example code:
string file = GetProcessPath(); StringCollection assemblies = new StringCollection(); ReflectionAndDynamicProgramming.BuildDependentAssemblyList(file,assemblies); Console.WriteLine($"Assembly {file} has a dependency tree of these assemblies:{Environment.NewLine}"); foreach(string name in assemblies) { Console.WriteLine($" {name}{Environment.NewLine}"); }
GetProcessPath
, shown here, returns the current path to the process executable:
private static string GetProcessPath() { // fix the path so that if running under the debugger we get the original // file string processName = Process.GetCurrentProcess().MainModule.FileName; int index = processName.IndexOf("vshost", StringComparison.Ordinal); if (index != -1) { string first = processName.Substring(0, index); int numChars = processName.Length - (index + 7); string second = processName.Substring(index + 7, numChars); processName = first + second; } return processName; }
Note that this method does not account for assemblies loaded via Assembly. ReflectionOnlyLoad*
methods, as it is inspecting for only compile-time references.
When loading assemblies for inspection using reflection, you should use the ReflectionOnlyLoad*
methods. These methods do not allow you to execute code from the loaded assembly. The reasoning is that you may not know if you are loading assemblies containing hostile code or not. These methods prevent any hostile code from executing.
The “Assembly Class” topic in the MSDN documentation.
Use reflection to enumerate the types that match the characteristics you are looking for. For the characteristics we have outlined, you would use the methods listed in Table 6-1.
Characteristic | Reflection method |
---|---|
Method name | Type.GetMember |
Exported types | Assembly.GetExportedTypes() |
Serializable types | Type.IsSerializeable |
Subclasses of a type | Type.IsSubclassOf |
Nested types | Type.GetNestedTypes |
To find methods by name in an assembly, use the extension method GetMembersInAssembly
:
public static IEnumerable<MemberInfo> GetMembersInAssembly(this Assembly asm, string memberName) => from type in asm.GetTypes() from ms in type.GetMember(memberName, MemberTypes.All, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) select ms;
GetMembersInAssembly
uses Type.GetMember
to search for all members that have a matching name and returns the set of MethodInfos
for those:
var members = asm.GetMembersInAssembly(memberSearchName);
For types available outside an assembly, use Assembly.GetExportedTypes
to obtain the exported types of an assembly:
var types = asm.GetExportedTypes();
To determine the Serializable
types in an assembly, use the extension method GetSerializableTypes
:
public static IEnumerable<Type> GetSerializableTypes(this Assembly asm) => from type in asm.GetTypes() where type.IsSerializable && !type.IsNestedPrivate // filters out anonymous types select type;
GetSerializableType
uses the Type.IsSerializable
property to determine if the type supports serialization and returns a set of serializable types. Instead of testing the implemented interfaces and attributes on every type, you can query the Type.IsSerialized
property to determine whether it is marked as serializable:
var serializeableTypes = asm.GetSerializableTypes();
To get the set of types in an assembly that subclass a particular type, use the extension method GetSubclassesForType
:
public static IEnumerable<Type> GetSubclassesForType(this Assembly asm, Type baseClassType) => from type in asm.GetTypes() where type.IsSubclassOf(baseClassType) select type;
GetSubclassesForType
uses the Type.IsSubclassOf
method to determine which types in the assembly subclass the given type and accepts an assembly path string and a type to represent the base class. This method returns an IEnumerable<Type>
representing the subclasses of the type passed to the baseClassType
parameter. In the example, first you get the assembly path from the current process, and then you set up use of CSharpRecipes.ReflectionUtils+BaseOverrides
as the type to test for subclasses. You call GetSubClassesForType
, and it returns an IEnumerable<Type>
:
Type type = Type.GetType( "CSharpRecipes.ReflectionAndDynamicProgramming+BaseOverrides"); var subClasses = asm.GetSubclassesForType(type);
Finally, to determine the nested types in an assembly, use the extension method GetNestedTypes
:
public static IEnumerable<Type> GetNestedTypes(this Assembly asm) => from t in asm.GetTypes() from t2 in t.GetNestedTypes(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where !t2.IsEnum && !t2.IsInterface && !t2.IsNestedPrivate // filters out anonymous types select t2;
GetNestedTypes
uses the Type.GetNestedTypes
method and inspects each type in the assembly to determine if it has nested types:
var nestedTypes = asm.GetNestedTypes();
Why should you care about these random facts about types in assemblies? Because they help you figure out how you are constructing your code and discover coding practices you may or may not want to allow. Let’s look at each one individually so you can see why you might want to know about it.
The memberName
argument can contain the wildcard character *
to indicate any character or characters. So, to find all methods starting with the string "Test"
, pass the string "Test*"
to the memberName
parameter. Note that the memberName
argument is case-sensitive, but the asmPath
argument is not. If you’d like to do a case-insensitive search for members, add the BindingFlags.IgnoreCase
flag to the other BindingFlags
in the call to Type.GetMember
.
The GetMember
method of the System.Type
class is useful for finding one or more methods within a type. This method returns an array of MemberInfo
objects that describe any members that match the given parameters.
The *
character may be used as a wildcard character only at the end of the name
parameter string. If placed anywhere else in the string, it will not be treated as a wildcard character. In addition, it must be the only character in the name
parameter to ensure that all members are returned. No other wildcard characters, such as ?
, are supported.
Once you obtain an array of MemberInfo
objects, you need to examine what kind of members they are. The MemberInfo
class contains a MemberType
property that returns a System.Reflection.MemberTypes
enumeration value, which can be any of the values defined in Table 6-2 except All
.
Enumeration value | Definition |
---|---|
All |
All member types |
Constructor |
A constructor member |
Custom |
A custom member type |
Event |
An event member |
Field |
A field member |
Method |
A method member |
NestedType |
A nested type |
Property |
A property member |
TypeInfo |
A type member that represents a TypeInfo member |
Obtaining the exported types in an assembly is useful when you are trying to determine the public interface to that assembly. This ability can greatly aid someone learning to use a new assembly or can aid the assembly developer in determining all the assembly’s access points to verify that they are adequately secure from malicious code. To get these exported types, use the GetExportedTypes
method on the System.Reflection.Assembly
type. The exported types consist of all of the types that are publicly accessible from outside of the assembly. A type may be publicly accessible but not be accessible from outside of the assembly. Take, for example, the following code:
public class Outer { public class Inner {} private class SecretInner {} }
The exported types are Outer
and Outer.Inner
; the type SecretInner
is not exposed to the world outside of this assembly. If you change the Outer
accessibility from public
to private
, you now have no types accessible to the outside world—the Inner
class access level is downgraded because of the private
on the Outer
class.
A type may be marked as serializable with the SerializableAttribute
attribute. Testing for SerializableAttribute
on a type can turn into a fair amount of work. This is because SerializableAttribute
is a magic attribute that the C# compiler actually strips off your code at compile time. Using ildasm
(the .NET platform decompiler), you will see that this custom attribute just isn’t there—normally you see a .custom
entry for each custom attribute, but not with SerializableAttribute
. The C# compiler removes it and instead sets a flag in the metadata of the class. In source code, it looks like a custom attribute, but it compiles into one of a small set of attributes that gets a special representation in metadata. That’s why it gets special treatment in the reflection APIs. Fortunately, you do not have to do all of this work. The IsSerializable
property on the Type
class returns true
if the current type is marked as serializable with the SerializableAttribute
; otherwise, this property returns false
.
The IsSubclassOf
method on the Type
class allows you to determine whether the current type is a subclass of the type passed in to this method. Knowing if a type has been subclassed allows you to explore the type hierarchy that your team or company has created and can lead to opportunities for code reuse, refactoring, or developing a better understanding of the dependencies in the code base.
Determining the nested types allows you to programmatically examine various aspects of some design patterns. Various design patterns may specify that a type will contain another type; for example, the Decorator and State design patterns make use of object containment.
The GetNestedTypes
extension method uses a LINQ query to query all types in the assembly specified by the asmPath
parameter. The LINQ query also queries for the nested types with the assembly by using the Type.GetNestedTypes
method of the Type
class.
Usually the dot operator is used to delimit namespaces and types; however, nested types are somewhat special. You set nested types apart from other types by using the +
operator in their fully qualified name when dealing with them in the reflection APIs. By passing this fully qualified name in to the static GetType
methods, you can acquire the actual type that it represents.
These methods return a Type
object that represents the type identified by the typeName
parameter.
Calling Type.GetType
to retrieve a type defined in a dynamic assembly (one that is created using the types defined in the System.Reflection.Emit
namespace) returns a null
if that assembly has not already been persisted to disk. Typically, you would use the static Assembly.GetType
method on the dynamic assembly’s Assembly
object.
The “Assembly Class,” “Type Class,” “TypeAttributes Enumeration,” “Determining and Obtaining Nested Types Within an Assembly,” “BindingFlags Enumeration,” “MemberInfo Class,” and “Finding Members in an Assembly” topics in the MSDN documentation.
Use reflection to enumerate the inheritance chains and base class method overrides, as shown in Table 6-3.
Characteristic | Reflection method |
---|---|
Inheritance hierarchy | Type.BaseType |
Base class methods | MethodInfo.GetBaseDefinition |
Use the extension method GetInheritanceChain
to retrieve the entire inheritance hierarchy for a single type. GetInheritanceChain
uses the GetBaseTypes
method to enumerate the types and then reverses the default order to present the enumerated list sorted from base type to derived type. In other words, when GetBaseTypes
traverses the BaseType
property of each type it encounters, the resulting list of types is ordered from most derived to least derived, so we call Reverse
to order the list with the least derived type (Object
) first:
public static IEnumerable<Type> GetInheritanceChain(this Type derivedType) => (from t in derivedType.GetBaseTypes() select t).Reverse(); private static IEnumerable<Type> GetBaseTypes(this Type type) { Type current = type; while (current != null) { yield return current; current = current.BaseType; } }
If you wanted to do this for all types in an assembly, you could use the extension method GetTypeHierarchies
, which uses the custom TypeHierarchy
class to represent the derived type and its inheritance chain:
public class TypeHierarchy { public Type DerivedType { get; set; } public IEnumerable<Type> InheritanceChain { get; set; } } public static IEnumerable<TypeHierarchy> GetTypeHierarchies(this Assembly asm) => from Type type in asm.GetTypes() select new TypeHierarchy { DerivedType = type, InheritanceChain = GetInheritanceChain(type) };
GetTypeHierarchies
projects each type as the DerivedType
and uses GetInheritanceChain
to determine the InheritanceChain
for the type.
To determine if base class methods are being overridden, use the MethodInfo.GetBaseDefinition
method to determine which method is overridden in what base class. The extension method GetMethodOverrides
shown in Example 6-1 examines all of the public instance methods in a class and displays which methods override their respective base class methods. This method also determines which base class the overridden method is in. This extension method is based on Type
and uses the type to find overriding methods.
public class ReflectionUtils { public static IEnumerable<MemberInfo> GetMethodOverrides(this Type type) => from ms in type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) where ms != ms.GetBaseDefinition() select ms.GetBaseDefinition;
The next extension method, GetBaseMethodOverridden
, allows you to determine whether a particular method overrides a method in its base class and to get the MethodInfo
for that overridden method back. It also extends Type
, the full method name, and an array of Type
objects representing its parameter types:
public class ReflectionUtils { public static MethodInfo GetBaseMethodOverridden(this Type type, string methodName, Type[] paramTypes) { MethodInfo method = type.GetMethod(methodName, paramTypes); MethodInfo baseDef = method?.GetBaseDefinition(); if (baseDef != method) { bool foundMatch = (from p in baseDef.GetParameters() join op in paramTypes on p.ParameterType.UnderlyingSystemType equals op.UnderlyingSystemType select p).Any(); if (foundMatch) return baseDef; } return null; } }
Unfortunately, no property of the Type
class exists to obtain the inheritance hierarchy of a type. The DisplayInheritanceChain
methods in this recipe, however, allow you to do so. All that is required is the assembly path and the name of the type with the inheritance hierarchy you wish to obtain. The DisplayInheritanceChain
method requires only an assembly path since it displays the inheritance hierarchy for all types within that assembly.
The core code of this recipe exists in the GetBaseTypes
method. This is a recursive method that walks each inherited type until it finds the ultimate base class—which is always the object
class. Once it arrives at this ultimate base class, it returns to its caller. Each time the method returns to its caller, the next base class in the inheritance hierarchy is added to the list until the final GetBaseTypes
method returns the completed inheritance chain.
To display the inheritance chain of a type, use the DisplayInheritanceChain
method call.
private static void DisplayInheritanceChain(IEnumerable<Type> chain) { StringBuilder builder = new StringBuilder(); foreach (var type in chain) { if (builder.Length == 0) builder.Append(type.Name); else builder.AppendFormat($"<-{type.Name}"); } Console.WriteLine($"Base Type List: {builder.ToString()}"); }
To display the inheritance hierarchy of all types in an assembly, use GetTypeHierarchies
in conjunction with DisplayInheritanceChain
:
// all types in the assembly var typeHierarchies = asm.GetTypeHierarchies(); foreach (var th in typeHierarchies) { // Recurse over all base types Console.WriteLine($"Derived Type: {th.DerivedType.FullName}"); DisplayInheritanceChain(th.InheritanceChain); Console.WriteLine(); }
These methods result in output like the following:
Derived Type: CSharpRecipes.Reflection Base Type List: Object<-Reflection Derived Type: CSharpRecipes.ReflectionUtils+BaseOverrides Base Type List: Object<-BaseOverrides Derived Type: CSharpRecipes.ReflectionUtils+DerivedOverrides Base Type List: Object<-BaseOverrides <-DerivedOverrides
This output shows that the base type list (or inheritance hierarchy) of the Reflection
class in the CSharpRecipes
namespace starts with Object
(like all class and struct types in .NET). The nested class BaseOverrides
also shows a base type list starting with Object
. The nested class DerivedOverrides
shows a more interesting base type list, where DerivedOverrides
derives from BaseOverrides
, which derives from Object
.
Determining which methods override their base class methods would be a tedious chore if it were not for the GetBaseDefinition
method of the System.Reflection.MethodInfo
type. This method takes no parameters and returns a MethodInfo
object that corresponds to the overridden method in the base class. If this method is used on a MethodInfo
object representing a method that is not being overridden—as is the case with a virtual or abstract method—GetBaseDefinition
returns the original MethodInfo
object.
The Type
object’s GetMethod
method is called when both the method name and its parameter array are passed in to GetBaseMethodOverridden
; otherwise, GetMethods
is used for GetMethodOverrides
. If the method is correctly located and its MethodInfo
object obtained, the GetBaseDefinition
method is called on that MethodInfo
object to get the first overridden method in the nearest base class in the inheritance hierarchy. This MethodInfo
type is compared to the MethodInfo
type on which the GetBaseDefinition
method was called. If these two objects are the same, it means that there were no overridden methods in any base classes; therefore, nothing is returned. This code will return only the overridden method; if no methods are overridden, then null
is returned.
The following code shows how to use each of these overloaded methods:
Type derivedType = asm.GetType("CSharpRecipes.ReflectionAndDynamicProgramming+DerivedOverrides", true, true); var methodOverrides = derivedType.GetMethodOverrides(); foreach (MethodInfo mi in methodOverrides) { Console.WriteLine(); Console.WriteLine($"Current Method: {mi.ToString()}"); Console.WriteLine($"Base Type FullName: {mi.DeclaringType.FullName}"); Console.WriteLine($"Base Method: {mi.ToString()}"); // list the types of this method foreach (ParameterInfo pi in mi.GetParameters()) { Console.WriteLine($" Param {pi.Name} : {pi.ParameterType.ToString()}"); } } // try the signature findmethodoverrides string methodName = "Foo"; var baseTypeMethodInfo = derivedType.GetBaseMethodOverridden(methodName, new Type[3] { typeof(long), typeof(double), typeof(byte[]) }); Console.WriteLine( $"{Environment.NewLine}For [Type] Method: [{derivedType.Name}]" + $" {methodName}"); Console.WriteLine( $"Base Type FullName: {baseTypeMethodInfo.ReflectedType.FullName}"); Console.WriteLine($"Base Method: {baseTypeMethodInfo}"); foreach (ParameterInfo pi in baseTypeMethodInfo.GetParameters()) { // list the params so we can see which one we got Console.WriteLine($" Param {pi.Name} : {pi.ParameterType.ToString()}"); }
In the usage code, you get the path to the test code assembly (CSharpRecipes.exe) via the Process
class. You then use that to find a class that has been defined in the ReflectionUtils
class, called DerivedOverrides
, which derives from BaseOverrides
. DerivedOverrides
and BaseOverrides
are both shown here:
public abstract class BaseOverrides { public abstract void Foo(string str, int i); public abstract void Foo(long l, double d, byte[] bytes); } public class DerivedOverrides : BaseOverrides { public override void Foo(string str, int i) { } public override void Foo(long l, double d, byte[] bytes) { } }
GetMethodOverrides
returns every overridden method for each method it finds in the Reflection.DerivedOverrides
type. If you want to display all overriding methods and their corresponding overridden methods, you can remove the BindingFlags.DeclaredOnly
binding enumeration from the GetMethods
method call:
return from ms in type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) where ms != ms.GetBaseDefinition() select ms.GetBaseDefinition();
GetBaseMethodOverridden
passes a method name, and the parameters for this method, to find the override that specifically matches the signature based on the parameters. In this case, the parameter types of method Foo
are long
, double
, and byte[]
. This method displays the method that DerivedOverrides.Foo
overrides.
The “Assembly Class,” “Type.BaseType Method,” “Finding Members in an Assembly,” “MethodInfo Class,” and “ParameterInfo Class” topics in the MSDN documentation.
You have a list of method names that you wish to invoke dynamically within your application. As your code executes, it will pull names off this list and attempt to invoke these methods. This technique might be useful to create a test harness for components that reads in the methods to execute from an XML (or JSON) file and executes them with the given arguments.
The TestReflectionInvocation
method shown in Example 6-3 calls the ReflectionInvoke
method, which opens the XML configuration file, reads out the test information using LINQ, and executes each test method.
public static void TestReflectionInvocation() { XDocument xdoc = XDocument.Load(@"....SampleClassLibrarySampleClassLibraryTests.xml"); ReflectionInvoke(xdoc, @"SampleClassLibrary.dll"); }
This is the XML document in which the test method information is contained:
<?xml version="1.0" encoding="utf-8" ?> <Tests> <Test className='SampleClassLibrary.SampleClass' methodName='TestMethod1'> <Argument>Running TestMethod1</Argument> </Test> <Test className='SampleClassLibrary.SampleClass' methodName='TestMethod2'> <Parameter>Running TestMethod2</Parameter> <Parameter>27</Parameter> </Test> </Tests>
ReflectionInvoke
, as shown in Example 6-4, dynamically invokes the method that is passed to it using the information contained in the XDocument
. This code determines each parameter’s type by examining the ParameterInfo
items on the MethodInfo
, and then converts the values to the actual type from a string via the Convert.ChangeType
method. Finally, the return value of the invoked method is returned by the MethodBase.Invoke
method.
public static void ReflectionInvoke(XDocument xdoc, string asmPath) { var test = from t in xdoc.Root.Elements("Test") select new { typeName = (string)t.Attribute("className").Value, methodName = (string)t.Attribute("methodName").Value, parameter = from p in t.Elements("Parameter") select new { arg = p.Value } }; // Load the assembly Assembly asm = Assembly.LoadFrom(asmPath); foreach (var elem in test) { // create the actual type Type reflClassType = asm.GetType(elem.typeName, true, false); // Create an instance of this type and verify that it exists object reflObj = Activator.CreateInstance(reflClassType); if (reflObj != null) { // Verify that the method exists and get its MethodInfo obj MethodInfo invokedMethod = reflClassType.GetMethod(elem.methodName); if (invokedMethod != null) { // Create the argument list for the dynamically invoked methods object[] arguments = new object[elem.parameter.Count()]; int index = 0; // for each parameter, add it to the list foreach (var arg in elem.parameter) { // get the type of the parameter Type paramType = invokedMethod.GetParameters()[index].ParameterType; // change the value to that type and assign it arguments[index] = Convert.ChangeType(arg.arg, paramType); index++; } // Invoke the method with the parameters object retObj = invokedMethod.Invoke(reflObj, arguments); Console.WriteLine($" Returned object: {retObj}"); Console.WriteLine($" Returned object: {retObj.GetType().FullName}"); } } } }
These are the dynamically invoked methods located on the SampleClass
type in the SampleClassLibrary
assembly:
public bool TestMethod1(string text) { Console.WriteLine(text); return (true); } public bool TestMethod2(string text, int n) { Console.WriteLine(text + " invoked with {0}",n); return (true); }
And here is the output from these methods:
Running TestMethod1 Returned object: True Returned object: System.Boolean Running TestMethod2 invoked with 27 Returned object: True Returned object: System.Boolean
Reflection enables you to dynamically invoke both static and instance methods within a type in either the same assembly or in a different one. This can be a very powerful tool to allow your code to determine at runtime which method to call. This determination can be based on an assembly name, a type name, or a method name, though the assembly name is not required if the method exists in the same assembly as the invoking code, if you already have the Assembly
object, or if you have a Type
object for the class the method is on.
As always, with great power comes great responsibility. Dynamically loading an assembly without knowing the origin (or even invoking a legit one in an elevated context) can cause unwanted consequences, so use this technique wisely and securely!
This technique may seem similar to delegates since both can dynamically determine at runtime which method is to be called. Delegates, on the whole, require you to know signatures of methods you might call at runtime, whereas with reflection, you can invoke methods when you have no idea of the signature, providing a much looser binding. However, you will still have to pass in reasonable arguments. More dynamic invocation can be achieved with Delegate
.DynamicInvoke
, but this is more of a reflection-based method than the traditional delegate invocation.
The DynamicInvoke
method shown in the Solution contains all the code required to dynamically invoke a method. This code first loads the assembly using its assembly name (passed in through the asmPath
parameter). Next, it gets the Type
object for the class containing the method to invoke (it obtains the class name from the Test
element’s className
attribute using LINQ). It then retrieves the method name from the Test
element’s methodName
attribute using LINQ. Once you have all of the information from the Test
element, an instance of the Type
object is created, and you then invoke the specified method on this created instance:
First, the static Activator.CreateInstance
method is called to actually create an instance of the Type
object contained in the local variable dynClassType
. The method returns an object reference to the instance of type
that was created or throws an exception if the object cannot be created.
Once you have successfully obtained the instance of this class, the MethodInfo
object of the method to be invoked is acquired through a call to GetMethod
on the Type
object.
The instance of the object created with the CreateInstance
method is then passed as the first parameter to the MethodInfo.Invoke
method. This method returns an object containing the return value of the invoked method. This object is then returned by InvokeMethod
. The second parameter to MethodInfo.Invoke
is an object array containing any parameters to be passed to this method. This array is constructed based on the number of Parameter
elements under each Test
element in the XML. You then look at the ParameterInfo
of each parameter (obtained from MethodInfo. GetParameters
) and use the Convert.ChangeType
method to coerce the string value from the XML to the proper type.
The DynamicInvoke
method finally displays each returned object value and its type. Note that there is no extra logic required to return different return values from the invoked methods since they are all returned as an object, unlike when you pass differing arguments to the invoked methods.
The “Activator Class,” “MethodInfo Class,” “Convert.ChangeType Method,” and “ParameterInfo Class” topics in the MSDN documentation.
Use the LocalVariables
property on the MethodBody
class to return an IList
of LocalVariableInfo
objects, each of which describes a local variable within the method:
public static ReadOnlyCollection<LocalVariableInfo> GetLocalVars(string asmPath, string typeName, string methodName) { Assembly asm = Assembly.LoadFrom(asmPath); Type asmType = asm.GetType(typeName); MethodInfo mi = asmType.GetMethod(methodName); MethodBody mb = mi.GetMethodBody(); System.Collections.ObjectModel.ReadOnlyCollection<LocalVariableInfo> vars = (System.Collections.ObjectModel.ReadOnlyCollection<LocalVariableInfo>) mb.LocalVariables; // Display information about each local variable foreach (LocalVariableInfo lvi in vars) { Console.WriteLine($"IsPinned: {lvi.IsPinned}"); Console.WriteLine($"LocalIndex: {lvi.LocalIndex}"); Console.WriteLine($"LocalType.Module: {lvi.LocalType.Module}"); Console.WriteLine($"LocalType.FullName: {lvi.LocalType.FullName}"); Console.WriteLine($"ToString(): {lvi.ToString()}"); } return (vars); }
You can call the GetLocalVars
method using the following code:
public static void TestGetLocalVars() { string file = GetProcessPath(); // Get all local var info for the // CSharpRecipes.Reflection.GetLocalVars method System.Collections.ObjectModel.ReadOnlyCollection<LocalVariableInfo> vars = GetLocalVars(file, "CSharpRecipes.ReflectionAndDynamicProgramming", "GetLocalVars"); }
GetProcessPath
, shown here, returns the current path to the process executable:
private static string GetProcessPath() { // fix the path so that if running under the debugger we get the // original file string processName = Process.GetCurrentProcess().MainModule.FileName; int index = processName.IndexOf("vshost", StringComparison.Ordinal); if (index != -1) { string first = processName.Substring(0, index); int numChars = processName.Length - (index + 7); string second = processName.Substring(index + 7, numChars); processName = first + second; } return processName; }
Here is the output of this method:
IsPinned: False LocalIndex: 0 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Reflection.Assembly ToString(): System.Reflection.Assembly (0) IsPinned: False LocalIndex: 1 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Type ToString(): System.Type (1) IsPinned: False LocalIndex: 2 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Reflection.MethodInfo ToString(): System.Reflection.MethodInfo (2) IsPinned: False LocalIndex: 3 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Reflection.MethodBody ToString(): System.Reflection.MethodBody (3) IsPinned: False LocalIndex: 4 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Collections.ObjectModel.ReadOnlyCollection`1[[System. Reflection.LocalVariableInfo, mscorlib, Version=4.0.0.0, Culture=neutral, Public KeyToken=b77a5c561934e089]] ToString(): System.Collections.ObjectModel.ReadOnlyCollection`1[System.Reflectio n.LocalVariableInfo] (4) IsPinned: False LocalIndex: 5 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Collections.Generic.IEnumerator`1[[System.Reflection. LocalVariableInfo, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b7 7a5c561934e089]] ToString(): System.Collections.Generic.IEnumerator`1[System.Reflection.LocalVari ableInfo] (5) IsPinned: False LocalIndex: 6 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Reflection.LocalVariableInfo ToString(): System.Reflection.LocalVariableInfo (6) IsPinned: False LocalIndex: 7 LocalType.Module: CommonLanguageRuntimeLibrary LocalType.FullName: System.Collections.ObjectModel.ReadOnlyCollection`1[[System. Reflection.LocalVariableInfo, mscorlib, Version=4.0.0.0, Culture=neutral, Public KeyToken=b77a5c561934e089]] ToString(): System.Collections.ObjectModel.ReadOnlyCollection`1[System.Reflectio n.LocalVariableInfo] (7)
The LocalVariableInfo
objects for each local variable found in the CSharpRecipes.Reflection.GetLocalVars
method will be returned in the vars IList
collection.
The LocalVariables
property can give you a good amount of information about variables within a method. It returns an IList<LocalVariableInfo>
collection. Each LocalVariableInfo
object contains the information described in Table 6-4.
Member | Definition |
---|---|
IsPinned |
Returns a bool indicating if the object that this variable refers to is pinned in memory (true ) or not (false ). In unmanaged code, an object must be pinned before it can be referred to by an unmanaged pointer. While it is pinned, it cannot be moved by garbage collection. |
LocalIndex |
Returns the index of this variable within this method’s body. |
LocalType |
Returns a Type object that describes the type of this variable. |
ToString |
Returns the LocalType.FullName , a space, and then the LocalIndex value surrounded by parentheses. |
The “MethodInfo Class,” “MethodBody Class,” “ReadOnlyCollection<T> Class,” and “LocalVariableInfo Class” topics in the MSDN documentation.
You create a generic type similarly to how you create a nongeneric type; however, there is an extra step to create the type arguments you want to use and to bind these type arguments to the generic type’s type parameters at construction. You will use a new method added to the Type
class called BindGenericParameters
:
public static void CreateDictionary() { // Get the type we want to construct Type typeToConstruct = typeof(Dictionary<,>); // Get the type arguments we want to construct our type with Type[] typeArguments = {typeof(int), typeof(string)}; // Bind these type arguments to our generic type Type newType = typeToConstruct.MakeGenericType(typeArguments); // Construct our type Dictionary<int, string> dict = (Dictionary<int, string>)Activator.CreateInstance(newType); // Test our newly constructed type Console.WriteLine($"Count == {dict.Count}"); dict.Add(1, "test1"); Console.WriteLine($"Count == {dict.Count}"); }
This is the code to test the CreateDictionary
method:
public static void TestCreateMultiMap() { Assembly asm = Assembly.LoadFrom("C:\CSCB6 " + "\Code\CSharpRecipes\bin\Debug\CSharpRecipes.exe"); CreateDictionary(asm); }
And here is the output of this method:
Count == 0 Count == 1
Type parameters are defined on a class and indicate that any type that can be converted to an Object
can be substituted for this type parameter (unless, of course, there are constraints placed on this type parameter via the where
keyword). For example, the following class has two type parameters, T
and U
:
public class Foo<T, U> {...}
Of course, you do not have to use T
and U
; you can instead use another letter or even a full name, such as TypeParam1
and TypeParam2
.
A type argument is defined as the actual type that will be substituted for the type parameter. In the previously defined class Foo
, you can replace type parameter T
with the type argument int
, and type parameter U
with the type argument string
.
The BindGenericParameters
method allows you to substitute type parameters with actual type arguments. This method accepts a single Type
array parameter. This Type
array consists of each type argument that will be substituted for each type parameter of the generic type. These type arguments must be added to this Type
array in the same order as they are defined on the class. For example, the Foo
class defines type parameters T
and U
, in that order. The Type
array that you define contains an int
type and a string
type, in that order. This means that the type parameter T
will be substituted for the type argument int
, and U
will be replaced with a string
type. The BindGenericParameters
method returns a Type
object of the type you specified along with the type arguments.
The “Type.BindGenericParameters method” topic in the MSDN documentation.
To demonstrate the primary difference between dynamic
and object
, we will revisit the sample class we used in Recipe 6.4. That code dynamically loaded an instance of the SampleClass
type and then, using an XML file and reflection, ran certain operations on the instance. That instance was of type object
. If we created the type and made it dynamic
, we could actually write the code to call the methods right in the code (giving up the flexibility of the first example but making the code much neater) even though our dynamic
object instance is not of type SampleClass
:
// Load the assembly Assembly asm = Assembly.LoadFrom(@"SampleClassLibrary.dll"); // Get the SampleClass type Type reflClassType = asm?.GetType("SampleClassLibrary.SampleClass", true, false); if (reflClassType != null) { // Create our sample class instance dynamic sampleClass = Activator.CreateInstance(reflClassType); Console.WriteLine($"LastMessage: {sampleClass.LastMessage}"); Console.WriteLine("Calling TestMethod1"); sampleClass.TestMethod1("Running TestMethod1"); Console.WriteLine($"LastMessage: {sampleClass.LastMessage}"); Console.WriteLine("Calling TestMethod2"); sampleClass.TestMethod2("Running TestMethod2", 27); Console.WriteLine($"LastMessage: {sampleClass.LastMessage}"); }
Notice that we can call the methods directly without error even though the type of the object instance is dynamic
. This is because the compiler knows to defer type checking of these calls (LastMessage
, TestMethod1
, TestMethod2
) until runtime. Although dynamic
is treated like object
and even ultimately compiles to object
, it tells the compiler, “Hey relax, I know what I’m doing!” and allows you to invoke methods and properties that the compiler can’t resolve.
The output of this example is shown here:
LastMessage: Not set yet Calling TestMethod1 Running TestMethod1 LastMessage: Running TestMethod1 Calling TestMethod2 Running TestMethod2 invoked with 27 LastMessage: Running TestMethod2
The dynamic
type allows you to bypass compile-time type checking and binds the operations to call sites at runtime.
Just remember, if you aren’t finding out until runtime, you might see exceptions you weren’t expecting if you access things on a dynamic object that are not present.
Most of the time, dynamic
acts just like object
, with the main difference being the deferred checking. Once the operation for a dynamic
type is invoked, the results of the binding are cached to help with performance the next time the operation is called. If you look at the IL for a dynamic method, you will see that the sampleClass
local variable actually compiles down to the type object
:
.locals init ([0] class [mscorlib]System.Reflection.Assembly asm, [1] class [mscorlib]System.Type reflClassType, [2] bool V_2, [3] object sampleClass)
If we tried to do the same operations on our SampleClass
instance using object
instead of dynamic
, like this:
object objSampleClass = Activator.CreateInstance(reflClassType); Console.WriteLine($"LastMessage: {objSampleClass.LastMessage}"); Console.WriteLine("Calling TestMethod1"); objSampleClass.TestMethod1("Running TestMethod1"); Console.WriteLine($"LastMessage: {objSampleClass.LastMessage}"); Console.WriteLine("Calling TestMethod2"); objSampleClass.TestMethod2("Running TestMethod2", 27); Console.WriteLine($"LastMessage: {objSampleClass.LastMessage}");
We would get the following compiler errors:
Error CS1061 'object' does not contain a definition for 'LastMessage' and no extension method 'LastMessage' accepting a first argument of type 'object' could be found(are you missing a using directive or an assembly reference ?) 06_ReflectionAndDynamicProgramming.cs 482 Error CS1061 'object' does not contain a definition for 'TestMethod1' and no extension method 'TestMethod1' accepting a first argument of type 'object' could be found(are you missing a using directive or an assembly reference ?) 06_ReflectionAndDynamicProgramming.cs 484 Error CS1061 'object' does not contain a definition for 'LastMessage' and no extension method 'LastMessage' accepting a first argument of type 'object' could be found(are you missing a using directive or an assembly reference ?) 06_ReflectionAndDynamicProgramming.cs 485 Error CS1061 'object' does not contain a definition for 'TestMethod2' and no extension method 'TestMethod2' accepting a first argument of type 'object' could be found(are you missing a using directive or an assembly reference ?) 06_ReflectionAndDynamicProgramming.cs 487 Error CS1061 'object' does not contain a definition for 'LastMessage' and no extension method 'LastMessage' accepting a first argument of type 'object' could be found(are you missing a using directive or an assembly reference ?) 06_ReflectionAndDynamicProgramming.cs 488
The “dynamic” topic in the MSDN documentation.
Use ExpandoObject
to create an object that you can add properties, methods, and events to and be able to bind data to in a user interface.
We can use ExpandoObject
to create an initial object to hold someone’s Name
and current Country
:
dynamic expando = new ExpandoObject(); expando.Name = "Brian"; expando.Country = "USA";
Once we have added properties directly, we can also add properties to our object in a more dynamic fashion using the AddProperty
method we have provided for you. One example of why you might do this is to add properties to your object from another source of data. We will add the Language
property:
// Add properties dynamically to expando AddProperty(expando, "Language", "English");
The AddProperty
method takes advantage of ExpandoObject
’s support for IDictionary<string, object>
and allows us to add properties using values we determine at runtime:
public static void AddProperty(ExpandoObject expando, string propertyName, object propertyValue) { // ExpandoObject supports IDictionary so we can extend it like this var expandoDict = expando as IDictionary<string, object>; if (expandoDict.ContainsKey(propertyName)) expandoDict[propertyName] = propertyValue; else expandoDict.Add(propertyName, propertyValue); }
We can also add methods to the ExpandoObject
by using the Func<>
generic type, which represents a method call. In our example, we will add a validation method for our object:
// Add method to expando expando.IsValid = (Func<bool>)(() => { // Check that they supplied a name if(string.IsNullOrWhiteSpace(expando.Name)) return false; return true; }); if(!expando.IsValid()) { // Don't allow continuation... }
Now we can also define and add events to the ExpandoObject
using the Action<>
generic type. We will add two events, LanguageChanged
and CountryChanged
. We’ll add LanguageChanged
after defining the eventHandler
variable to hold the Action<object,EventArgs>
, and we’ll add CountryChanged
directly as an inline anonymous method. CountryChanged
looks at the Country
that changed and invokes the LanguageChanged
event with the proper Language
for the Country
. (Note that LanguageChanged
is also an anonymous method, but sometimes it can make for cleaner code to have a variable for these.)
// You can also add event handlers to expando objects var eventHandler = new Action<object, EventArgs>((sender, eventArgs) => { dynamic exp = sender as ExpandoObject; var langArgs = eventArgs as LanguageChangedEventArgs; Console.WriteLine($"Setting Language to : {langArgs?.Language}"); exp.Language = langArgs?.Language; }); // Add a LanguageChanged event and predefined event handler AddEvent(expando, "LanguageChanged", eventHandler); // Add a CountryChanged event and an inline event handler AddEvent(expando, "CountryChanged", new Action<object, EventArgs>((sender, eventArgs) => { dynamic exp = sender as ExpandoObject; var ctryArgs = eventArgs as CountryChangedEventArgs; string newLanguage = string.Empty; switch (ctryArgs?.Country) { case "France": newLanguage = "French"; break; case "China": newLanguage = "Mandarin"; break; case "Spain": newLanguage = "Spanish"; break; } Console.WriteLine($"Country changed to {ctryArgs?.Country}, " + $"changing Language to {newLanguage}"); exp?.LanguageChanged(sender, new LanguageChangedEventArgs() { Language = newLanguage }); }));
We have provided the AddEvent
method for you to encapsulate the details of adding the event to the ExpandoObject
. This again takes advantage of ExpandoObject
’s support of IDictionary<string,object>
:
public static void AddEvent(ExpandoObject expando, string eventName, Action<object, EventArgs> handler) { var expandoDict = expando as IDictionary<string, object>; if (expandoDict.ContainsKey(eventName)) expandoDict[eventName] = handler; else expandoDict.Add(eventName, handler); }
Finally, ExpandoObject
supports INotifyPropertyChanged
, which is the foundation of binding data to properties in .NET. We hook up the event handler, and when the Country
property is changed we fire the CountryChanged
event:
((INotifyPropertyChanged)expando).PropertyChanged += new PropertyChangedEventHandler((sender, ea) => { dynamic exp = sender as dynamic; var pcea = ea as PropertyChangedEventArgs; if(pcea?.PropertyName == "Country") exp.CountryChanged(exp, new CountryChangedEventArgs() { Country = exp.Country }); });
Now that we’ve finished constructing our object, we can invoke it like this to simulate our friend travelling around the world:
Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, " + $"{expando.Language}"); Console.WriteLine(); Console.WriteLine("Changing Country to France..."); expando.Country = "France"; Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, " + $"{expando.Language}"); Console.WriteLine(); Console.WriteLine("Changing Country to China..."); expando.Country = "China"; Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, " + $"{expando.Language}"); Console.WriteLine(); Console.WriteLine("Changing Country to Spain..."); expando.Country = "Spain"; Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, " + $"{expando.Language}"); Console.WriteLine();
The output of this example is shown here:
expando contains: Brian, USA, English Changing Country to France... Country changed to France, changing Language to French Setting Language to: French expando contains: Brian, France, French Changing Country to China... Country changed to China, changing Language to Mandarin Setting Language to: Mandarin expando contains: Brian, China, Mandarin Changing Country to Spain... Country changed to Spain, changing Language to Spanish Setting Language to: Spanish expando contains: Brian, Spain, Spanish
ExpandoObject
allows you to write code that is more readable than typical reflection code with GetProperty("Field")
syntax. When you’re dealing with XML or JSON, ExpandoObject
can be useful for quickly setting up a type to program against instead of always having to create data transfer objects. ExpandoObject
’s support for data binding through INotifyPropertyChanged
is a huge win for anyone using WPF, MVC, or any other binding framework in .NET, as it allows you to use these objects, as well as other statically typed classes, “on the fly.”
Since ExpandoObject
can take delegates as members, you can attach methods and events to these dynamic types while the code looks like you are addressing a static type:
public static void AddEvent(ExpandoObject expando, string eventName, Action<object, EventArgs> handler) { var expandoDict = expando as IDictionary<string, object>; if (expandoDict.ContainsKey(eventName)) expandoDict[eventName] = handler; else expandoDict.Add(eventName, handler); }
You might be wondering why we didn’t use extension methods for AddProperty
and AddEvent
. They both could hang off of ExpandoObject
and make the syntax even cleaner, right? Unfortunately, no. The way extension methods work is that the compiler does a search on all classes that might be a match for the extended class. This means that the DLR would have to know all of this information at runtime as well (since ExpandoObject
is handled by the DLR), and currently not all of that information is encoded into the call site for the class and methods.
The event argument classes for the LanguageChanged
and CountryChanged
events are listed here:
public class LanguageChangedEventArgs : EventArgs { public string Language { get; set; } } public class CountryChangedEventArgs : EventArgs { public string Country { get; set; } }
The “ExpandoObject class,” ”Func<> delegate,” “Action<> delegate,” and “INotifyPropertyChanged interface” topics in the MSDN documentation.
Use the DynamicBase<T>
class derived from DynamicObject
to create a new class or encapsulate an existing class:
public class DynamicBase<T> : DynamicObject where T : new() { private T _containedObject = default(T); [JsonExtensionData] //JSON.NET 5.0 and above private Dictionary<string, object> _dynamicMembers = new Dictionary<string, object>(); private List<PropertyInfo> _propertyInfos = new List<PropertyInfo>(typeof(T).GetProperties()); public DynamicBase() { } public DynamicBase(T containedObject) { _containedObject = containedObject; } public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { if (_dynamicMembers.ContainsKey(binder.Name) && _dynamicMembers[binder.Name] is Delegate) { result = (_dynamicMembers[binder.Name] as Delegate).DynamicInvoke( args); return true; } return base.TryInvokeMember(binder, args, out result); } public override IEnumerable<string> GetDynamicMemberNames() => _dynamicMembers.Keys; public override bool TryGetMember(GetMemberBinder binder, out object result) { result = null; var propertyInfo = _propertyInfos.Where(pi => pi.Name == binder.Name).FirstOrDefault(); // Make sure this member isn't a property on the object yet if (propertyInfo == null) { // look in the additional items collection for it if (_dynamicMembers.Keys.Contains(binder.Name)) { // return the dynamic item result = _dynamicMembers[binder.Name]; return true; } } else { // get it from the contained object if (_containedObject != null) { result = propertyInfo.GetValue(_containedObject); return true; } } return base.TryGetMember(binder, out result); } public override bool TrySetMember(SetMemberBinder binder, object value) { var propertyInfo = _propertyInfos.Where(pi => pi.Name == binder.Name).FirstOrDefault(); // Make sure this member isn't a property on the object yet if (propertyInfo == null) { // look in the additional items collection for it if (_dynamicMembers.Keys.Contains(binder.Name)) { // set the dynamic item _dynamicMembers[binder.Name] = value; return true; } else { _dynamicMembers.Add(binder.Name, value); return true; } } else { // put it in the contained object if (_containedObject != null) { propertyInfo.SetValue(_containedObject, value); return true; } } return base.TrySetMember(binder, value); } public override string ToString() { StringBuilder builder = new StringBuilder(); foreach (var propInfo in _propertyInfos) { if(_containedObject != null) builder.AppendFormat("{0}:{1}{2}", propInfo.Name, propInfo.GetValue(_containedObject), Environment.NewLine); else builder.AppendFormat("{0}:{1}{2}", propInfo.Name, propInfo.GetValue(this), Environment.NewLine); } foreach (var addlItem in _dynamicMembers) { // exclude methods that are added from the description Type itemType = addlItem.Value.GetType(); Type genericType = itemType.IsGenericType ? itemType.GetGenericTypeDefinition() : null; if (genericType != null) { if (genericType != typeof(Func<>) && genericType != typeof(Action<>)) builder.AppendFormat("{0}:{1}{2}", addlItem.Key, addlItem.Value, Environment.NewLine); } else builder.AppendFormat("{0}:{1}{2}", addlItem.Key, addlItem.Value, Environment.NewLine); } return builder.ToString(); } }
To understand how DynamicBase<T>
is used, consider a scenario where we have a web service that is receiving a serialized JSON payload of athlete information. Currently we have defined the DynamicAthlete
class with properties for both a Name
and a Sport
:
public class DynamicAthlete : DynamicBase<DynamicAthlete> { public string Name { get; set; } public string Sport { get; set; } }
In the payload being sent to us, the supplier has started to send additional information about the Position
the athlete plays. This can happen at times when legacy system integrations change and all systems cannot update at the same time. For our receiving system, we don’t want to lose the new data being sent from some systems. We simulate the construction of the JSON payload using dynamic
and the JSON.NET serializer available via NuGet (thank you, James Newton-King—this thing rocks!):
// Create a set of information on athletes // Note that the service receiving these doesn't have Position as a // property on the Athlete object dynamic initialAthletes = new[] { new { Name = "Tom Brady", Sport = "Football", Position = "Quarterback" }, new { Name = "Derek Jeter", Sport = "Baseball", Position = "Shortstop" }, new { Name = "Michael Jordan", Sport = "Basketball", Position = "Small Forward" }, new { Name = "Lionel Messi", Sport = "Soccer", Position = "Forward" } }; // serialize the JSON to send to a web service about athletes... string serializedAthletes = JsonNetSerialize(initialAthletes);
Assume the JSON payload for the athletes comes in to your service and is deserialized (once again, props to JSON.NET) and we deserialize it as an array of DynamicAthletes
:
// deserialize the JSON we were sent var athletes = JsonNetDeserialize<DynamicAthlete[]>(serializedAthletes);
Now, everyone who has done any kind of web service development (or any serialization development, for that matter) knows that if you don’t have a place to put things while deserializing, they get lost or cause errors. So what happens to the Position
property value that was passed in since that property is not declared on DynamicAthlete
? If you look back at the declaration of DynamicBase<T>
(from which DynamicAthlete
derives), you will see an internal private Dictionary<string,object>
that is marked with the JsonExtensionData
attribute. This attribute tells the serializer where to put property values that do not have a place in the derived object. How cool is that?! So our Position
value is stored in this internal dictionary, which is great, but how do we access it?
[JsonExtensionData] //JSON.NET 5.0 and above private Dictionary<string, object> _dynamicMembers = new Dictionary<string, object>();
Since our DynamicAthlete
is derived from DynamicBase<T>
, which in turn derives from DynamicObject
, we can assign the first athlete we received into the dynamic variable da
. Once it is in a dynamic variable, we can access Position
just as if it were one of the defined properties of DynamicAthlete
:
dynamic da = athletes[0]; Console.WriteLine($"Position of first athlete: {da.Position}");
So we can preserve the value of the properties sent to us even if we don’t know about them directly when we deploy the service, which is a nice robustness feature. We could also add a new method to each DynamicAthlete
to get the Name
in uppercase while printing out the contents we received:
// Inspect the athletes and see that we not only got the Position // information, but we can also add an operation to work on the // entity and invoke that as part of the dynamic entity foreach(var athlete in athletes) { dynamic dynamicAthlete = (dynamic)athlete; dynamicAthlete.GetUppercaseName = (Func<string>)(() => { return ((string)dynamicAthlete.Name).ToUpper(); }); Console.WriteLine($"Athlete:"); Console.WriteLine(athlete); Console.WriteLine($"Uppercase Name: {dynamicAthlete.GetUppercaseName()}"); Console.WriteLine(); Console.WriteLine(); }
GetUppercaseName
is added to the object and then called to return the uppercase version of the Name
. Here is the output:
Athlete: Name:Tom Brady Sport:Football Position:Quarterback Uppercase Name: TOM BRADY Athlete: Name:Derek Jeter Sport:Baseball Position:Shortstop Uppercase Name: DEREK JETER Athlete: Name:Michael Jordan Sport:Basketball Position:Small Forward Uppercase Name: MICHAEL JORDAN Athlete: Name:Lionel Messi Sport:Soccer Position:Forward Uppercase Name: LIONEL MESSI
What about the case where we already have our objects defined? How can we get in on this extension goodness? Let’s look at the StaticAthlete
class as an example:
public class StaticAthlete { public string Name { get; set; } public string Sport { get; set; } }
StaticAthlete
looks almost the same as DynamicAthlete
, but it is not derived from anything.
If we create an instance of StaticAthlete
, we can still use DynamicBase<T>
to wrap it and get the same extension behavior as we did when DynamicAthlete
was inheriting from DynamicBase<T>
. DynamicBase<T>
is no Super Bass-O-Matic ’76, but it slices and dices classes pretty well too!
//Wrap an existing athlete StaticAthlete staticAthlete = new StaticAthlete() { Sport = "Hockey" }; dynamic extendedAthlete = new DynamicBase<StaticAthlete>(staticAthlete); extendedAthlete.Name = "Bobby Orr"; extendedAthlete.Position = "Defenseman"; extendedAthlete.GetUppercaseName = (Func<string>)(() => { return ((string)extendedAthlete.Name).ToUpper(); }); Console.WriteLine($"Static Athlete (extended):"); Console.WriteLine(extendedAthlete); Console.WriteLine($"Uppercase Name: {extendedAthlete.GetUppercaseName()}"); Console.WriteLine(); Console.WriteLine();
You can see that the output for StaticAthlete
is exactly the same as it was for the DynamicAthletes
:
Static Athlete (extended): Name:Bobby Orr Sport:Hockey Position:Defenseman Uppercase Name: BOBBY ORR
DynamicObject
acts as a base class to help you add dynamic behaviors to your classes. Unlike ExpandoObject
it cannot be instantiated, but it can be derived from. With DynamicObject
, you can override many different types of operations, such as property or method access or any binary, unary, or type conversion operations, which allows you the flexibility to determine how the class will react at runtime.
We do some of these things in DynamicBase<T>
by overriding the following methods on DynamicObject
:
TryInvokeMember
GetDynamicMemberNames
TryGetMember
TrySetMember
TryInvokeMember
allows us to determine what should happen when a member is invoked on the object. We use it in DynamicBase<T>
to look at the internal collection and if we have a matching item, we invoke it dynamically as a delegate:
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { if (_dynamicMembers.ContainsKey(binder.Name) && _dynamicMembers[binder.Name] is Delegate) { result = (_dynamicMembers[binder.Name] as Delegate).DynamicInvoke( args); return true; } return base.TryInvokeMember(binder, args, out result); }
GetDynamicMemberNames
gets the set of all members that were added dynamically:
public override IEnumerable<string> GetDynamicMemberNames() { return _dynamicMembers.Keys; }
TryGetMember
is overridden to allow the caller to get property values for the items that have been added dynamically. If we don’t find it in the main property information for the class, we look in the internal dictionary of dynamic members and return it from there:
public override bool TryGetMember(GetMemberBinder binder, out object result) { result = null; var propertyInfo = _propertyInfos.Where(pi => pi.Name == binder.Name).FirstOrDefault(); // Make sure this member isn't a property on the object yet if (propertyInfo == null) { // look in the additional items collection for it if (_dynamicMembers.Keys.Contains(binder.Name)) { // return the dynamic item result = _dynamicMembers[binder.Name]; return true; } } else { // get it from the contained object if (_containedObject != null) { result = propertyInfo.GetValue(_containedObject); return true; } } return base.TryGetMember(binder, out result); }
The override for TrySetMember
handles when a property value is being set. Once again, we look at the typed object first and then look to the dynamic dictionary for where to store the value:
public override bool TrySetMember(SetMemberBinder binder, object value) { var propertyInfo = _propertyInfos.Where(pi => pi.Name == binder.Name).FirstOrDefault(); // Make sure this member isn't a property on the object yet if (propertyInfo == null) { // look in the additional items collection for it if (_dynamicMembers.Keys.Contains(binder.Name)) { // set the dynamic item _dynamicMembers[binder.Name] = value; return true; } else { _dynamicMembers.Add(binder.Name, value); return true; } } else { // put it in the contained object if (_containedObject != null) { propertyInfo.SetValue(_containedObject, value); return true; } } return base.TrySetMember(binder, value); }
We have also overridden ToString
so that we can get all of the properties (static and dynamic) on the class to be represented in the string:
public override string ToString() { StringBuilder builder = new StringBuilder(); foreach (var propInfo in _propertyInfos) { if(_containedObject != null) builder.AppendFormat("{0}:{1}{2}", propInfo.Name, propInfo.GetValue(_containedObject), Environment.NewLine); else builder.AppendFormat("{0}:{1}{2}", propInfo.Name, propInfo.GetValue(this), Environment.NewLine); } foreach (var addlItem in _dynamicMembers) { // exclude methods that are added from the description Type itemType = addlItem.Value.GetType(); Type genericType = itemType.IsGenericType ? itemType.GetGenericTypeDefinition() : null; if (genericType != null) { if (genericType != typeof(Func<>) && genericType != typeof(Action<>)) builder.AppendFormat("{0}:{1}{2}", addlItem.Key, addlItem.Value, Environment.NewLine); } else builder.AppendFormat("{0}:{1}{2}", addlItem.Key, addlItem.Value, Environment.NewLine); } return builder.ToString(); }
We do a bit of filtering to handle the cases where dynamic methods or events are added and when the member or method is on the contained object. This allows us to get the representation of all properties like this:
Name:Bobby Orr Sport:Hockey Position:Defenseman
As you can see, DynamicObject
gives you all the power you need to extend your objects as far as you want to take them.
The “DynamicObject Class” topic in the MSDN documentation.