The Plugin Factory
In the last weblog entry, I described that the plugin system presented two main problems to be solved. The first problem is that Charlie’s PluginManager needed a way to work with plugins without knowing specifically which plugins were in use. This problem was solved in the last weblog entry with a Plugin interface, which defines the methods that a plugin must implement if it is to be plugged into Charlie.
The second problem is that Charlie’s PluginManager must be able to find and get hold of plugin classes located in assemblies that were created after the Charlie assembly (which means that Charlie was not compiled against the plugin assemblies, and cannot then create the plugins directly).
So that Charlie’s PluginManager did not have too many reponsibilities, I gave the PluginManager a helper, the PluginFactory, whose task it is to load up specific plugins from unknown assemblies. The PluginManager will look at the Web.config file or the database and find the name and type of a plugin to use. If presented in the Web.config file, it may look something like this:
<plugins> <add name="SecurityPlugin" type="SecurityNamespace.SecurityPlugin, SecurityAssembly"/> <add name="ArticlesPlugin" type="ArticlesNamespace.ArticlesPlugin, ArticlesAssembly"/> </plugins>
The name attribute is simply a friendly name for the specific plugin. The type attribute has two parts, separated by a comma. The first part presents the namespace and class name of the specific plugin, and the second part presents the name of the assemly (but without the .dll extension) in which that class can be found. The PluginManager can grab these string values, but the string values themselves are not much use. It falls to the PluginManager’s helper, the PluginFactory, to take the type string, find the assembly presented, find the class presented, and create an instance of that class, which will then be passed back to the PluginManager. The PluginManager can then call the plugin’s GetModules method and GetFilters method, which it must have because these methods are defined in the IPlugin interface.
The PluginFactory splits the type string into its two parts, the assembly name and the class name. It then uses a simple file operation to locate the assembly. It then uses Reflection to “find” the class within that assembly. Having found the class, the PluginFactory again uses Reflection to “get” the constructor of that class. Finally, the PluginFactory invokes that constructor, which will give it an instance of that class. So even though it only had a string to start with, the PluginFactory ends up having a full plugin object which it can return to the PluginManager. The Reflection used to find and create the constructor is a little slow, so we put the constructor in cache so that we will have faster access to it next time.
Here is the bulk of the PluginFactory class, should you want a template for doing this in your own application:
public sealed class PluginFactory { public IPlugin InstantiatePlugin(String typeLocation) { ConstructorInfo constructor = GetConstructor(typeLocation); return (IPlugin)(constructor.Invoke(null)); } private ConstructorInfo GetConstructor(String typeLocation) { Char[] splitter = { ',' }; String[] typeNameAndAssembly = typeLocation.Split(splitter); if (typeNameAndAssembly.Length != 2) { throw new ArgumentException( "Specified type location was in an invalid format."); } String className = typeNameAndAssembly[0].Trim(); String assemblyName = typeNameAndAssembly[1].Trim(); ConstructorInfo constructor = null; String cacheKey = contextKey+"."+assemblyName+"."+className; HttpContext context = HttpContext.Current; constructor = LoadConstructorFromCache(cacheKey, context); if (constructor == null) { constructor = CreateNewConstructor(assemblyName, className); CacheDependency dependency = null; context.Cache.Insert(cacheKey, constructor, dependency); } return constructor; } private String contextKey = "Charlie.Framework.PluginFactory"; private ConstructorInfo LoadConstructorFromCache( String cacheKey, HttpContext context) { Object cached = context.Cache[cacheKey]; if (cached != null && cached is ConstructorInfo) { return (ConstructorInfo)cached; } return null; } private ConstructorInfo CreateNewConstructor( String assemblyName, String className) { String assemblyPath = HttpRuntime.BinDirectory + assemblyName; String pluginExtension = ".dll"; if (assemblyPath.EndsWith(pluginExtension) == false) assemblyPath += pluginExtension; Assembly assembly = Assembly.LoadFrom(assemblyPath); if (assembly == null) throw new ArgumentException( "Assembly " + assemblyPath + " could not be found."); Type type = assembly.GetType(className); if (type == null) throw new ArgumentException("Type " + className + " could not be found in " + assemblyName); ConstructorInfo constructor = type.GetConstructor(new Type[0]); return constructor; } }
Charlie’s PluginManager now had an IPlugin interface by which it could work with unknown plugins, and a helper PluginFactory to create instances of specific plugins, and those plugins had the IWebModule and IWebFilter interfaces by which they could participate in the processing of a web requested. The only thing left was the unanswered question of what convention could be used so that all the plugins worked in a similar fashion, even though they all did different things.
by Alister Jones | Next up: The Manager Convention →
----
Post a Comment