Using a Separate Application Domain to Test for Implemented Interfaces

Sometimes you need to load an assembly for use or inspection.  The problem is that loading an assembly is permanent for the lifetime of an application domain.  Sometimes this is acceptable and sometimes it is not.   Mike Shughrue and I are working on something were it is definitely not preferable. 

I have a folder full of assemblies and I need to inspect each assembly to see if it implements a specific interface.  There may be a lot of assemblies in this directory that do not implement the specific interface because there are components built on components--say, for example, that only 1 in 4 assemblies implement the interface I am looking for because the other 3 assemblies are dependences of the one...

So, how do I figure out at runtime which assemblies I should be loading without loading each assembly into my app domain (where which they will remain until my app domain terminates)?     The key is to use another app domain that I can put up and tear down specifically for testing whether a given assembly implements an interface.

To put this into context, I have a Windows service that executes "agents" periodically, similar to the Windows Task Scheduler but much simpler.  My Agents directory is filled with assemblies that are either agents or agent dependencies and the task is to build a list of the agents at run-time.

Application Domains - Why Isn't There an UnLoad Method?

Jason Zander explained the reasoning behind the lack of an UnLoad method here.  

Deriving from MarshalByRefObject to Exchange Messages by Proxy Across Application Domains

An application domain is much like an operating system process.  For many reasons, there are definite rules about how to communicate across application domains.  The easiest way to think of communicating across application domains is to think of it terms of remoting.  If you have done any remoting in .NET, then the concepts are easily understood.

The act of moving data between boundaries is called marshalling.  Marshalling by proxy puts a remotable object in play as the conduit by which messages are passed between application domains.  Another approach is to push objects by value across the boundary.  This is done by serializing the data and moving the bytes across the boundary.  Eric has a good post on the difference between marshalling by reference and marshalling by value.

MarshalByRefObject is the base class for objects communicating by messages through a proxy across boundaries. 

Loading Agents at Run-Time

I have a Windows service that looks to an "Agents" directory for assemblies that implement the IAgent interface.  The IAgent interface is very simple:

    1: using System;
    2:  
    3: namespace Mcs.Epm.Services.EpmExecutive
    4: {
    5:     public delegate void OnStatusChangedEventHandler(object sender, IOnStatusChangedArgs args);
    6:  
    7:     public interface IAgent
    8:     {
    9:         /// <summary>
   10:         /// Globally Unique Identifer for the Agent
   11:         /// </summary>
   12:         Guid Guid { get; }
   13:  
   14:         /// <summary>
   15:         /// Name of the Agent
   16:         /// </summary>
   17:         string Name { get; }
   18:  
   19:         /// <summary>
   20:         /// Description of the Agent
   21:         /// </summary>
   22:         string Description { get; }
   23:  
   24:         /// <summary>
   25:         /// Name of the publisher of the Agent
   26:         /// </summary>
   27:         string Publisher { get; }
   28:  
   29:         /// <summary>
   30:         /// Date and time of the last run
   31:         /// </summary>
   32:         DateTime? LastRun { get; }
   33:  
   34:         /// <summary>
   35:         /// Open the agent (perform initialization)
   36:         /// </summary>
   37:         void Open();
   38:  
   39:         /// <summary>
   40:         /// Run the agent
   41:         /// </summary>
   42:         void Run();
   43:  
   44:         /// <summary>
   45:         /// Close the agent (cleanup)
   46:         /// </summary>
   47:         void Close();
   48:  
   49:         event OnStatusChangedEventHandler OnStatusChanged;
   50:     }
   51: }

Each Agent in the Agents directory may have zero or more dependencies so not every assembly in the Agents directory will implement IAgent.  When the service starts, I need to get a list of files and types that implement the IAgent interface.  The first step is to get the list of DLL assemblies:

    1: private string[] GetAssembliesInAgentDirectory(string filter)
    2: {
    3:     Assembly thisAssembly = Assembly.GetExecutingAssembly();
    4:  
    5:     if (thisAssembly == null)
    6:     {
    7:         Log(LogArea.GetAssemblies, Resources.NullAssembly, EventLogEntryType.Error);
    8:         return null;
    9:     }
   10:  
   11:     string path = Path.Combine(Path.GetDirectoryName(thisAssembly.Location), Resources.Agents);
   12:  
   13:     if (!Directory.Exists(path))
   14:     {
   15:         Log(LogArea.GetAssemblies, string.Format(Resources.AgentDirectoryNotFound, path),
   16:             EventLogEntryType.Error);
   17:  
   18:         return null;
   19:     }
   20:  
   21:     return Directory.GetFiles(path, filter);
   22: }

