.Net App Domain question

brandonb

Diamond Member
Oct 17, 2006
3,731
2
0
This seems like a very simple question, and for some reason it has eluded me, so I just need someone to get me back in check.

I simply want to know if there is a graceful way to have an appdomain shut itself down?

I have a plugin engine that I'm working on (I know there are a million of those out there) but I want to learn how to do this on my own for knowledge. I have a plugin which is in a .DLL, and I want it to tell the host (.EXE) to have it shut down the .DLL.

I raise an event from the .DLL, to the .EXE, and it does the AppDomain.Unload() on the .DLL, which causes a runtime error because the callstack passes through the .DLL which has now been unloaded.

---

I was thinking that maybe have the event set an event that is in a thread. But the thread could possilby unload the appdomain while the .dll was in process as well. Is there truely a nice clean way to handle this?
 

Oyster

Member
Nov 20, 2008
151
0
0
OK, I don't know of an easy way of explaining this because I can't just blatantly share all my code. But I have done this for a lot plugin containers, and it has worked for me like a charm. You can ask questions, and if it comes to posting code, I can always share snippets with you.

For the following discussion, I am going to assume that all your plugins implement a common interface.

1) You need to take a step back and think about marshaling objects across domain boundaries. This entails using MarshalByRefObject.

2) This may sound a bit perplexing, but obviously, you're at an advanced level, so I hope it makes sense. You need to create a proxy object for all your domain-related work. So this proxy inherits from MarshalByRefObject and all it does is it performs any domain-related tasks and calls methods inside your plugins. Think of this proxy as a wrapper around your plugins. My basic structure involves declaring all the metadata associated with the plugin in an instance of this proxy object, meaning each plugin will have its own proxy object. So methods may include Load(), Unload(), Execute(), etc. For example, Load() uses Reflection to load the assembly and call any instantiation methods on your plugin. Unload() would call any "stop" methods in your plugin. Also, call me paranoid, but I like to tap into CurrentDomain.UnhandledException and CurrentDomain.AssemblyResolve events in the constructor of this proxy. Note that you're not loading or unloading any AppDomains from this proxy object. This proxy is simply a wrapper for your launching and "delaunching" your plugins. All this proxy knows, is the interface definitions - note, you're NOT implementing any interfaces here. You may wonder as to how information gets sent to the proxy - for example, you're calling certain methods on your plugin from the Load() function above - the function needs to know the assembly folder, class name, etc. This is the fun part... you'd create a serializable class that can "pass" through domains. This is code I can share (note that this is for .NET 4.0 and uses WCF) -- in my case, each plugin references other plugins, so I have ExtendedProperties, but the idea is, you can have whatever metadata you want for the plugin over here:

Code:
/// <summary>
    /// Holds information about a plug-in.
    /// </summary>
    [Serializable()]
    [DataContract()]
    public class PlugInDefinition
    {
        #region Properties

        /// <summary>
        /// Gets or sets the unique ID via which the plug-in can be recognized.
        /// </summary>
        [DataMember()]
        public String PlugInID { get; set; }

        /// <summary>
        /// Gets or sets the namespace-qualified class name of the plug-in.
        /// </summary>
        [DataMember()]
        public String ClassName { get; set; }

        /// <summary>
        /// Gets or sets the name of the assembly in which the plug-in is defined.
        /// </summary>
        [DataMember()]
        public String AssemblyName { get; set; }

        /// <summary>
        /// Gets or sets the path to the folder where the plug-in resides.
        /// </summary>
        [DataMember()]
        public String AssemblyFolder { get; set; }

        /// <summary>
        /// Gets or sets the extended properties.
        /// </summary>
        [DataMember()]
        public List<String> ExtendedProperties { get; set; }

        #endregion

        #region Methods

        public override String ToString()
        {
            if (ExtendedProperties != null)
            {
                return String.Format("PlugInID={0}, ClassName={1}, AssemblyName={2}, AssemblyFolder={3}, ReferencedPlugIns={4}",
                    PlugInID, ClassName, AssemblyName, AssemblyFolder,
                        (
                            from s in ExtendedProperties
                            select s
                        ).Aggregate((current, next) => current + "||" + next));
            }
            else
            {
                return String.Format("PlugInID={0}, ClassName={1}, AssemblyName={2}, AssemblyFolder={3}, ExtendedProperties=NotInitialized",
                    PlugInID, ClassName, AssemblyName, AssemblyFolder);
            }
        }

        #endregion
    }
