Delen via


How to increase application scalability using Plugins

This time I will talk about how to implement a plugin module that loads and runs plugins in your application.
but first why do we need such thing?
In so many cases, you need to provide a way that enables someone else to write a piece of code that can be loaded in your application in runtime and runs in the same context of your application without the need to stop the base application and change your code and redeploy.
These cases regularily happen when you software that it is very hard to shutdown every time you want to extend its functionality (e.g.Windows Services..)

Things you should put in mind before implementing such system

  1. Plugin should be easy to implement,
    This can be done by providing enough documentation, and providing a flexible model for implementing plugins
  2. The system must be able to
    1. Load plugins during runtime
    2. Unload unused (or stopped plugins)
    3. Detect plugin crashes and unload crashed plugins without crashing the whole system
    4. Run multiple plugins at the same time
    5. Detect new plugins and run them.
  3. Plugins shouldn't be locked to make it easier to update the plugins without closing the application

So lets discuss every point in more details

1- Plugin should be easy to implement
You should decide how developers are going to implement plugins in your system, and how you can specify mandatory specifications that are logical, acceptable and also easy to be done by the developers
There are many approaches to do so

  1. Implementing Specific Intrfaces
  2. Using Attributes & Reflection
  3. Using XML configuration files and Reflection

The most common used technique is the first one, that the system developer (The one develops the main system) will provide an inteface for the plugin class like this

 interface Pluggable
{
     string PluginTitle{get;}
     void Init();
     void Start();
}

and every plugin will implement the interface

What's good about this

  1. Some how it is simple for the plugin developer
  2. Makes the pluggable classes looks the same from the system point of view...
  3. If you want to get a return value from the methods, this is the only way to inforce the method signature

What's bad about this

  1. Interfaces are not that flexible, because you have to implement functions even that you may not use

Using Attributes is more flexible because you can specify some attrbitues like Pluggable for classes and Runnable for functions like this

 namespace PluginModule
{
    [AttributeUsage(AttributeTargets.Class)]
    public class PluggableAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Method)]
    public class RunnableAttribute : Attribute
    {
    }
}

and then you just need to add the suitable attribute before the declaration of the class or function
like this

 namespace TestPlugin
{
    [Pluggable]
    public class TestPlug 
    {
        [Runnable]
        public void SayHello()
        {
            Console.WriteLine("First Plugin");
        }
    }
}

I prefer this way... for these reasons

  1. It is very much cleaner, you don't need to implement a certain interface
  2. You can have as much runnable functions as you want in the same class

The third wy to define a plugin is using config files, that you write the plugin class name and every method in every class in a configuration files, then use reflection to load and run these functions
the bad things about this technique

  1. The programmer should write a config file for every plugin,
  2. Spelling mistakes in configuration files will make some bugs like function and modules not found

2- Loading plugin in runtime
Now we get to the important part, How are we going to load the plugins...
we have two ways for this

  1. Loading the plugin in the same appdomain of the main application
  2. Loading the plugin in a separate appdomain

So, what is the difference between the two ways.
Ofcource the first one is simple you will load the assembly (dll, or exe) plugin using either Assembly.Load() or AppDomain.CurrentDomain.Load() and once the assembly is loaded you can get all the Types (classes) defined in it and execute them,
But, you will have some drawbacks like

  1. You can't Unload assemblies from the domain, without shutting down the whole application, because neither Assembly class nor AppDomain class has a function UnloadAssembly, and this has a very good reason that maybe there are other loaded assemblies depend on this Assembly and this will make a very big problem.
  2. You can't write separate App.config (*.dll.config, *.exe.config) file for each plugin and load with the assembly
  3. In cases of fatal errors and crashes, it is very possible that the whole application will crash.
  4. You can't limit the security level of the plugins lower that the level of your application
  5. Dll files will be locked so you can't overrite the files unless you shutdown the whole application

You can still use this technique if you don't care about these problems...

But what about Loading in a different domain...
Well, it is the best way to do it, because all the above problems are solved in this technique, but it is more complicated and will involve some extra work in Remoting, because this is the only way I found to istantiate and execute an object in different domain..

First you need to create a new domain and setup this domain

 private AppDomain CreateAppDomain(string domainName, string fileName, string configFileName)
        {
            AppDomainSetup setup = new AppDomainSetup();
            setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
            setup.ApplicationName = Path.GetFileName(fileName);
            setup.ShadowCopyDirectories = Path.GetDirectoryName(fileName);
            setup.CachePath = AppDomain.CurrentDomain.BaseDirectory;
            setup.PrivateBinPath = Path.GetDirectoryName(fileName);
            setup.ShadowCopyFiles = "true";
            setup.ConfigurationFile = configFileName;
            AppDomain domain = AppDomain.CreateDomain(domainName, AppDomain.CurrentDomain.Evidence, setup);
            return domain;
        }

