By far my favorite feature of .NET reflection is the ability to easily implement custom plugin libraries.
Whenever you have a nice generic business class that suddenly needs custom functionality for a particular scenario or customer, you usually have three choices:
- Implement the custom code in the library itself.
- This is the quick and dirty way of implementing custom functionality, but if done frequently leads to lots of maintenance problems
- You end up with lots of special-case switch statements or if-elseif clauses
- This requires recompilation and redeployment of the library every time
- It is prone to introducing errors in previously working code
- Implementing the custom functionality through a generic web service interface
- Allows for plug-and-play custom web services to be implemented
- Allows for running custom code on different servers
- Performance is very poor
- Implement a dynamically-loaded custom plugin interface
- Allows for plug-and-play custom DLLs to be implemented
- Performance is basically same as embedding the code in library itself
FuzzySearch Custom Search Modules
The FuzzySearch engine has the ability to call custom search code for complicated searches like location, where the data you are searching for is not simply compared against a database column value.
Originally, we implemented this custom search functionality as a set of generic web service calls. Each custom search type would implement its own version of one or more of the three available web service methods depending on what it needed to do. The URL of the custom web service would then be defined in the search template so the search engine would know where to locate the custom web service.
Everyone loves web services these days and initially using a web service interface did allow us to implement custom search abilities in a very generic and flexible way in the search engine. However, when we started testing with larger databases we realized we had a serious performance issue. The basic overhead of building up and calling a web service method as we iterated over each dataset row slowed the search engine to a crawl.
For example, in a test flight database calling a very basic custom search web service method that did only a few simple math calculations to return a distance given two latitude/longitude values was taking over five minutes(!) when executing over 60,000 rows of flight data.
Obviously, this was completely unacceptable for real-time searching. So first we researched ways to improve the web service performance, either by rewriting the services using WCF, calling them asynchronously, or perhaps batching up the data and making fewer web service calls instead of once per dataset row.
However, none of the potential performance gains, even if they were 2x, 3x, even 20x gains, was going to achieve what we really needed, which was basically the same nearly instantaneous execution time we were getting executing our standard non-custom search code.
We solved the performance problems while maintaining our generic search engine library by using a plugin architecture. Using some simple reflection tricks, we swapped out using the generic web service interface to directly calling custom code DLLs that are loaded at runtime when the template is loaded. The template simply defines the name/location of the custom search module DLL, and the search engine can then dynamically load it, instantiate an instance of the custom search class, and call the custom search code directly.
By being able to load the custom code and call it directly, we were able to reduce the time to execute custom search code on 60,000 records from over five minutes to under half a second! Now that’s a noticeable performance improvement!
Custom Module Sample Code Projects
Here are some sample projects that demonstrate a very simple way to implement a custom plugin interface in .NET. You can also look for the ISearchModule code in the FuzzySearch solution for a more complicated implementation example.
The sample code projects can be found inside the CustomModuleSample solution folder. The sample console TestApp loads two custom plugin libraries that simply output two different formats of “Hello World”. Below is the output you should see when running the TestApp.
Step One – Create your custom interface class library
Create an interface class library DLL and define your custom interface methods.
Step Two – Create your custom class library
Create one or more custom class libraries that implement specific custom functionality. The two sample custom library classes are shown below.
Step Three – Load and invoke the custom class library code in your business class
Now you need to actually load and invoke your custom plugin libraries. You can see how to do this easily in the TestApp Main function shown below.
The main steps required are:
- Get the path to where the custom module DLL resides.
- Load the custom module assembly.
- Loop through the types looking for the one that implements your custom interface.
- Use Activator.CreateInstance to create an instance of this custom module.
- Note that the TestApp knows nothing about the custom module itself. It only knows about the ICustomModule interface.
- This is the “magic” that allows you to create a generic business library that can load custom modules without recompiling or adding references.
This is only the simplest implementation of a plugin architecture, but it is very powerful. There are many additional features you could add easily by tweaking this plugin code to do things like:
- Create multiple instances of a custom module and spin them up in independent worker threads.
- By loading the custom module assembly into a sub AppDomain instead of the application AppDomain, you could not only just dynamically load but also dynamically unload/update plugins without requiring a restart of the application.