I filter the results of the above method by passing "*.dll":

    1: string[] files = GetAssembliesInAgentDirectory("*.dll");

What I really need is a list of the DLL files that implement IAgent and the fully qualified name of the type so I can use Activator to create an instance of each Agent and execute methods on the object.  I don't want to load every DLL assembly into my current application domain, so I need a way to load each DLL in an another application domain and query each type contained in the DLL to see if it implements IAgent.

IInterfaceInspector

I defined a simple interface for the component that will load an assembly and check for interface implementations:

    1: namespace Mcs.Epm.Services
    2: {
    3:     interface IInterfaceInspector
    4:     {
    5:         bool IsInterfaceImplemented(string filePath, string interfaceName);
    6:         bool TryGetTypeNameByFileAndInterface(string filePath, string interfaceName, out string typeName);        
    7:     }
    8: }

Given a path to a file and an interface name, I can find out of the interface is implemented in the assembly (the first method above) or more importantly, I can find out which type the interface is implemented in (the second method above).

Here is the implementation:

    1: using System;
    2: using System.Reflection;
    3:  
    4: namespace Mcs.Epm.Services
    5: {
    6:     /// <summary>
    7:     /// Implements a remotable interface inspector for determining whether a given interface
    8:     /// is implemented in an assembly
    9:     /// </summary>
   10:     class InterfaceInspector : MarshalByRefObject, IInterfaceInspector
   11:     {
   12:         #region Public Methods
   13:  
   14:         /// <summary>
   15:         /// Given a path to an assembly and a fully-qualified interface name,
   16:         /// determine whether the interface is implemented.  NOTE: Only exported types are
   17:         /// examined.
   18:         /// </summary>
   19:         /// <param name="filePath">The full path to the assembly</param>
   20:         /// <param name="interfaceName">The fully-qualified name of the interface</param>
   21:         /// <returns>True if the interface is implemented on an exported type, false if not.</returns>
   22:         public bool IsInterfaceImplemented(string filePath, string interfaceName)
   23:         {
   24:             Type[] types = GetExportedTypes(filePath);
   25:             
   26:             foreach (Type type in types)
   27:             {
   28:                 Type iAgentInterface = type.GetInterface(interfaceName);
   29:                 
   30:                 if (iAgentInterface != null)
   31:                 {
   32:                     return true;
   33:                 }
   34:             }
   35:  
   36:             return false;
   37:         }
   38:  
   39:         /// <summary>
   40:         /// Given a path to an assembly and a fully-qualified interface name,
   41:         /// determine whether the interface is implemented and return the fully qualified name of the type
   42:         /// that implements the interface.  NOTE: Only exported types are examined.
   43:         /// </summary>
   44:         /// <param name="filePath">The full path to the assembly</param>
   45:         /// <param name="interfaceName">The fully-qualified name of the interface</param>
   46:         /// <param name="typeName">On success, the fully qualified name of the type that implements the interface.</param>
   47:         /// <returns>True if the interface is implemented on an exported type, false if not.</returns>
   48:         public bool TryGetTypeNameByFileAndInterface(string filePath, string interfaceName, out string typeName)
   49:         {
   50:             typeName = string.Empty;
   51:  
   52:             Type[] types = GetExportedTypes(filePath);
   53:  
   54:             foreach (Type type in types)
   55:             {
   56:                 Type iAgentInterface = type.GetInterface(interfaceName);
   57:  
   58:                 if (iAgentInterface != null)
   59:                 {
   60:                     typeName = type.FullName;
   61:                     return true;
   62:                 }
   63:             }
   64:  
   65:             return false;
   66:             
   67:         }
   68:  
   69:         #endregion
   70:  
   71:         #region Private Methods
   72:  
   73:         /// <summary>
   74:         /// Get an array of exported Types.
   75:         /// </summary>
   76:         /// <param name="filePath">The full path to an assembly</param>
   77:         /// <returns>An array of exported types.</returns>
   78:         private static Type[] GetExportedTypes(string filePath)
   79:         {
   80:             Assembly agentAssembly = Assembly.LoadFile(filePath);
   81:  
   82:             return agentAssembly.GetExportedTypes();
   83:         }
   84:  
   85:         #endregion
   86:     }
   87: }

