Chapter 3. Application Domains, Reflection, and Metadata

The power and flexibility of the Microsoft .NET Framework is enhanced by the ability to inspect and manipulate types and metadata at runtime. The recipes in this chapter describe how to use application domains, reflection, and metadata. Specifically, the recipes in this chapter describe how to do the following:

  • Create application domains into which you can load assemblies that are isolated from the rest of your application (recipe 3-1)

  • Create types that have the capability to cross application domain boundaries (recipe 3-2) and types that are guaranteed to be unable to cross application domain boundaries (recipe 3-4)

  • Control the loading of assemblies and the instantiation of types in local and remote application domains (recipes 3-3, 3-5, 3-6, and 3-7)

  • Pass simple configuration data between application domains (recipe 3-8)

  • Unload application domains, which provides the only means through which you can unload assemblies at runtime (recipe 3-9)

  • Inspect and test the type of an object using a variety of mechanisms built into the C# language and capabilities provided by the objects themselves (recipes 3-10 and 3-11)

  • Dynamically instantiate an object and execute its methods at runtime using reflection (recipe 3-12)

  • Create custom attributes (recipe 3-13), allowing you to associate metadata with your program elements and inspect the value of those custom attributes at runtime (recipe 3-14)

  • Use reflection to discover type members and to invoke a member at runtime (recipes 3-15 and 3-16)

  • Use dynamic types to simplify invoking a member using reflection (recipe 3-17)

  • Create custom dynamic types (recipe 3-18)

Create an Application Domain

Problem

You need to create a new application domain.

Solution

Use the static method CreateDomain of the System.AppDomain class.

How It Works

The simplest overload of the CreateDomain method takes a single string argument specifying a human-readable name (friendly name) for the new application domain. Other overloads allow you to specify evidence and configuration settings for the new application domain. You specify evidence using a System.Security.Policy.Evidence object, and you specify configuration settings using a System.AppDomainSetup object.

The AppDomainSetup class is a container of configuration information for an application domain. Table 3-1 lists some of the properties of the AppDomainSetup class that you will use most often when creating application domains. These properties are accessible after creation through members of the AppDomain object. Some have different names, and some are modifiable at runtime; refer to the .NET Framework's software development kit (SDK) documentation on the AppDomain class for a comprehensive discussion.

Table 3.1. Commonly Used AppDomainSetup Properties

Property

Description

ApplicationBase

The directory where the CLR will look during probing to resolve private assemblies. (Recipe 3-5 discusses probing.) Effectively, ApplicationBase is the root directory for the executing application. By default, this is the directory containing the assembly. This is readable after creation using the AppDomain.BaseDirectory property.

ConfigurationFile

The name of the configuration file used by code loaded into the application domain. This is readable after creation using the AppDomain.GetData method with the key APP_CONFIG_FILE. By default, the configuration file is stored in the same folder as the application EXE file, but if you set ApplicationBase, it will be in that same folder.

DisallowPublisherPolicy

Controls whether the publisher policy section of the application configuration file is taken into consideration when determining which version of a strongly named assembly to bind to. Recipe 3-5 discusses publisher policy.

PrivateBinPath

A semicolon-separated list of directories that the runtime uses when probing for private assemblies. These directories are relative to the directory specified in ApplicationBase. This is readable after application domain creation using the AppDomain.RelativeSearchPath property.

The Code

The following code demonstrates the creation and initial configuration of an application domain:

