How to write a Plug-in for an application in .NET

in #utopian-io7 years ago (edited)

What Will I Learn?

You will learn:

  • Writing Plug-Ins for Loose Coupling of Application Components
  • Implementing Interfaces and Using Reflection For Dependency Injection

Requirements

.NET Framework

Difficulty

Intermediate

Tutorial Contents

  1. Basic Interface
  2. Main Application
  3. Validate Contract DLL
  4. Plugin Code

How to write a Plug-in for an application in .NET

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 tutorial, 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();
   }
}

Next, I will write a simple application that will process a batch and one transaction.  Normally, there would be a loop through the transactions in the batch, but I will ignore that because that is not relevant to showing how to use the plug-in.  Figure 2 shows the application that can call the plug-in.

Figure 2 - Main Application

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:\VS 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();
         }
      }
   }
}

Next, I will write the Plugin Verification DLL.  Note that this component accepts a path to a DLL and the fully qualified Interface name as a string.  It returns an instance of the plugin DLL, if it implements the interface, as Type object.  It is then up to the main program to cast the plugin instance to the correct type before attempting to call its methods.  Without the cast you will not have Intellisense on the object.  I have left the casting of the object in the main program so that the Validation DLL can validate any Interface whose name is passed as a string.  Figure 3 shows the code for the Plugin Verification component.

Figure 3 - Validate Contract DLL


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;
      }
   }
}

Next, I will write a simple plugin DLL project that will implement the three methods of the Interface.  Note that you must implement all methods of the Interface whether or not you have anything to do in them.  In this particular plugin, I will return true in the first two method, telling the main program that I want to process the batch and that I want to process the transaction.  Returning false would tell the respective methods that I do not want to process the batch or the transaction.  What this does is allows the plugin to alter the normal processing in the main application.  If there is no plugin, the batch and it's transactions will be processed.  However, the methods of the plugin could query database, etc., to determine whether or not to process the batch or particular transactions.  That's the beauty of the ability of run-time plugins to customize or modify the action of an application.  Figure 4 shows the code for the plugin.

Figure 4 - Plugin Code

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.");
      }
   }
}

Finally, I will show the code in the main application.  In each set of code the "using" directives will show you the dependencies with respect to the various components of the five projects in this solution.  Figure 5 shows the code for the main application.

Figure 5 - Main Application

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:\VS 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();
         }
      }
   }
}

Once the application is built and tested, including the verification of a single plug-in, you can write any number of plug-ins without having to touch the base application.  Some do not like the thought of using Reflection at run-time, saying that Reflection is slow.  However, you can see from the example that Reflection is used only once in the application and from then on, the application simply tests whether there is a plug-in object to call and if so, it calls it.  No big deal.  Sure, I have to go through unit and system testing of any new plugin, but I never touch the application or its appConfig file and discovery of new plug-ins is totally dynamic and seamless to the base application. 



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @carver I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.19
TRX 0.15
JST 0.029
BTC 63792.82
ETH 2563.50
USDT 1.00
SBD 2.66