Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Plug-ins in C# 2.0: Generics Enabled Extension Library

0.00/5 (No votes)
7 Nov 2007 5  
A follow up to my previous article, this article takes the plug-in concept and encapsulates it in a Generics enabled library, including support for source code compilation at runtime.

Screenshot - PluginsInCSharp2.jpg

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);
        
  // Configure parameters
  CompilerParameters parms = new CompilerParameters();
  parms.GenerateExecutable = false; //Don't make exe file
  parms.GenerateInMemory = true; //Don't make ANY file, do it in memory
  parms.IncludeDebugInformation = false; //Don't include debug symbols
  
  //Add references passed in 
  if (references != null)
    parms.ReferencedAssemblies.AddRange(references.ToArray());
        
  // Compile            
  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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here