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.)

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:

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:

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!

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!

Comments and Discussions

Do you know how I can tell the plugin where to look for the addtional assembiles which are addtional 3rd party dlls loaded as referances in the project?

I'm getting the following error for a referance loaded in my pluging. I've tried to copy the DLLs into the application bin directory and the directory where the plugin is loading from.

"terminating because of unexpected exception.
System.IO.FileLoadException: Could not load file or assembly 'Interop.jmail, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)"

I haven't had a chance to work much with System.AddIn, but I've read about it a fair bit. I definitely think that's going to be the way to go of course in the future, but this works with .NET 2.0 right now, and that's what it is targeted at. There's also Mono.Addins which is a viable option right now. One issue I found with Mono.Addins (and I'm not sure the same exists with System.AddIn is that without changing the source code, there was no way to compile source files at runtime to be used as extensions. I've had a couple projects where it was very desirable to be able to do so, thus ExtensionManager was useful.

I'll consider writing some article on System.AddIn after I've had a chance to use it more thoroughly.

Oops, I should say that if you don't want to hold a lock on the assembly then maintain the original way of loading the raw bytes, but pass in the pdb raw bytes as well. Clean this code up, but as a rough example:

Thank you!!! I have a similar situation, and I've been searching for days for a way to debug the code being compiled with CompileAssemblyFromSource(). This works! I knew it must be possible somehow. It's going to be so much easier to debug this stuff now. There isn't a smiley with a big enough smile!

The key for me was to set a TempFileCollection that doesn't automatically delete the files used by the compiler so that the source is still available to the debugger, because the orginal is stored in a SQL Server table.