As you see from this small function, first it creates an AppDomainSetup object that will store all the configuration
then it sets the ApplicationBase directory for the new appdomain as same as the main application and this helps to reduce the number of external dll's needed by the plugins (Dependencies) because if the main application uses them so they can share them.
and sets an application name, here we set the application name as same as the dll file name.
then we Shadow copy the files and directories and this makes the files free to be removed or replaced during runtime (the file locking problem)
then we set the config file(*.dll.config, *.exe.config) name (if any)
then we create the appdomain using the setup information  and using the same security eveidance of the base application (This can be changed to limit the security level if needed)

We are now ready to load the plugin, next we are going to reflect the plugin and get all the Pluggable classes and for every Pluggable class get all Runnable functions
First this module will get all the pluggable classes from the assembly

 
        private Type[] GetModules(string filename, AppDomain domain)
        {
            Assembly assembly = domain.Load(filename);
            List<Type> plugins = new List<Type>();
            Type[] types = assembly.GetTypes();
            foreach (Type type in types)
            {
                if (type.GetCustomAttributes(typeof(PluggableAttribute), false).Length > 0)
                {
                        plugins.Add(type);
                }
            }
            return plugins.ToArray();
        }

Then we need the module that gets all the Runnable functions from the Pluggable types and run them

 private void RunTypes(Type[] types)
        {
            foreach (Type type in types)
            {
                Object obj = type.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);
                foreach (MethodInfo method in type.GetMethods())
                {
                    //instanciating object from the type specified
                    if (method.GetCustomAttributes(typeof(RunnableAttribute), false).Length > 0)
                    {
                        //running the runnable function
                        method.Invoke(obj, null);
                    }
                }
            }
        }

so now we have everything we need to create a plugin manager class that does these things

  1. Load the Dll
  2. Reflect all the pluggable types
  3. Reflect all the Runnable methods
  4. Instantiating objects from Runnable classes
  5. Run the methods

But, Are we finished ???
No, we still have something missing, we didn't create the class that will be responsible of creating the plugin manager class in separate domain  !!!!
Yes, it sounds strange but let's discuss this...
We said that using remoting is the only way (I found) to load dll's in a separate domain, which means that we need a MarshalByRefObject class that handle the plugin, so first we need to put all the methods we made in one class the inherits from MarshalByRefObject, so we can instantiate object from it using remoting...
and this how the PluginManager class will look like

 class PluginManager : MarshalByRefObject
{
     private Type[] GetModules(string filename, AppDomain domain)
        {
            Assembly assembly = domain.Load(filename);
            List<Type> plugins = new List<Type>();
            Type[] types = assembly.GetTypes();
            foreach (Type type in types)
            {
                if (type.GetCustomAttributes(typeof(PluggableAttribute), false).Length > 0)
                {
                        plugins.Add(type);
                }
            }
            return plugins.ToArray();
        }

private void RunTypes(Type[] types)
        {
            foreach (Type type in types)
            {
                Object obj = type.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);
                foreach (MethodInfo method in type.GetMethods())
                {
                    //instanciating object from the type specified
                    if (method.GetCustomAttributes(typeof(RunnableAttribute), false).Length > 0)
                    {
                        //running the runnable function
                        method.Invoke(obj, null);
                    }
                }
            }
        }
public void RunMethods(string fileName, AppDomain domain)
        {
            RunTypes(this.GetModules(filename, domain));
        }
}

then we create another class (let's call it PluginLoader) which is responsible of creating a plugin manager object using remoting, so the code will be something like this

 public static class PluginLoader
{
        public static void Run(string filename, AppDomain domain)
        {
                //Creating an object of PluginManager using remoting
                PluginManager plugin = appdomain.CreateInstanceAndUnwrap(AssemblyName.GetAssemblyName(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
                "PluginModule.dll")).FullName, typeof(PluginManager).FullName) as PluginManager;
                domain.AssemblyResolve += new ResolveEventHandler(Domain_AssemblyResolve);
                plugin.RunMethods(fileName, domain);
        }

        static Assembly Domain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            AppDomain domain = sender as AppDomain;
            string pluginFolder = domain.RelativeSearchPath;
            string path = Path.Combine(pluginFolder, args.Name.Split(',')[0] + ".dll");
            return domain.Load(AssemblyName.GetAssemblyName(path));
        }
}

so as you see the PluginLoader class has one public static method called Run, that takes the filename and the target appdomain, and it creates an object of type PluginManager in the appdomain and runs the functions inside its appdomain...
final thing, sometime the plugins dll's have some dependencies, and this is what the AssemblyResolve event does, so we added  an event handler that takes care of this, becausewhen a dll needs a dependency dll, the system will first look in the GAC, then look inside the Application Bin, and if both of the actions failed, it will fire this event to the programmer to provide the missing dll and that what we do,

Finally, I hope that you find what you were looking for in this post, and if you need more help you can contact me and I'll be glad to help you.
Regards
kick it on DotNetKicks.com
Updated, Sample solution is attached

CLR_Hosting.zip

Comments

  • Anonymous
    September 08, 2009
    What about sources?I'm release that example, but it is does not work!
  • Anonymous
    September 19, 2009
    @StanWhat is the error you're getting?