To summarize, you haven't implemented your common interface in this step. All you have done is created a proxy object which can be talk across domain boundaries. And you have created a metadata repository for your plugins which will be referenced by each instance of the proxy object. So, for example, if you have 10 plugins then you'll have 10 proxies, and each proxy containing its own metadata for the corresponding plugin. Again, although you haven't implemented the interface, you are calling methods in the interface from your proxy object.
The proxy and the metadata object(s) can be wrapped in their own DLL along with your common interfaces, if you want. I usually do this to keep things clean. This way, you have everything associated with your interfaces in one project/DLL.

3) Now comes your core service: This is the EXE that is responsible for loading and unloading your plugins. In my case, I have a lot of threading going over here, so there is no easy way to share the code. Regardless, the idea is that you have to launch each plugin on its own background thread. This way, if the EXE crashes, it brings down everything (this may or may not be desirable based on your business rules). There also has to be thread synchronization (with timeouts, etc.) so that the service can attend to all plugins when it comes to unloading your plugins. From a design standpoint, you should assume that every plugin is "unsafe," so you can't wait forever for each plugin to unload. I have done some funky things in the service (the plugins communicate over TCP using WCF), so if you want the details, I can spill them later. I am going to skip over those details because they're not applicable to this discussion. Here is some more code -- this is where the plugins get loaded and unloaded. Note that Proxy here is the "proxy" I explained earlier:

Code:
    // Plugins
    static Dictionary<String, Proxy> loaders = new Dictionary<String, Proxy>();
    static Dictionary<String, AppDomain> domains = new Dictionary<String, AppDomain>();
    static List<PlugInDefinition> plugIns = new List<PlugInDefinition>();

In my case, I store all the metadata associated with the plugins in an XML file. You can store it wherever you want. When the service initializes, the first thing it does, is it initializes the "plugIns" list above. Once you have this, you loop through all plugIns and start launching them:

Code:
static void InitializePlugIn(PlugInDefinition plugIn)
        {
            AppDomainSetup domainSetup = new AppDomainSetup();
            String configurationFilePath;
            
            // Custom, private method
            if (AssemblyHasConfiguartionFile(plugIn.AssemblyFolder, out configurationFilePath))
            {
                domainSetup.ConfigurationFile = configurationFilePath;
            }

            domainSetup.ApplicationBase = plugIn.AssemblyFolder;
            domainSetup.ApplicationName = plugIn.PlugInID;
            domainSetup.DynamicBase = plugIn.AssemblyFolder;

            // Create a new app domain for the plug-in using the current AppDomain's evidence
            // and the plug-in-specific configuration settings.
            AppDomain domain = AppDomain.CreateDomain(plugIn.AssemblyName, AppDomain.CurrentDomain.Evidence, domainSetup);
            
            // Note that this information resides in the EXE's config
            Proxy loader = domain.CreateInstanceAndUnwrap(
                Settings.Default.ProxyAssembly, Settings.Default.ProxyClass) as Proxy;

            // Note that here is where you set the metadata we talked about in step 2
            loader.PlugInObject = plugIn;

            // Here is where we populate the other two lists -- these will be used later when we unload domains and plugins
            domains.Add(plugIn.PlugInID, domain);
            loaders.Add(plugIn.PlugInID, loader);

            // Executes the plug-in in the newly created AppDomain. Here is where the power of MarshalByRef and the proxy object comes into play -- you just launched a new AppDomain, without any core dependencies. This comes into play when you unload the AppDomains.
            loader.Load();

        }