Here is the implementation of the GetAgentTypes method, which returns a dictionary containing the full path of the assembly the implements the IAgent interface the fully qualified name of the type that implements it.

    1: private Dictionary<string, string> GetAgentTypes()
    2: {
    3:     // Retrieve the list of DLL assemblies in the Agents directory
    4:     string[] files = GetAssembliesInAgentDirectory("*.dll");
    5:  
    6:     if (files != null)
    7:     {
    8:         AppDomain testDomain;
    9:  
   10:         //  The agents list contains the file path as the key and the full qualified name of the type implementating
   11:         //  IAgent as the value.
   12:         Dictionary<string, string> agentsList = new Dictionary<string, string>();
   13:  
   14:         try
   15:         {                    
   16:             testDomain = AppDomain.CreateDomain(Resources.ServiceName);
   17:  
   18:             //  Get the full name of the currently executing assembly, which contains an implementation of IInterfaceInspector
   19:             string executingAssemblyName = GetExecutingAssemblyFullName();
   20:  
   21:             //  Get an instance of the interface inspector
   22:             IInterfaceInspector interfaceInspector = GetInterfaceInspector(testDomain, executingAssemblyName);
   23:  
   24:             foreach (string file in files)
   25:             {
   26:                 string typeName;
   27:  
   28:                 // For each file, look to see if the IAgent interface is implemented
   29:                 if (interfaceInspector.TryGetTypeNameByFileAndInterface(file, Resources.IAgent, out typeName))
   30:                 {
   31:                     //  Add the full path and fully qualified type name to our list of agents.
   32:                     agentsList.Add(file, typeName);
   33:                 }
   34:             }
   35:         }
   36:         catch (Exception exception)
   37:         {
   38:             Log(LogArea.ServiceMainThread,
   39:                 string.Format(Resources.Exception, exception.Message),
   40:                 EventLogEntryType.Warning);
   41:  
   42:             return null;
   43:         }
   44:  
   45:         return agentsList;
   46:     }
   47:  
   48:     Log(LogArea.ServiceMainThread,
   49:         Resources.AgentsNotPresent,
   50:         EventLogEntryType.Warning);
   51:  
   52:     return null;
   53: }

Of particular interest, is the the call to GetInterfaceInspector, which attempts to create an instance of the interface inspector in the test application domain:

    1: private IInterfaceInspector GetInterfaceInspector(AppDomain testDomain, string executingAssemblyName)
    2: {
    3:     IInterfaceInspector interfaceInspector = testDomain.CreateInstanceAndUnwrap(executingAssemblyName,
    4:                                                                                 Resources.Inspector) as IInterfaceInspector;
    5:  
    6:     if (interfaceInspector == null)
    7:     {
    8:         throw new NullReferenceException("IInterfaceInspector");
    9:     }
   10:  
   11:     return interfaceInspector;
   12: }

Take a look at CreateInstanceAndUnwrap for more information.

So, now I can load an assembly in an a separate application domain and test whether it implements the IAgent interface.  All that is left is actually creating an instance of each Agent and executing methods on each one:

    1: foreach (KeyValuePair<string, string> agentFile in agentsList)
    2: {
    3:     IAgent agent = null;
    4:  
    5:     try
    6:     {
    7:         Assembly agentAssembly = Assembly.LoadFile(agentFile.Key);
    8:         Type agentType = agentAssembly.GetType(agentFile.Value);
    9:         Type iAgentInterface = agentType.GetInterface(Resources.IAgent);
   10:  
   11:         if (iAgentInterface != null)
   12:         {
   13:             agent = Activator.CreateInstance(agentType) as IAgent;
   14:  
   15:             if (agent != null)
   16:             {
   17:                 Log(LogArea.ServiceMainThread,
   18:                     string.Format(Resources.LoadingAgent, agent.Name),
   19:                     EventLogEntryType.Information);
   20:  
   21:                 agent.OnStatusChanged += agent_OnStatusChanged; // Unhooked in finally
   22:  
   23:                 if (IsTimeToRun(agent, agentFile.Key))
   24:                 {                                        
   25:                     agent.Open();
   26:                     agent.Run();
   27:                     agent.Close();
   28:                 }
   29:             }
   30:         }
   31:     }
   32:     catch (Exception exception)
   33:     {
   34:         Log(LogArea.ServiceMainThread,
   35:             string.Format(Resources.AgentLoadFailure, agentFile.Key, exception.Message),
   36:             EventLogEntryType.Error);
   37:     }
   38:     finally
   39:     {
   40:         if (agent != null)
   41:         {
   42:             agent.OnStatusChanged -= agent_OnStatusChanged;
   43:         }
   44:     }
   45: }

Very handy!  Thanks to Mike Shughrue for pointing me in the right direction.

Comments

  • Anonymous
    January 11, 2009
    I needed a test harness for mpFx, so I built a simple WinForms application and wrote little utilities