Introduction
If you have not already read my first article (from several years ago), please do so now.
Now that you are done, welcome back! I've received a lot of positive feedback regarding the first article, and had always intended on writing another one on the topic, but years passed. Well, here it finally is, long overdue!
The purpose of this particular article is to take some of the functionality we discussed in the last article and encapsulate it into a simple, easy to use library that anybody can include in their own projects. We'll take it one step further by making use of Generics. As a bonus, we'll also build in the ability for it to load source files as plug-ins (they will be compiled at runtime using CodeDom). In this light, an appropriate name for the library would be ExtensionManager, which is what I'm calling it, since it will load compiled assemblies and un-compiled source files in the same manner.
Extensions
First off, let's define what an Extension really is, in this case. We have two levels of extensions in this library, if you want to think of them that way. First, we have an Extension<ClientInterface>
object which isn't an extension, but a wrapper to store information for an actual extension. This wrapper's job is just to store information that we need for each extension, in addition to the methods and properties of the actual extensions.
Our extension wrapper will include several bits of data. It will accept a single generic parameter which will be the plug-in interface (IPlugin
in our last article), or as we now call it, ClientInterface
, which will specify the interface that all acceptable extensions will need to inherit.
We also want to store the filename of the extension in our wrapper and what kind of extension it is (an assembly or source file). Even though it won't matter for assembly extensions, if the extension is a source file, we need to keep track of the Language
of that source file for compilation later.
Finally, the extension wrapper needs to store an instance of the actual extension once it is loaded. In this project, we also expose the Assembly
object of that instance, in case it is needed later. (Similarly, there is also a GetType(string name)
method that looks for a type in the extension's assembly object. This is needed since the Type.GetType()
method will not look in our extension assemblies for types.)
public class Extension<ClientInterface>
{
public Extension()
{
}
public Extension(string filename, ExtensionType extensionType,
ClientInterface instance)
{
this.extensionType = extensionType;
this.instance = instance;
this.filename = filename;
}
private ExtensionType extensionType = ExtensionType.Unknown;
private string filename = "";
private SourceFileLanguage language = SourceFileLanguage.Unknown;
private ClientInterface instance = default(ClientInterface);
private Assembly instanceAssembly = default(Assembly);
public ExtensionType ExtensionType
{
get { return extensionType; }
set { extensionType = value; }
}
public string Filename
{
get { return filename; }
set { filename = value; }
}
public SourceFileLanguage Language
{
get { return language; }
set { language = value; }
}
public ClientInterface Instance
{
get { return instance; }
set { instance = value; }
}
public Assembly InstanceAssembly
{
get { return instanceAssembly; }
set { instanceAssembly = value; }
}
public Type GetType(string name)
{
return instanceAssembly.GetType(name, false, true);
}
}
ExtensionManager
Now, let's focus on the ExtensionManager
object. This is the core of the library, and as its name implies, it is responsible for finding, loading, and managing all of our extensions.
I'm going to go through the ExtensionManager
in logical order of its usage. First of all, you need to tell the manager what file extensions it should be on the lookout for, and how they should be mapped. For this purpose, ExtensionManager
has two properties: SourceFileExtensionMappings
and CompiledFileExtensions
.
SourceFileExtensionMappings
is a dictionary that will be responsible for mapping certain file extensions to certain languages. For example, if we wanted to map a custom file extension of ".customcsharp" to C#, we would call:
SourceFileExtensionMappings.Add(".customcsharp", SourceFileLanguage.CSharp);
In the above example, all *.customcsharp files that the ExtensionManager
finds will be compiled as C# files.
CompiledFileExtensions
is a simple string list containing extensions that should be loaded as assemblies that are compiled already. Typically, you would do:
CompiledFileExtensions.Add(".dll");
This would treat any .dll extension file as a compiled assembly.
You can optionally call the method ExtensionManager.LoadDefaultFileExtensions()
which will load .cs, .vb, and .js as SourceFileExtensionMappings
, as well as .dll as a CompiledExtension
.
The next step is to tell the ExtensionManager
where to look for extensions to load. You can load a single file, or look through a directory of files with the LoadExtension()
and LoadExtensions()
methods, respectively. It's pretty common to set something up like this, where your extensions are stored in the "Extensions" directory in your application's directory:
myExtensionManager.LoadExtensions(Application.StartupPath + \\Extensions);
It is in these methods that the extension manager will decide if the file is a compiled assembly or source code file, and take the appropriate action to load it into memory. It will call the private methods loadSourceFile
or loadCompiledFile
, as appropriate.
Loading Source Code Files
Since my first plug-in article already covers the concept of loading an assembly based on it containing a certain interface, I'm not going to cover that again here. I will, however, go over the process of taking a source code file and compiling it, and loading it all in memory.
The real magic here is in System.CodeDom.Compiler
. This lets us take the source code and compile it into an assembly with relative ease! Our loadSourceFile
private method calls upon another private method compileScript
which handles the process of taking the source file and getting it into an Assembly
. From there, the rest of the process is the same as loading a compiled assembly. The only other thing to note is that loadSourceFile
will raise an AssemblyFailedLoading
event if there are compilation errors. The AssemblyFailedLoadingEventArgs
for this will provide information about the compilation errors that could be used in your application for debugging.
In compileScript
, all we're doing is creating a CodeDomProvider
based on the given language. Now, by default, CodeDom supports C#, VB ,and JavaScript. Other languages may be supported, but you would have to download the appropriate assemblies and reference them in this project to include them. IronPython might be a nice CodeDom provider to include in this for your own use!
Other than that, we set some parameters to tell CodeDom not to produce an executable, and to compile to memory. We also specify to leave out debugging symbols. You could expose this option as a property of ExtensionManager
, if you desire.
The other very important step here is telling the CodeDom what references to use. With this, you can reference third party assemblies for the ExtensionManager
to use. These can be setup in the ExtensionManager
's ReferencedAssemblies
property.
Finally, we invoke the compiler and return its results. CodeDom, as you can see, is very simple to work with!
private CompilerResults compileScript(string filename,
List<string> references, string language)
{
System.CodeDom.Compiler.CodeDomProvider cdp =
System.CodeDom.Compiler.CodeDomProvider.CreateProvider(language);
CompilerParameters parms = new CompilerParameters();
parms.GenerateExecutable = false;
parms.GenerateInMemory = true;
parms.IncludeDebugInformation = false;
if (references != null)
parms.ReferencedAssemblies.AddRange(references.ToArray());
CompilerResults results = cdp.CompileAssemblyFromFile(parms, filename);
return results;
}
Other Considerations
As with my previous article, I do have a method to 'Unload
' an extension, but this doesn't really unload the extension. The problem is, we are loading all of our extensions into the same AppDomain as the host. You can only truly unload assemblies from an AppDomain by unloading the AppDomain itself. As a future consideration, I may make ExtensionManager
truly load extensions into their own AppDomain. For now, this is not an option.
There are a couple other events that ExtensionManager
exposes. AssemblyLoading
and AssemblyLoaded
provide notification of what they sound like they would.
Example Solution
I've included an example solution to help you understand how to implement this in your own projects. There are seven projects in this solution (four of them are extensions):
- HostApplication – The startup program that hosts all of our extensions.
- Common – Simply an assembly defining the interfaces for the host and extensions, all extensions and the host need to reference this common assembly.
- ExtensionManager – The library we just talked about?
- ExtensionOne – Simply updates the host status with the current time.
- ExtensionTwo – A .cs file, not a compiled extension, that updates the host status with the OS version.
- ExtensionThree – Asks the user for input, and updates the host status with whatever the user entered.
- ExtensionFour – Another .cs file; this time, it has an error in the code, and will not compile, but invokes an error message in the host.
It is worth noting, each extension project has a post build event to copy the extension (whether it be a .cs or a .dll file) to the HostApplication's Extensions folder. You should just be able to compile everything and run the HostApplication for a demo.
Conclusion
This ExtensionManager is fairly simplistic, not full of features, but it does what it is intended to do. I've used it countless times in my own projects where I've needed to provide a quick and simple way of extending my application.
Maybe, it can serve as a base from which you build upon, or perhaps, it does just what you need. Either way, I hope you have found this article useful. Enjoy!
History
- October 31, 2007 - Original submission to CodeProject.