Within the same loop, you'd also call the Execute() method on your proxy object -- remember that your proxy is a wrapper around your interface, so in essence, you're "remotely" calling your proxy which, in turn, calls any execution code of your plugin. Basically, in order to gracefully handle AppDomains, you have offloaded all your work to this proxy object. Hope this is starting to make sense...
On your service stop, or any disastrous exception, you now start tearing down the AppDomains -- again, you loop through your plugin objects defined above:

Code:
public void TerminatePlugIns()
        {
            foreach (Proxy loader in loaders.Values)
            {
                try
                {
                    loader.Unload();
                }
                catch (Exception error)
                {
                    // Do nothing - continue
                }
            }

            foreach (AppDomain domain in domains.Values)
            {
                String domainName = domain.FriendlyName;

                try
                {
                    // The CLR waits 10 seconds (by default) for the threads in the unloading AppDomain to leave it. 
                    // If after 10 seconds, the thread that called AppDomain.Unload doesn&#8217;t return, it will throw a 
                    // CannotUnloadAppDomainException, and the AppDomain may or may not be unloaded in the future. If this 
                    // turns out to be the case, there is nothing else we can do here.
                    AppDomain.Unload(domain);
                }
                catch (Exception error)
                {
                    // Do nothing - continue
                }
            }
        }

Now, to answer your main question -- how to shut down a specific plugin gracefully. In my case, I have a separate WCF interface which the plugins implement and call when they need to shut themselves (or the EXE) down. You can do this or find some other mechanism to communicate ACROSS AppDomains. Ultimately, once you have the proper infrastructure in place, you can terminate your plugins individually. In my sample above, I am terminating and launching all plugins, but the basic idea is the same for individual plugins. Since your proxy object allows you to communicate across boundaries, your EXE doesn't know jack about the remote AppDomains you created - to shut down those remote AppDomains, you have to notify the EXE's AppDomain "from the outside." Otherwise we run into the problem you alluded to earlier. Note that this is not a flaw in any way (at least IMO) -- the notion of AppDomains is that they are sandboxed in their own environment. When you try to tear AppDomains from within each other, it goes against the core CLR design principles.

I am sure this might confuse you further, but we can cross that bridge once we get there :).

As I mentioned, I have used this multiple times without issues. Let me know if you can actually think of improvements. I guess this is one of those advanced topics that doesn't get discussed often, so I am always on the lookout for improvements.


Edit: This is a known issue in .NET - you want to insert the following code in your proxy that inherits from MarshalByRefObject to ensure that the proxy object stays alive for eternity. After a lot of troubleshooting, I found that remoting objects get expunged on a regular basis as part of the CLR cleanup. Might save you some headaches!

Code:
    /// <summary>
        /// Indicates to the Remoting environment that the lease 
        /// for the Remoting objects never expires.
        /// </summary>
        /// <returns>Null to indicate that the object stays alive forever.</returns>
        public override Object InitializeLifetimeService()
        {
            return null;
        }
 
Last edited:

brandonb

Diamond Member
Oct 17, 2006
3,731
2
0
Oyster,

Thank you for your response. You've covered alot with your post, and is an excellent reference for anybody who wishes to get into AppDomains.

I read over it, and I think we are on the same page. Indeed the sticking point is that AppDomains needs to be unloaded from the outside, and I'm attempting to do it from the inside.

I've been thinking about alternate solutions and wish to test them out here shortly but I'm currently trying to hammer through another problem related to AppDomains, and that happens to be data binding in WPF, so once I get that tackled, then I can move back to this issue.

Like I mentioned in the OP, if I have a Manual Event that gets triggered when the unload is supposed to occur, I can have a thread waiting on it. Once it gets triggered, call the AppDomain.Unload(). The only other sticky part is I'd have to wait until the AppDomain is idle. Which I suppose I could create another Event there which is triggered whenever something comes through the proxy so I know if the appdomain is busy or idle. If both event handlers are triggered (idle + shutdown) then call the unload. Then add in a read/write lock so while the shutdown is happening, the appdomain can't be called. So some syncronizing is involved, but I'm hoping that might get me going down the right path.