How do I write a Plug-in for an application and how do I know whether a DLL implements an Interface that my application expects? Dependency Injection and Inversion of Control are hot topis in OOP development today. These terms sound complex and their implementation can indeed be complex. There are several ways of implementing Dependency Injection, but in this article I am going to describe a type of DI that is not normally covered under the definition of this term.
Simplistically, Dependency Injection means that an application learns about new dependencies at run time rather than compile time. System architects often disagree as to the best way for an application to accomplish this. One of the ways that I have found to be valuable, especially when I do not want to change anything in the base application, including the appConfig file is the use of Plug-ins whose contract verification is determined by the use of Reflection. The reason that I like Plug-ins is that their names can be exposed in database table associated with an application/process and no changes are required for the base application. Some argue that Dependency Injection should be done via the appConfig, but in most Enterprise environments, a change to the appConfig file is just as much a "code move" as an actual change to the application would be. Strictly speaking, it should go through System Testing and Quality Assurance just as surely as any other code change
In this article, I will concentrate on the verification of the implementation of an Interface by a plug-in DLL. It is possible that a DLL might be listed in database table but that it does not implement the specific Interface that an application or one of the processes of an application is concerned with. Trying to call a DLL that does not Implement a specific Interface will obviously result in undesirable consequences.
I am going to assume that we have a application class that processes batches of transactions from a database. The design of the various tables used by this application is not relevant to this article. I will assume three tables including a Batch table, Transaction table, and a Process table. I will assume that this applcation will perform certain actions during the processing of a a batch and its associated transactions. I will keep the number of actions and their respective processes simple so that I may concentrate on the mechanics of validating contracts. Figure 1 shows a basic Interface (Contract) Class Library (DLL) project that represents the basic points within the application where I will allow a Plugin to be called, should one be found that wants to be called.
Figure 1 - Basic Interface
| namespace PluginInterface1 { public interface BasicActions { bool BatchStartup(int batchNumber, int processNumber); bool ProcessTransaction(DataRow dr); void BatchComplete(); } } |
| using System; using System.Collections.Generic; using System.Linq; using System.Text; using PluginInterface1; using ValidateContract; using System.Data; namespace PluginApplication { class Program { private static PluginInterface1.IBasicActions plugin; static void Main(string[] args) { // first find if a plugin exists object dllObject = CheckForContract.VerifyPluginInterface( @"C:\VS2008 Projects\PluginApplication\PluginTest\bin\Debug\PluginTest.dll", "PluginInterface1.IBasicActions"); if (dllObject != null) { plugin = (PluginInterface1.IBasicActions)dllObject; } // later in the execution when the batch // is ready for processing // if plugin is not null, we already have // an instance of the pluging // and we can call any of its public methods // any time during the application // without using reflection again... if (plugin != null) { int batchNbr = 12345; int processNbr = 92; if (plugin.BatchStartup(batchNbr, processNbr)) Console.WriteLine("continue processing batch"); else { Console.WriteLine("plugin said to bypass the batch"); return; } } // here we might be processing a transaction if (plugin != null) { DataRow dr = null; if (plugin.ProcessTransaction(dr)) { // plugin returned true, process the transaction } else { // plugin returned false, bypass the transaction } } else { // process the transaction normally, no plugin exists } // finally, we have completed processing the // batch, just tell the plugin if (plugin != null) { plugin.BatchComplete(); } } } } |
| using PluginInterface1; using System.Reflection; using System.IO; using System; namespace ValidateContract { public class CheckForContract { public static object VerifyPluginInterface(string pluginName, string interfaceFullName) { if (pluginName != null && File.Exists(pluginName) && Path.GetExtension(pluginName).ToLower().Equals(".dll")) { // an Assembly is not an instance of the dll // it is an object that can be examined by reflection Assembly pluginAssembly = default(Assembly); // attempt to create an instance of the object DLL // using reflection try { pluginAssembly = Assembly.LoadFrom(pluginName); } catch (Exception ex) { return null; } object dllInstance = CheckAssemblyForInterface(pluginAssembly, interfaceFullName); return dllInstance; } return null; } private static object CheckAssemblyForInterface(Assembly plugin, string interfaceFullName) { //Type objType; // = default(Type); Type objInterface; // = default(Type); //Loop through each type in the DLL foreach (Type objType in plugin.GetTypes()) { //Only look at public types if (objType.IsPublic == true) { //Ignore abstract classes if (!((objType.Attributes & TypeAttributes.Abstract) == TypeAttributes.Abstract)) { //See if this type implements our interface objInterface = objType.GetInterface(interfaceFullName, true); if ((objInterface != null)) { string className = objType.FullName; object returnObject = plugin.CreateInstance(className); return returnObject; } } } } return null; } } } |
| using System.Collections.Generic; using System.Linq; using System.Text; using PluginInterface1; using System.Data; namespace PluginTest { public class Class1 : PluginInterface1.IBasicActions { public bool BatchStartup(int batchNumber, int processNumber) { Console.WriteLine("BatchStartup in plugin was called"); return true; } public bool ProcessTransaction(DataRow dr) { Console.WriteLine("ProcessTransaction in plugin was called"); return true; } public void BatchComplete() { // do something when batch completes Console.WriteLine("BatchComplete in plugin was called."); } } } |
| using System; using System.Collections.Generic; using System.Linq; using System.Text; using PluginInterface1; using ValidateContract; using System.Data; namespace PluginApplication { class Program { private static PluginInterface1.IBasicActions plugin; static void Main(string[] args) { // first find if a plugin exists object dllObject = CheckForContract.VerifyPluginInterface( @"C:\VS2008 Projects\PluginApplication\PluginTest\bin\Debug\PluginTest.dll", "PluginInterface1.IBasicActions"); if (dllObject != null) { plugin = (PluginInterface1.IBasicActions)dllObject; } // later in the execution when the batch // is ready for processing // if plugin is not null, we already have // an instance of the pluging // and we can call any of its public methods // any time during the application // without using reflection again... if (plugin != null) { int batchNbr = 12345; int processNbr = 92; if (plugin.BatchStartup(batchNbr, processNbr)) Console.WriteLine("continue processing batch"); else { Console.WriteLine("plugin said to bypass the batch"); return; } } // here we might be processing a transaction if (plugin != null) { DataRow dr = null; if (plugin.ProcessTransaction(dr)) { // plugin returned true, process the transaction } else { // plugin returned false, bypass the transaction } } else { // process the transaction normally, no plugin exists } // finally, we have completed processing the // batch, just tell the plugin if (plugin != null) { plugin.BatchComplete(); } } } } |
| Ask a Question, or give your feedback on my articles or products by going to the KnowDotNet Forum or by clicking on My Blog. | ![]() |