using System;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_01
    {
        public static void Main()
        {
            // Instantiate an AppDomainSetup object.
            AppDomainSetup setupInfo = new AppDomainSetup();

            // Configure the application domain setup information.
            setupInfo.ApplicationBase = @"C:MyRootDirectory";
            setupInfo.ConfigurationFile = "MyApp.config";
            setupInfo.PrivateBinPath = "bin;plugins;external";

            // Create a new application domain passing null as the evidence
            // argument. Remember to save a reference to the new AppDomain as
            // this cannot be retrieved any other way.
            AppDomain newDomain =
                AppDomain.CreateDomain("My New AppDomain",null, setupInfo);

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Create Types That Can Be Passed Across Application Domain Boundaries

Problem

You need to pass objects across application domain boundaries as arguments or return values.

Solution

Use marshal-by-value or marshal-by-reference objects.

How It Works

The .NET Remoting system (discussed in Chapter 10) makes passing objects across application domain boundaries straightforward. However, to those unfamiliar with .NET Remoting, the results can be very different from those expected. In fact, the most confusing aspect of using multiple application domains stems from the interaction with .NET Remoting and the way objects traverse application domain boundaries.

All types fall into one of three categories: nonremotable, marshal-by-value (MBV), or marshal-by-reference (MBR). Nonremotable types cannot cross application domain boundaries and cannot be used as arguments or return values in cross-application domain calls. Recipe 3-4 discusses nonremotable types.

MBV types are serializable types. When you pass an MBV object across an application domain boundary as an argument or a return value, the .NET Remoting system serializes the object's current state, passes it to the destination application domain, and creates a new copy of the object with the same state as the original. This results in a copy of the MBV object existing in both application domains. The content of the two instances are initially identical, but they are independent; changes made to one instance are not reflected in the other instance (this applies to static members as well). This often causes confusion as you try to update the remote object but are in fact updating the local copy. If you actually want to be able to call and change an object from a remote application domain, the object needs to be an MBR type.

MBR types are those classes that derive from System.MarshalByRefObject. When you pass an MBR object across an application domain boundary as an argument or a return value, the .NET Remoting system creates a proxy in the destination application domain that represents the remote MBR object. To any class in the destination application domain, the proxy looks and behaves like the remote MBR object that it represents. In reality, when a call is made against the proxy, the .NET Remoting system transparently passes the call and its arguments to the remote application domain and issues the call against the original object. Any results are passed back to the caller via the proxy. Figure 3-1 illustrates the relationship between an MBR object and the objects that access it across application domains via a proxy.

An MBR object is accessed across application domains via a proxy.

Figure 3.1. An MBR object is accessed across application domains via a proxy.

The Code

The following example highlights (in bold) the fundamental difference between creating classes that are passed by value (Recipe03_02MBV) and those passed by reference (Recipe03_02MBR). The code creates a new application domain and instantiates two remotable objects in it (discussed further in recipe 3-7). However, because the Recipe03_02MBV object is an MBV object, when it is created in the new application domain, it is serialized, passed across the application domain boundary, and deserialized as a new independent object in the caller's application domain. Therefore, when the code retrieves the name of the application domain hosting each object, Recipe03_02MBV returns the name of the main application domain, and Recipe03_02MBR returns the name of the new application domain in which it was created.

using System;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    // Declare a class that is passed by value.
    [Serializable]
    public class Recipe03_02MBV
    {
public string HomeAppDomain
        {
            get
            {
                return AppDomain.CurrentDomain.FriendlyName;
            }
        }
    }

    // Declare a class that is passed by reference.
    public class Recipe03_02MBR: MarshalByRefObject
    {
        public string HomeAppDomain
        {
            get
            {
                return AppDomain.CurrentDomain.FriendlyName;
            }
        }
    }

    public class Recipe03_02
    {
        public static void Main(string[] args)
        {
            // Create a new application domain.
            AppDomain newDomain =
                AppDomain.CreateDomain("My New AppDomain");

            // Instantiate an MBV object in the new application domain.
            Recipe03_02MBV mbvObject =
                (Recipe03_02MBV)newDomain.CreateInstanceFromAndUnwrap(
                    "Recipe03-02.exe",
                    "Apress.VisualCSharpRecipes.Chapter03.Recipe03_02MBV");

            // Instantiate an MBR object in the new application domain.
            Recipe03_02MBR mbrObject =
                (Recipe03_02MBR)newDomain.CreateInstanceFromAndUnwrap(
                    "Recipe03-02.exe",
                    "Apress.VisualCSharpRecipes.Chapter03.Recipe03_02MBR");

            // Display the name of the application domain in which each of
            // the objects is located.
            Console.WriteLine("Main AppDomain = {0}",
                AppDomain.CurrentDomain.FriendlyName);
            Console.WriteLine("AppDomain of MBV object = {0}",
                mbvObject.HomeAppDomain);
            Console.WriteLine("AppDomain of MBR object = {0}",
                mbrObject.HomeAppDomain);
// Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Note

Recipe 13-1 provides more details on creating serializable types, and recipe 10-16 describes how to create remotable types.

Avoid Loading Unnecessary Assemblies into Application Domains

Problem

You need to pass an object reference across multiple application domain boundaries; however, to conserve memory and avoid impacting performance, you want to ensure that the Common Language Runtime (CLR) loads only the object's type metadata into the application domains where it is required (that is, where you will actually use the object).

Solution

Wrap the object reference in a System.Runtime.Remoting.ObjectHandle, and unwrap the object reference only when you need to access the object.

How It Works

When you pass an MBV object across application domain boundaries, the runtime creates a new instance of that object in the destination application domain. This means the runtime must load the assembly containing that type metadata into the application domain. Passing MBV references across intermediate application domains can result in the runtime loading unnecessary assemblies into application domains. Once loaded, these superfluous assemblies cannot be unloaded without unloading the containing application domain. (See recipe 3-9 for more information.)

The ObjectHandle class allows you to wrap an object reference so that you can pass it between application domains without the runtime loading additional assemblies. When the object reaches the destination application domain, you can unwrap the object reference, causing the runtime to load the required assembly and allowing you to access the object as usual.

The Code

The following code contains some simple methods that demonstrate how to wrap and unwrap a System.Data.DataSet using an ObjectHandle:

using System;
using System.Data;
using System.Runtime.Remoting;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_03
    {
        // A method to wrap a DataSet.
        public static ObjectHandle WrapDataSet(DataSet ds)
        {
            // Wrap the DataSet.
            ObjectHandle objHandle = new ObjectHandle(ds);

            // Return the wrapped DataSet.
            return objHandle;
        }

        // A method to unwrap a DataSet.
        public static DataSet UnwrapDataSet(ObjectHandle handle)
        {
            // Unwrap the DataSet.
            DataSet ds = (System.Data.DataSet)handle.Unwrap();

            // Return the wrapped DataSet.
            return ds;
        }

        public static void Main()
        {
            DataSet ds = new DataSet();
            Console.WriteLine(ds.ToString());

            ObjectHandle oh = WrapDataSet(ds);
            DataSet ds2 = UnwrapDataSet(oh);
            Console.WriteLine(ds2.ToString());

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Create a Type That Cannot Cross Application Domain Boundaries

Problem

You need to create a type so that instances of the type are inaccessible to code in other application domains.

Solution

Ensure the type is nonremotable by making sure it is not serializable and it does not derive from the MarshalByRefObject class.

How It Works

On occasion, you will want to ensure that instances of a type cannot transcend application domain boundaries. To create a nonremotable type, ensure that it isn't serializable and that it doesn't derive (directly or indirectly) from the MarshalByRefObject class. If you take these steps, you ensure that an object's state can never be accessed from outside the application domain in which the object was instantiated—such objects cannot be used as arguments or return values in cross–application domain method calls.

Ensuring that a type isn't serializable is easy because a class doesn't inherit the ability to be serialized from its parent class. To ensure that a type isn't serializable, make sure it does not have System.SerializableAttribute applied to the type declaration.

Ensuring that a class cannot be passed by reference requires a little more attention. Many classes in the .NET class library derive directly or indirectly from MarshalByRefObject; you must be careful you don't inadvertently derive your class from one of these. Commonly used base classes that derive from MarshalByRefObject include System.ComponentModel.Component, System.IO.Stream, System.IO.TextReader, System.IO.TextWriter, System.NET.WebRequest, and System.Net.WebResponse. (Check the .NET Framework SDK documentation on MarshalByRefObject. The inheritance hierarchy listed for the class provides a complete list of classes that derive from it.)

Load an Assembly into the Current Application Domain

Problem

You need to load an assembly at runtime into the current application domain.

Solution

Use the static Load method or the LoadFrom method of the System.Reflection.Assembly class.

How It Works

The CLR will only load the assemblies identified at build time as being referenced by your assembly when the metadata for their contained types is required. However, you can also explicitly instruct the runtime to load assemblies. The Load and LoadFrom methods both result in the runtime loading an assembly into the current application domain, and both return an Assembly instance that represents the newly loaded assembly. The differences between each method are the arguments you must provide to identify the assembly to load and the process that the runtime undertakes to locate the specified assembly.

The Load method provides overloads that allow you to specify the assembly to load using one of the following:

  • A string containing the fully or partially qualified display name of the assembly

  • A System.Reflection.AssemblyName containing details of the assembly

  • A byte array containing the raw bytes that constitute the assembly

A fully qualified display name contains the assembly's text name, version, culture, and public key token, separated by commas (for example, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089). To specify an assembly that doesn't have a strong name, use PublicKeyToken=null. You can also specify a partial display name, but as a minimum, you must specify the assembly name (without the file extension).

In response to the Load call, the runtime undertakes an extensive process to locate and load the specified assembly. The following is a summary; consult the section "How the Runtime Locates Assemblies" in the .NET Framework SDK documentation for more details:

  1. If you specify a strongly named assembly, the Load method will apply the version policy and publisher policy to enable requests for one version of an assembly to be satisfied by another version. You specify the version policy in your machine or application configuration file using <bindingRedirect> elements. You specify the publisher policy in special resource assemblies installed in the Global Assembly Cache (GAC).

  2. Once the runtime has established the correct version of an assembly to use, it attempts to load strongly named assemblies from the GAC.

  3. If the assembly is not strongly named or is not found in the GAC, the runtime looks for applicable <codeBase> elements in your machine and application configuration files. A <codeBase> element maps an assembly name to a file or a uniform resource locator (URL). If the assembly is strongly named, <codeBase> can refer to any location, including Internet-based URLs; otherwise, <codeBase> must refer to a directory relative to the application directory. If the assembly doesn't exist at the specified location, Load throws a System.IO.FileNotFoundException.

  4. If no <codeBase> elements are relevant to the requested assembly, the runtime will locate the assembly using probing. Probing looks for the first file with the assembly's name (with either a .dll or an .exe extension) in the following locations:

    • The application root directory

    • Directories under the application root that match the assembly's name and culture

    • Directories under the application root that are specified in the private binpath

The Load method is the easiest way to locate and load assemblies but can also be expensive in terms of processing if the runtime needs to start probing many directories for a weakly named assembly. The LoadFrom method allows you to load an assembly from a specific location. If the specified file isn't found, the runtime will throw a FileNotFoundException. The runtime won't attempt to locate the assembly in the same way as the Load method—LoadFrom provides no support for the GAC, policies, <codebase> elements, or probing.

The Code

The following code demonstrates various forms of the Load and LoadFrom methods. Notice that unlike the Load method, LoadFrom requires you to specify the extension of the assembly file.

using System;
using System.Reflection;
using System.Globalization;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_05
    {
        public static void ListAssemblies()
        {
            // Get an array of the assemblies loaded into the current
            // application domain.
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

            foreach (Assembly a in assemblies)
            {
                Console.WriteLine(a.GetName());
            }
        }

        public static void Main()
        {
            // List the assemblies in the current application domain.
            Console.WriteLine("**** BEFORE ****");
            ListAssemblies();

            // Load the System.Data assembly using a fully qualified display name.
            string name1 = "System.Data,Version=2.0.0.0," +
                "Culture=neutral,PublicKeyToken=b77a5c561934e089";
            Assembly a1 = Assembly.Load(name1);
// Load the System.Xml assembly using an AssemblyName.
            AssemblyName name2 = new AssemblyName();
            name2.Name = "System.Xml";
            name2.Version = new Version(2, 0, 0, 0);
            name2.CultureInfo = new CultureInfo("");    //Neutral culture.
            name2.SetPublicKeyToken(
                new byte[] {0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89});
            Assembly a2 = Assembly.Load(name2);

            // Load the SomeAssembly assembly using a partial display name.
            Assembly a3 = Assembly.Load("SomeAssembly");

            // Load the assembly named c:sharedMySharedAssembly.dll.
            Assembly a4 = Assembly.LoadFrom(@"c:sharedMySharedAssembly.dll");

            // List the assemblies in the current application domain.
            Console.WriteLine("

**** AFTER ****");
            ListAssemblies();

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Execute an Assembly in a Different Application Domain

Problem

You need to execute an assembly in an application domain other than the current one.

Solution

Call the ExecuteAssembly or ExecuteAssemlyByName method of the AppDomain object that represents the application domain, and specify the file name of an executable assembly.

How It Works

If you have an executable assembly that you want to load and run in an application domain, the ExecuteAssembly or ExecuteAssemblyByName methods provides the easiest solution. The ExecuteAssembly method provides several overloads. The simplest overload takes only a string containing the name of the executable assembly to run; you can specify a local file or a URL. Other overloads allow you to specify arguments to pass to the assembly's entry point (equivalent to command-line arguments).

The ExecuteAssembly method loads the specified assembly and executes the method defined in metadata as the assembly's entry point (usually the Main method). If the specified assembly isn't executable, ExecuteAssembly throws a System.MissingMethodException. The CLR doesn't start execution of the assembly in a new thread, so control won't return from the ExecuteAssembly method until the newly executed assembly exits. Because the ExecuteAssembly method loads an assembly using partial information (only the file name), the CLR won't use the GAC or probing to resolve the assembly. (See recipe 3-5 for more information.)

The ExecuteAssemblyByName method provides a similar set of overloads and takes the same argument types, but instead of taking just the file name of the executable assembly, it gets passed the display name of the assembly. This overcomes the limitations inherent in ExecuteAssembly as a result of supplying only partial names. Again, see recipe 3-5 for more information on the structure of assembly display names.

The Code

The following code demonstrates how to use the ExecuteAssembly method to load and run an assembly. The Recipe03-06 class creates an AppDomain and executes itself in that AppDomain using the ExecuteAssembly method. This results in two copies of the Recipe03-06 assembly loaded into two different application domains.

using System;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_06
    {
        public static void Main(string[] args)
        {
            // For the purpose of this example, if this assembly is executing
            // in an AppDomain with the friendly name "NewAppDomain", do not
            // create a new AppDomain. This avoids an infinite loop of
            // AppDomain creation.
            if (AppDomain.CurrentDomain.FriendlyName != "NewAppDomain")
            {
                // Create a new application domain.
                AppDomain domain = AppDomain.CreateDomain("NewAppDomain");

                // Execute this assembly in the new application domain and
                // pass the array of command-line arguments.
                domain.ExecuteAssembly("Recipe03-06.exe", args);
            }

            // Display the command-line arguments to the screen prefixed with
            // the friendly name of the AppDomain.
            foreach (string s in args)
            {
                Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + " : " + s);
            }
// Wait to continue.
            if (AppDomain.CurrentDomain.FriendlyName != "NewAppDomain")
            {
                Console.WriteLine("
Main method complete. Press Enter.");
                Console.ReadLine();
            }
        }
    }
}

Usage

If you run Recipe03-06 using the following command:

Recipe03-06 Testing AppDomains

you will see that the command-line arguments are listed from both the existing and new application domains:

NewAppDomain : Testing

NewAppDomain : AppDomains

Recipe03-06.exe : Testing

Recipe03-06.exe : AppDomains

Instantiate a Type in a Different Application Domain

Problem

You need to instantiate a type in an application domain other than the current one.

Solution

Call the CreateInstance method or the CreateInstanceFrom method of the AppDomain object that represents the target application domain.

How It Works

The ExecuteAssembly method discussed in recipe 3-6 is straightforward to use, but when you are developing sophisticated applications that use application domains, you are likely to want more control over loading assemblies, instantiating types, and invoking object members within the application domain.

The CreateInstance and CreateInstanceFrom methods provide a variety of overloads that offer fine-grained control over the process of object instantiation. The simplest overloads assume the use of a type's default constructor, but both methods implement overloads that allow you to provide arguments to use any constructor.

The CreateInstance method loads a named assembly into the application domain using the process described for the Assembly.Load method in recipe 3-5. CreateInstance then instantiates a named type and returns a reference to the new object wrapped in an ObjectHandle (described in recipe 3-3). The CreateInstanceFrom method also instantiates a named type and returns an ObjectHandle-wrapped object reference; however, CreateInstanceFrom loads the specified assembly file into the application domain using the process described in recipe 3-5 for the Assembly.LoadFrom method.

AppDomain also provides two convenience methods named CreateInstanceAndUnwrap and CreateInstanceFromAndUnwrap that automatically extract the reference of the instantiated object from the returned ObjectHandle object; you must cast the returned object to the correct type.

Warning

Be aware that if you use CreateInstance or CreateInstanceFrom to instantiate MBV types in another application domain, the object will be created, but the returned object reference won't refer to that object. Because of the way MBV objects cross application domain boundaries, the reference will refer to a copy of the object created automatically in the local application domain. Only if you create an MBR type will the returned reference refer to the object in the other application domain. (See recipe 3-2 for more details about MBV and MBR types.)

A common technique to simplify the management of application domains is to use a controller class. A controller class is a custom MBR type. You create an application domain and then instantiate your controller class in the application domain using CreateInstance. The controller class implements the functionality required by your application to manipulate the application domain and its contents. This could include loading assemblies, creating further application domains, cleaning up prior to deleting the application domain, or enumerating program elements (something you cannot normally do from outside an application domain). It is best to create your controller class in an assembly of its own to avoid loading unnecessary classes into each application domain. You should also be careful about what types you pass as return values from your controller to your main application domain to avoid loading additional assemblies.

The Code

The following code demonstrates how to use a simplified controller class named PluginManager. When instantiated in an application domain, PluginManager allows you to instantiate classes that implement the IPlugin interface, start and stop those plug-ins, and return a list of currently loaded plug-ins.

using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    // A common interface that all plug-ins must implement.
    public interface IPlugin
    {
        void Start();
        void Stop();
    }

    // A simple IPlugin implementation to demonstrate the PluginManager
    // controller class.
    public class SimplePlugin : IPlugin
    {
        public void Start()
        {
            Console.WriteLine(AppDomain.CurrentDomain.FriendlyName +
                ": SimplePlugin starting...");
        }

        public void Stop()
        {
            Console.WriteLine(AppDomain.CurrentDomain.FriendlyName +
                ": SimplePlugin stopping...");
        }
    }

    // The controller class, which manages the loading and manipulation
    // of plug-ins in its application domain.
    public class PluginManager : MarshalByRefObject
    {
        // A Dictionary to hold keyed references to IPlugin instances.
        private Dictionary<string, IPlugin> plugins =
            new Dictionary<string, IPlugin> ();

        // Default constructor.
        public PluginManager() { }

        // Constructor that loads a set of specified plug-ins on creation.
        public PluginManager(NameValueCollection pluginList)
        {
            // Load each of the specified plug-ins.
            foreach (string plugin in pluginList.Keys)
            {
                this.LoadPlugin(pluginList[plugin], plugin);
            }
        }

        // Load the specified assembly and instantiate the specified
// IPlugin implementation from that assembly.
        public bool LoadPlugin(string assemblyName, string pluginName)
        {
            try
            {
                // Load the named private assembly.
                Assembly assembly = Assembly.Load(assemblyName);

                // Create the IPlugin instance, ignore case.
                IPlugin plugin = assembly.CreateInstance(pluginName, true)
                    as IPlugin;

                if (plugin != null)
                {
                    // Add new IPlugin to ListDictionary.
                    plugins[pluginName] = plugin;

                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch
            {
                // Return false on all exceptions for the purpose of
                // this example. Do not suppress exceptions like this
                // in production code.
                return false;
            }
        }

        public void StartPlugin(string plugin)
        {
            try
            {
                // Extract the IPlugin from the Dictionary and call Start.
                plugins[plugin].Start();
            }
            catch
            {
                // Log or handle exceptions appropriately.
            }
        }
public void StopPlugin(string plugin)
        {
            try
            {
                // Extract the IPlugin from the Dictionary and call Stop.
                plugins[plugin].Stop();
            }
            catch
            {
                // Log or handle exceptions appropriately.
            }
        }

        public ArrayList GetPluginList()
        {
            // Return an enumerable list of plug-in names. Take the keys
            // and place them in an ArrayList, which supports marshal-by-value.
            return new ArrayList(plugins.Keys);
        }
    }

    class Recipe03_07
    {
        public static void Main()
        {
            // Create a new application domain.
            AppDomain domain1 = AppDomain.CreateDomain("NewAppDomain1");

            // Create a PluginManager in the new application domain using
            // the default constructor.
            PluginManager manager1 =
                (PluginManager)domain1.CreateInstanceAndUnwrap("Recipe03-07",
                "Apress.VisualCSharpRecipes.Chapter03.PluginManager");

            // Load a new plugin into NewAppDomain1.
            manager1.LoadPlugin("Recipe03-07",
                "Apress.VisualCSharpRecipes.Chapter03.SimplePlugin");

            // Start and stop the plug-in in NewAppDomain1.
            manager1.StartPlugin(
                "Apress.VisualCSharpRecipes.Chapter03.SimplePlugin");
            manager1.StopPlugin(
                "Apress.VisualCSharpRecipes.Chapter03.SimplePlugin");

            // Create a new application domain.
            AppDomain domain2 = AppDomain.CreateDomain("NewAppDomain2");

            // Create a ListDictionary containing a list of plug-ins to create.
            NameValueCollection pluginList = new NameValueCollection();
            pluginList["Apress.VisualCSharpRecipes.Chapter03.SimplePlugin"] =
                "Recipe03-07";
// Create a PluginManager in the new application domain and
            // specify the default list of plug-ins to create.
            PluginManager manager2 = (PluginManager)domain1.CreateInstanceAndUnwrap(
                "Recipe03-07", "Apress.VisualCSharpRecipes.Chapter03.PluginManager",
                true, 0, null, new object[] { pluginList }, null, null);

            // Display the list of plug-ins loaded into NewAppDomain2.
            Console.WriteLine("
Plugins in NewAppDomain2:");
            foreach (string s in manager2.GetPluginList())
            {
                Console.WriteLine(" - " + s);
            }

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Pass Data Between Application Domains

Problem

You need a simple mechanism to pass general configuration or state data between application domains.

Solution

Use the SetData and GetData methods of the AppDomain class.

How It Works

You can pass data between application domains as arguments and return values when you invoke the methods and properties of objects that exist in other application domains. However, at times it is useful to pass data between application domains in such a way that the data is easily accessible by all code within the application domain.

Every application domain maintains a data cache that contains a set of name/value pairs. Most of the cache content reflects configuration settings of the application domain, such as the values from the AppDomainSetup object provided during application domain creation. (See recipe 3-1 for more information.) You can also use this data cache as a mechanism to exchange data between application domains or as a simple state storage mechanism for code running within the application domain.

The SetData method allows you to associate a string key with an object and store it in the application domain's data cache. The GetData method allows you to retrieve an object from the data cache using the key. If code in one application domain calls the SetData method or the GetData method to access the data cache of another application domain, the data object must support MBV or MBR semantics, or a System.Runtime.Serialization.SerializationException will be thrown. (See recipe 3-3 for details on the characteristics required to allow objects to transcend application domain boundaries.)

When using the SetData or GetData methods to exchange data between application domains, you should avoid using the following keys, which are already used by the .NET Framework:

  • APP_CONFIG_FILE

  • APP_NAME

  • APPBASE

  • APP_CONFIG_BLOB

  • BINPATH_PROBE_ONLY

  • CACHE_BASE

  • CODE_DOWNLOAD_DISABLED

  • DEV_PATH

  • DYNAMIC_BASE

  • DISALLOW_APP

  • DISALLOW_APP_REDIRECTS

  • DISALLOW_APP_BASE_PROBING

  • FORCE_CACHE_INSTALL

  • LICENSE_FILE

  • PRIVATE_BINPATH

  • SHADOW_COPY_DIRS

The Code

The following example demonstrates how to use the SetData and GetData methods by passing a System.Collections.ArrayList between two application domains. After passing a list of pets to a second application domain for modification, the application displays both the original and modified lists. Notice that the code running in the second application domain does not modify the original list because ArrayList is a pass-by-value type, meaning that the second application domain only has a copy of the original list. (See recipe 3-2 for more details.)

using System;
using System.Reflection;
using System.Collections;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    public class ListModifier
    {
public ListModifier()
        {
            // Get the list from the data cache.
            ArrayList list = (ArrayList)AppDomain.CurrentDomain.GetData("Pets");

            // Modify the list.
            list.Add("turtle");
        }
    }

    class Recipe03_08
    {
        public static void Main()
        {
            // Create a new application domain.
            AppDomain domain = AppDomain.CreateDomain("Test");

            // Create an ArrayList and populate with information.
            ArrayList list = new ArrayList();
            list.Add("dog");
            list.Add("cat");
            list.Add("fish");

            // Place the list in the data cache of the new application domain.
            domain.SetData("Pets", list);

            // Instantiate a ListModifier in the new application domain.
            domain.CreateInstance("Recipe03-08",
                "Apress.VisualCSharpRecipes.Chapter03.ListModifier");

            // Display the contents of the original list.
            Console.WriteLine("Original list contents:");
            foreach (string s in list)
            {
                Console.WriteLine("  - " + s);
            }

            // Get the list and display its contents.
            Console.WriteLine("
Modified list contents:");
            foreach (string s in (ArrayList)domain.GetData("Pets"))
            {
                Console.WriteLine("  - " + s);
            }

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Unload Assemblies and Application Domains

Problem

You need to unload assemblies or application domains at runtime.

Solution

You have no way to unload individual assemblies from a System.AppDomain. You can unload an entire application domain using the static AppDomain.Unload method, which has the effect of unloading all assemblies loaded into the application domain.

How It Works

The only way to unload an assembly is to unload the application domain in which the assembly is loaded. Unfortunately, unloading an application domain will unload all the assemblies that have been loaded into it. This might seem like a heavy-handed and inflexible approach, but with appropriate planning of your application domain, the assembly loading structure, and the runtime dependency of your code on that application domain, it is not overly restrictive.

You unload an application domain using the static AppDomain.Unload method and passing it an AppDomain reference to the application domain you want to unload. You cannot unload the default application domain created by the CLR at startup.

The Unload method stops any new threads from entering the specified application domain and calls the Thread.Abort method on all threads currently active in the application domain. If the thread calling the Unload method is currently running in the specified application domain (making it the target of a Thread.Abort call), a new thread starts in order to carry out the unload operation. If a problem is encountered unloading an application domain, the thread performing the unload operation throws a System.CannotUnloadAppDomainException.

While an application domain is unloading, the CLR calls the finalization method of all objects in the application domain. Depending on the number of objects and nature of their finalization methods, this can take an variable amount of time. The AppDomain.IsFinalizingForUnload method returns true if the application domain is unloading and the CLR has started to finalize contained objects; otherwise, it returns false.

The Code

This code fragment demonstrates the syntax of the Unload method:

// Create a new application domain.
AppDomain newDomain = AppDomain.CreateDomain("New Domain");

// Load assemblies into the application domain.
...

// Unload the new application domain.
AppDomain.Unload(newDomain);

Retrieve Type Information

Problem

You need to obtain a System.Type object that represents a specific type.

Solution

Use one of the following:

  • The typeof operator

  • The static GetType method of the System.Type class

  • The GetType method of an existing instance of the type

  • The GetNestedType or GetNestedTypes method of the Type class

  • The GetType or GetTypes method of the Assembly class

  • The GetType, GetTypes, or FindTypes method of the System.Reflection.Module class

How It Works

The Type class provides a starting point for working with types using reflection. A Type object allows you to inspect the metadata of the type, obtain details of the type's members, and create instances of the type. Because of its importance, the .NET Framework provides a variety of mechanisms for obtaining references to Type objects.

One method of obtaining a Type object for a specific type is to use the typeof operator shown here:

System.Type t1 = typeof(System.Text.StringBuilder);

The type name is not enclosed in quotes and must be resolvable by the compiler (meaning you must reference the assembly using a compiler switch). Because the reference is resolved at compile time, the assembly containing the type becomes a static dependency of your assembly and will be listed as such in your assembly's manifest.

An alternative to the typeof operator is the static method Type.GetType, which takes a string containing the type name. Because you use a string to specify the type, you can vary it at runtime, which opens the door to a world of dynamic programming opportunities using reflection (see recipe 3-12). If you specify just the type name, the runtime must be able to locate the type in an already loaded assembly. Alternatively, you can specify an assembly-qualified type name. Refer to the .NET Framework SDK documentation for the Type.GetType method for a complete description of how to structure assembly-qualified type names. Table 3-2 summarizes some other methods that provide access to Type objects.

Table 3.2. Methods That Return Type Objects

Method

Description

Type.GetNestedType

Gets a specified type declared as a nested type within the existing Type object.

Type.GetNestedTypes

Gets an array of Type objects representing the nested types declared within the existing Type object.

Assembly.GetType

Gets a Type object for the specified type declared within the assembly.

Assembly.GetTypes

Gets an array of Type objects representing the types declared within the assembly.

Module.GetType

Gets a Type object for the specified type declared within the module. (See recipe 1-3 for a discussion of modules.)

Module.GetTypes

Gets an array of Type objects representing the types declared within the module.

Module.FindTypes

Gets a filtered array of Type objects representing the types declared within the module. The types are filtered using a delegate that determines whether each Type should appear in the final array.

The Code

The following example demonstrates how to use typeof and the GetType method to return a Type object for a named type and from existing objects:

using System;
using System.Text;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_10
    {
        public static void Main()
        {
            // Obtain type information using the typeof operator.
            Type t1 = typeof(StringBuilder);
            Console.WriteLine(t1.AssemblyQualifiedName);

            // Obtain type information using the Type.GetType method.
            // Case-sensitive, return null if not found.
            Type t2 = Type.GetType("System.String");
            Console.WriteLine(t2.AssemblyQualifiedName);
// Case-sensitive, throw TypeLoadException if not found.
            Type t3 = Type.GetType("System.String", true);
            Console.WriteLine(t3.AssemblyQualifiedName);

            // Case-insensitive, throw TypeLoadException if not found.
            Type t4 = Type.GetType("system.string", true, true);
            Console.WriteLine(t4.AssemblyQualifiedName);

            // Assembly-qualifed type name.
            Type t5 = Type.GetType("System.Data.DataSet,System.Data," +
                "Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089");
            Console.WriteLine(t5.AssemblyQualifiedName);

            // Obtain type information using the Object.GetType method.
            StringBuilder sb = new StringBuilder();
            Type t6 = sb.GetType();
            Console.WriteLine(t6.AssemblyQualifiedName);

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Test an Object's Type

Problem

You need to test the type of an object.

Solution

Use the inherited Object.GetType method to obtain a Type for the object. You can also use the is and as operators to test an object's type.

How It Works

All types inherit the GetType method from the Object base class. As discussed in recipe 3-10, this method returns a Type reference representing the type of the object. The runtime maintains a single instance of Type for each type loaded, and all references for this type refer to this same object. This means you can compare two type references efficiently. For convenience, C# provides the is operator as a quick way to check whether an object is a specified type. In addition, is will return true if the tested object is derived from the specified class.

Both of these approaches require that the type used with the typeof and is operators be known and resolvable at compile time. A more flexible (but slower) alternative is to use the Type.GetType method to return a Type reference for a named type. The Type reference is not resolved until runtime, which causes the performance hit but allows you to change the type comparison at runtime based on the value of a string.

Finally, you can use the as operator to perform a safe cast of any object to a specified type. Unlike a standard cast, which triggers a System.InvalidCastException if the object cannot be cast to the specified type, the as operator returns null. This allows you to perform safe casts that are easy to verify, but the compared type must be resolvable at runtime.

Note

The runtime will usually maintain more than one instance of each type depending on how assemblies are loaded into application domains. Usually, an assembly will be loaded into a specific application domain, meaning a Type instance will exist in each application domain in which the assembly is loaded. However, assemblies can also be loaded by a runtime host in a domain-neutral configuration, which means the assembly's type metadata (and Type instances) is shared across all application domains. By default, only the mscorlib assembly is loaded in a domain-neutral configuration.

The Code

The following example demonstrates the various type-testing alternatives described in this recipe:

using System;
using System.IO;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_11
    {
        // A method to test whether an object is an instance of a type
        // or a derived type.
        public static bool IsType(object obj, string type)
        {
            // Get the named type, use case-insensitive search, throw
            // an exception if the type is not found.
            Type t = Type.GetType(type, true, true);

            return t == obj.GetType() || obj.GetType().IsSubclassOf(t);
        }

        public static void Main()
        {
            // Create a new StringReader for testing.
            Object someObject = new StringReader("This is a StringReader");
// Test if someObject is a StringReader by obtaining and
            // comparing a Type reference using the typeof operator.
            if (typeof(StringReader) == someObject.GetType())
            {
                Console.WriteLine("typeof: someObject is a StringReader");
            }

            // Test if someObject is, or is derived from, a TextReader
            // using the is operator.
            if (someObject is TextReader)
            {
                Console.WriteLine(
                    "is: someObject is a TextReader or a derived class");
            }

            // Test if someObject is, or is derived from, a TextReader using
            // the Type.GetType and Type.IsSubclassOf methods.
            if (IsType(someObject, "System.IO.TextReader"))
            {
                Console.WriteLine("GetType: someObject is a TextReader");
            }

            // Use the "as" operator to perform a safe cast.
            StringReader reader = someObject as StringReader;
            if (reader != null)
            {
                Console.WriteLine("as: someObject is a StringReader");
            }

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Tip

The static method GetUnderlyingType of the System.Enum class allows you to retrieve the underlying type of an enumeration.

Instantiate an Object Using Reflection

Problem

You need to instantiate an object at runtime using reflection.

Solution

Obtain a Type object representing the type of object you want to instantiate, call its GetConstructor method to obtain a System.Reflection.ConstructorInfo object representing the constructor you want to use, and execute the ConstructorInfo.Invoke method.

How It Works

The first step in creating an object using reflection is to obtain a Type object that represents the type you want to instantiate. (See recipe 3-10 for details.) Once you have a Type instance, call its GetConstructor method to obtain a ConstructorInfo representing one of the type's constructors. The most commonly used overload of the GetConstructor method takes a Type array argument and returns a ConstructorInfo representing the constructor that takes the number, order, and type of arguments specified in the Type array. To obtain a ConstructorInfo representing a parameterless (default) constructor, pass an empty Type array (use the static field Type.EmptyTypes or new Type[0]); don't use null, or else GetConstructor will throw a System.ArgumentNullException. If GetConstructor cannot find a constructor with a signature that matches the specified arguments, it will return null.

Once you have the desired ConstructorInfo, call its Invoke method. You must provide an object array containing the arguments you want to pass to the constructor. Invoke instantiates the new object and returns an object reference to it, which you must cast to the appropriate type.

Reflection functionality is commonly used to implement factories in which you use reflection to instantiate concrete classes that either extend a common base class or implement a common interface. Often, both an interface and a common base class are used. The abstract base class implements the interface and any common functionality, and then each concrete implementation extends the base class.

No mechanism exists to formally declare that each concrete class must implement constructors with specific signatures. If you intend third parties to implement concrete classes, your documentation must specify the constructor signature called by your factory. A common approach to avoiding this problem is to use a default (empty) constructor and configure the object after instantiation using properties and methods.

The Code

The following code fragment demonstrates how to instantiate a System.Text.StringBuilder object using reflection and how to specify the initial content for the StringBuilder (a string) and its capacity (an int):

using System;
using System.Text;
using System.Reflection;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_12
    {
        public static StringBuilder CreateStringBuilder()
        {
// Obtain the Type for the StringBuilder class.
            Type type = typeof(StringBuilder);

            // Create a Type[] containing Type instances for each
            // of the constructor arguments - a string and an int.
            Type[] argTypes = new Type[] { typeof(System.String),
              typeof(System.Int32) };

            // Obtain the ConstructorInfo object.
            ConstructorInfo cInfo = type.GetConstructor(argTypes);

            // Create an object[] containing the constructor arguments.
            object[] argVals = new object[] { "Some string", 30 };

            // Create the object and cast it to StringBuilder.
            StringBuilder sb = (StringBuilder)cInfo.Invoke(argVals);

            return sb;
        }
    }
}

The following code demonstrates a factory to instantiate objects that implement the IPlugin interface (first used in recipe 3-7):

using System;
using System.Reflection;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    // A common interface that all plug-ins must implement.
    public interface IPlugin
    {
        string Description { get; set; }
        void Start();
        void Stop();
    }

    // An abstract base class from which all plug-ins must derive.
    public abstract class AbstractPlugin : IPlugin
    {
        // Hold a description for the plug-in instance.
        private string description = "";

        // Sealed property to get the plug-in description.
        public string Description
        {
            get { return description; }
            set { description = value; }
        }
// Declare the members of the IPlugin interface as abstract.
        public abstract void Start();
        public abstract void Stop();
    }

    // A simple IPlugin implementation to demonstrate the PluginFactory class.
    public class SimplePlugin : AbstractPlugin
    {
        // Implement Start method.
        public override void Start()
        {
            Console.WriteLine(Description + ": Starting...");
        }

        // Implement Stop method.
        public override void Stop()
        {
            Console.WriteLine(Description + ": Stopping...");
        }
    }

    // A factory to instantiate instances of IPlugin.
    public sealed class PluginFactory
    {
        public static IPlugin CreatePlugin(string assembly,
            string pluginName, string description)
        {
            // Obtain the Type for the specified plug-in.
            Type type = Type.GetType(pluginName + ", " + assembly);

            // Obtain the ConstructorInfo object.
            ConstructorInfo cInfo = type.GetConstructor(Type.EmptyTypes);

            // Create the object and cast it to StringBuilder.
            IPlugin plugin = cInfo.Invoke(null) as IPlugin;

            // Configure the new IPlugin.
            plugin.Description = description;

            return plugin;
        }

        public static void Main(string[] args)
        {
            // Instantiate a new IPlugin using the PluginFactory.
            IPlugin plugin = PluginFactory.CreatePlugin(
                "Recipe03-12",  // Private assembly name
                "Apress.VisualCSharpRecipes.Chapter03.SimplePlugin",
                  // Plug-in class name.
                "A Simple Plugin"       // Plug-in instance description
            );
// Start and stop the new plug-in.
            plugin.Start();
            plugin.Stop();

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Tip

The System.Activator class provides two static methods named CreateInstance and CreateInstanceFrom that instantiate objects based on Type objects or strings containing type names. The key difference between using GetConstructor and Activator is that the constructor used by Activator is implied by the constructor arguments you pass to CreateInstance or CreateInstanceFrom. Using GetConstructor, you can determine exactly which constructor you want to use to instantiate the object. See the description of the Activator class in the .NET Framework SDK documentation for more details.

Create a Custom Attribute

Problem

You need to create a custom attribute.

Solution

Create a class that derives from the abstract base class System.Attribute. Implement constructors, fields, and properties to allow users to configure the attribute. Use System.AttributeUsageAttribute to define the following:

  • Which program elements are valid targets of the attribute

  • Whether you can apply more than one instance of the attribute to a program element

  • Whether the attribute is inherited by derived types

How It Works

Attributes provide a mechanism for associating declarative information (metadata) with program elements. This metadata is contained in the compiled assembly, allowing programs to retrieve it through reflection at runtime without creating an instance of the type. (See recipe 3-14 for more details.) Other programs, particularly the CLR, use this information to determine how to interact with and manage program elements.

To create a custom attribute, derive a class from the abstract base class System.Attribute. Custom attribute classes by convention should have a name ending in Attribute (but this is not essential). A custom attribute must have at least one public constructor—the automatically generated default constructor is sufficient. The constructor parameters become the attribute's mandatory (or positional) parameters. When you use the attribute, you must provide values for these parameters in the order they appear in the constructor. As with any other class, you can declare more than one constructor, giving users of the attribute the option of using different sets of positional parameters when applying the attribute. Any public nonconstant writable fields and properties declared by an attribute are automatically exposed as named parameters. Named parameters are optional and are specified in the format of name/value pairs where the name is the property or field name. The following example will clarify how to specify positional and named parameters.

To control how and where a user can apply your attribute, apply the attribute AttributeUsageAttribute to your custom attribute. AttributeUsageAttribute supports the one positional and two named parameters described in Table 3-3. The default values specify the value that is applied to your custom attribute if you do not apply AttributeUsageAttribute or do not specify a value for that particular parameter.

Table 3.3. Members of the AttributeUsage Type

Parameter

Type

Description

Default

ValidOn

Positional

A member of the System.AttributeTargets enumeration that identifies the program elements on which the attribute is valid

AttributeTargets.All

AllowMultiple

Named

Whether the attribute can be specified more than once for a single element

False

Inherited

Named

Whether the attribute is inherited by derived classes or overridden members

True

The Code

The following example shows a custom attribute named AuthorAttribute, which you can use to identify the name and company of the person who created an assembly or a class. AuthorAttribute declares a single public constructor that takes a string containing the author's name. This means users of AuthorAttribute must always provide a positional string parameter containing the author's name. The Company property is public, making it an optional named parameter, but the Name property is read-only—no set accessor is declared—meaning that it isn't exposed as a named parameter.

using System;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly,
         AllowMultiple = true, Inherited = false)]
public class AuthorAttribute : System.Attribute
    {
        private string company; // Creator's company
        private string name;    // Creator's name

        // Declare a public constructor.
        public AuthorAttribute(string name)
        {
            this.name = name;
            company = "";
        }

        // Declare a property to get/set the company field.
        public string Company
        {
            get { return company; }
            set { company = value; }
        }

        // Declare a property to get the internal field.
        public string Name
        {
            get { return name; }
        }
    }
}

Usage

The following example demonstrates how to decorate types with AuthorAttribute:

using System;

// Declare Allen as the assembly author. Assembly attributes
// must be declared after using statements but before any other.
// Author name is a positional parameter.
// Company name is a named parameter.
[assembly: Apress.VisualCSharpRecipes.Chapter03.Author("Allen",
    Company = "Apress")]

namespace Apress.VisualCSharpRecipes.Chapter03
{
    // Declare a class authored by Allen.
    [Author("Allen", Company = "Apress")]
    public class SomeClass
    {
        // Class implementation.
    }
// Declare a class authored by Lena.
    [Author("Lena")]
    public class SomeOtherClass
    {
        // Class implementation.
    }
}

Inspect the Attributes of a Program Element Using Reflection

Problem

You need to use reflection to inspect the custom attributes applied to a program element.

Solution

All program elements implement the System.Reflection.ICustomAttributeProvider interface. Call the IsDefined method of the ICustomAttributeProvider interface to determine whether an attribute is applied to a program element, or call the GetCustomAttributes method of the ICustomAttributeProvider interface to obtain objects representing the attributes applied to the program element.

How It Works

All the classes that represent program elements implement the ICustomAttributeProvider interface. This includes Assembly, Module, Type, EventInfo, FieldInfo, PropertyInfo, and MethodBase. MethodBase has two further subclasses: ConstructorInfo and MethodInfo. If you obtain instances of any of these classes, you can call the method GetCustomAttributes, which will return an object array containing the custom attributes applied to the program element. The object array contains only custom attributes, not those contained in the .NET Framework base class library.

The GetCustomAttributes method provides two overloads. The first takes a bool that controls whether GetCustomAttributes should return attributes inherited from parent classes. The second GetCustomAttributes overload takes an additional Type argument that acts as a filter, resulting in GetCustomAttributes returning only attributes of the specified type.

Alternatively, you can call the IsDefined method. IsDefined provides a single overload that takes two arguments. The first argument is a System.Type object representing the type of attribute you are interested in, and the second is a bool that indicates whether IsDefined should look for inherited attributes of the specified type. IsDefined returns a bool indicating whether the specified attribute is applied to the program element, and is less expensive than calling the GetCustomAttributes method, which actually instantiates the attribute objects.

The Code

The following example uses the custom AuthorAttribute declared in recipe 3-13 and applies it to the Recipe03-14 class. The Main method calls the GetCustomAttributes method, filtering the attributes so that the method returns only AuthorAttribute instances. You can safely cast this set of attributes to AuthorAttribute references and access their members without needing to use reflection.

using System;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    [Author("Lena")]
    [Author("Allen", Company = "Apress")]
    class Recipe03_15
    {
        public static void Main()
        {
            // Get a Type object for this class.
            Type type = typeof(Recipe03_15);

            // Get the attributes for the type. Apply a filter so that only
            // instances of AuthorAttribute are returned.
            object[] attrs =
                type.GetCustomAttributes(typeof(AuthorAttribute), true);

            // Enumerate the attributes and display their details.
            foreach (AuthorAttribute a in attrs) {
                Console.WriteLine(a.Name + ", " + a.Company);
            }

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
}

Programmatically Discover the Members of a Type

Problem

You need to determine the members of a type at runtime.

Solution

Obtain the System.Type for the type you need to reflect on, and call the GetMembers method to get information about all of the members of the type, or call the GetConstructors, GetMethods, and GetProperties methods to get information about specific categories of a member.

How It Works

You can get details of the members of a given type through the System.Type class. You can get an instance of Type by calling the GetType method on any object or by using the typeof keyword. Once you have a Type instance, the GetMembers method returns information about all members, while the GetConstructors, GetMethods, and GetProperties methods return just constructors, methods, and properties, respectively. The Type class also contains methods to get less commonly used member types (such as events) and interfaces that the type implements.

The GetMembers method returns an array of System.Reflection.MemberInfo instances, one for each member in the type you are reflecting on. You can differentiate between different categories of member using the MemberInfo.MemberType property, as shown by the following fragment:

MemberInfo[] members = myType.GetMembers();
foreach (MemberInfo member in members)
{
    switch (member.MemberType)
    {
        case MemberTypes.Constructor:
            // Do something.
            break;
        case MemberTypes.Method:
            // Do something.
            break;
    }
}

The other System.Type methods return arrays of classes from the System.Reflection namespace specific to the member category—for example, GetConstructors returns an array of System.Reflection.ConstructorInfo and GetMethods returns an array of System.Reflection.MethodInfo.

The Code

The following example uses reflection on its own type to enumerate the constructors, methods and properties.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_15
    {
        static void Main(string[] args)
        {

            // Get the type we are interested in.
            Type myType = typeof(Recipe03_15);

            // Get the constructor details.
            Console.WriteLine("
Constructors...");
            foreach (ConstructorInfo constr in myType.GetConstructors())
            {
                Console.Write("Constructor: ");
                // Get the paramters for this constructor.
                foreach (ParameterInfo param in constr.GetParameters())
                {
                    Console.Write("{0} ({1}), ", param.Name, param.ParameterType);
                }
                Console.WriteLine();
            }

            // Get the method details.
            Console.WriteLine("
Methods...");
            foreach (MethodInfo method in myType.GetMethods())
            {
                Console.Write(method.Name);
                // Get the paramters for this constructor.
                foreach (ParameterInfo param in method.GetParameters())
                {
                    Console.Write("{0} ({1}), ", param.Name, param.ParameterType);
                }
                Console.WriteLine();
            }

            // Get the property details.
            Console.WriteLine("
Property...");
            foreach (PropertyInfo property in myType.GetProperties())
            {
                Console.Write("{0} ", property.Name);
                // Get the paramters for this constructor.
                foreach (MethodInfo accessor in property.GetAccessors())
                {
                    Console.Write("{0}, ", accessor.Name);
                }
                Console.WriteLine();
            }
// Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }

        public string MyProperty
        {
            get;
            set;
        }

        public Recipe03_15(string param1, int param2, char param3)
        {

        }
    }
}

Invoke a Type Member Using Reflection

Problem

You need to invoke a method on a type.

Solution

Use the InvokeMember method of System.Type or the Invoke method on the MemberInfo class or its derived types (MethodInfo, PropertyInfo, etc.).

How It Works

You can call a member directly on a Type, using the InvokeMember method, to which you must supply the name of the method you wish to call, the instance of the type you wish to call against, and an array of objects containing the parameters you wish to pass to the member. You must also provide a value from the BindingFlags enumeration that specifies what kind of call should be made—values exist for invoking a method (BindingFlags.InvokeMethod), getting and setting properties (BindingFlags.GetProperty and BindingFlags.SetProperty), and so on.

You can obtain an instance of MemberInfo or one of its derived types (such as MethodInfo for methods, PropertyInfo for properties, etc.) and call the Invoke method. See the code for this recipe for an example of both approaches.

The Code

The following example calls a method using the InvokeMember method of System.Type and calls it again through the MemberInfo class:

using System;
using System.Reflection;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_16
    {
        static void Main(string[] args)
        {

            // Create an instance of this type.
            object myInstance = new Recipe03_16();

            // Get the type we are interested in.
            Type myType = typeof(Recipe03_16);

            // Get the method information.
            MethodInfo methodInfo = myType.GetMethod("printMessage",
                new Type[] { typeof(string), typeof(int), typeof(char) });

            // Invoke the method using the instance we created.
            myType.InvokeMember("printMessage", BindingFlags.InvokeMethod,
                null, myInstance, new object[] { "hello", 37, 'c' });

            methodInfo.Invoke(null, BindingFlags.InvokeMethod, null,
                new object[] { "hello", 37, 'c' }, null);

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }

        public static void printMessage(string param1, int param2, char param3)
        {
            Console.WriteLine("PrintMessage {0} {1} {2}", param1, param2, param3);
        }
    }
}

Running the program gives the following results:

PrintMessage hello 37 c

PrintMessage hello 37 c

PrintMessage hello 37 c



Main method complete. Press Enter.

Dynamically Invoke a Type Member

Problem

You want to invoke a member dynamically.

Solution

Declare your object using the special type dynamic.

How It Works

Recipe 3-16 illustrated how to use reflection to invoke a member on a type. An alternative approach to the same problem is to use the dynamic runtime support introduced in .NET 4.0. The net results are the same—you are responsible for ensuring that the member you want to call exists and that your parameters are of the right type—but the source code required to invoke the member is much simpler.

Simply declare your object as the special type dynamic and then invoke the members directly—the C# compiler will not check to see that the member you have called exists or that you have supplied the correct parameters. See the code for this recipe for an illustration.

The Code

The following example calls the same method as in the previous recipe, but does so dynamically:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_17
    {
        static void Main(string[] args)
        {
            // Create an instance of this type.
            dynamic myInstance = new Recipe03_17();

            // Call the method dynamically.
            myInstance.printMessage("hello", 37, 'c'),

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }

        public void printMessage(string param1, int param2, char param3)
        {
            Console.WriteLine("PrintMessage {0} {1} {2}", param1, param2, param3);
        }
    }
}

Create a Custom Dynamic Type

Problem

You need to create a type with custom behavior when dynamic member invocations are performed.

Solution

Extend the System.Dynamic.DynamicObject class and override one or more of the instance methods in that class that begin with Try, such as TrySetMember and TryGetMember.

How It Works

The previous recipe showed how to invoke a method dynamically. The System.Dynamic.DynamicObject class puts you on the other side of that equation and allows you to create a dynamic type with custom behavior.

When deriving a type from DynamicObject, you can implement members as you would usually and override one or more of the TryXXX methods, such as TrySetMember and TryGetMember. When you instantiate and call a member of your type, the runtime will look for a member you have implemented, and if it cannot find one, it will call the appropriate TryXXX method. Table 3-4 lists the TryXXX methods.

Table 3.4. Useful Methods from the System.Dynamic.DynamicObject Class

Method

Description

TryBinaryOperation

Called for binary operations such as addition and multiplication

TryConvert

Called for operations that convert from one type to another

TryCreateInstance

Called when the type is instantiated

TryGetIndex

Called when a value is requested via an array-style index

TryGetMember

Called when a value is requested via a property

TryInvokeMember

Called when a method is invoked

TrySetIndex

Called when a value is set via an array-style index

TrySetMember

Called when a property value is set

Each of the TryXXX methods defines arguments that allow you to determine what member the caller has called and the arguments or values that have been passed to you. For example, to implement custom behavior for getting a property value, we would implement the TryGetMember method, which is declared as follows:

public override bool TryGetMember(GetMemberBinder binder, out object result)

The GetMemberBinder class provides us with information about the property that the caller has requested—the most useful member being Name, which returns the name of the property that the caller wants—remember that dynamic types allow the caller to request any property, not just the ones we have implemented. You set the value of the result parameter to whatever you want to return to the caller and use the bool returned from the method to indicate whether you are willing to support the property that has been requested. For example, the following fragment uses the GetMemberBinder parameter to determine which property the caller has asked for, and will only return a value for those properties that begin with the letter a:

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (binder.Name.StartsWith("A"))
    {
       result = "Hello";
        return true;
    } else {
        result = null;
        return false;
    }
}

Returning false from a TryXXX method will cause a runtime exception to be thrown to the caller—see the example code for this recipe for an example of this. Note that you must assign a value to the result parameter even if you are returning false from the method.

You must declare an instance of your type using the dynamic keyword—if you do not, the compiler will perform static checking, and you will only be able to access the members you have defined and the TryXXX members of the DynamicObject class.

The Code

The following example extends the DynamicObject class to create a wrapper around a dictionary such that calls to get and set properties on the dynamic type are mapped to the key/value pairs contained in the dictionary:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Dynamic;

namespace Apress.VisualCSharpRecipes.Chapter03
{
    class Recipe03_18
    {
        static void Main(string[] args)
        {

            dynamic dynamicDict = new MyDynamicDictionary();
            // Set some properties.
            Console.WriteLine("Setting property values");
            dynamicDict.FirstName = "Adam";
            dynamicDict.LastName = "Freeman";

            // Get some properties.
            Console.WriteLine("
Getting property values");
            Console.WriteLine("Firstname {0}", dynamicDict.FirstName);
            Console.WriteLine("Lastname {0}", dynamicDict.LastName);

            // Call an implemented member.
            Console.WriteLine("
Getting a static property");
            Console.WriteLine("Count {0}", dynamicDict.Count);

            Console.WriteLine("
Getting a non-existent property");
            try
            {
                Console.WriteLine("City {0}", dynamicDict.City);
            }
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e)
            {
                Console.WriteLine("Caught exception");
            }

            // Wait to continue.
            Console.WriteLine("
Main method complete. Press Enter.");
            Console.ReadLine();
        }
    }
    class MyDynamicDictionary : DynamicObject
    {
        private IDictionary<string, object> dict = new Dictionary<string, object>();

        public int Count
        {
            get
            {
                Console.WriteLine("Get request for Count property");
                return dict.Count;
            }
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            Console.WriteLine("Get request for {0}", binder.Name);
            return dict.TryGetValue(binder.Name, out result);
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            Console.WriteLine("Set request for {0}, value {1}", binder.Name, value);
            dict[binder.Name] = value;
            return true;
        }

    }
}

Running the example gives the following results:

Setting property values

Set request for FirstName, value Adam

Set request for LastName, value Freeman



Getting property values

Get request for FirstName

Firstname Adam

Get request for LastName

Lastname Freeman



Getting a static property

Get request for Count property

Count 2



Getting a non-existent property

Get request for City

Caught exception



Main method complete. Press Enter.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.255.5