Click here to Skip to main content
15,880,364 members
Articles / Programming Languages / C#

The "Rule-O-Nator" - An example of dynamically loading classes at runtime

Rate me:
Please Sign up or sign in to vote.
4.81/5 (6 votes)
19 Jun 2013CPOL6 min read 33.2K   206   22   15
Being Dynamic in a Staticly Typed World

Update 

This solution is based on older technology (Prior to .Net 4). While it serves as a decent introduction to Interfaces and late binding, I would HIGHLY recommend learning MEF.  Check out Tim Corey's article From Zero To Proficient in MEF  to learn more.  

 Introduction and Background 

The company I work for owns and operates large ocean going vessels. U.S. maritime payroll is in a breed of its own. A typical able bodied seaman is at sea 60 to 90 days at a time. Communication costs are expensive, and they have limited access to email and the internet between ports. Because of this, we will make payment or disburse allotments on their behalf against their earnings while they are on board. We make car payments, house payments, send money to their wives, children, and girlfriends (the last tends to make us send alimony, and child support payments too!) In addition to getting base pay, they will earn overtime and watch pay. There are several different types and all have Union negotiated rates. It's pretty complex. The typical payroll check or pay voucher may cover 30 to 90 days of pay and they don't get their final payment until they disembark or arrive at certain ports. It's not uncommon for the Voucher summary to be two or three pages long.

Enter "Voucher Checker Version 1.0". I threw together a small utility to validate vouchers to be used in the office in an afternoon. Basically, the app would gather all of the hours worked from the payroll system's database along with all of the deductions and expenses (coms expenses, purchases, advances, etc.) and calculate the seafarers voucher. If the Voucher Checker came up with different amounts than the payroll system did, it would report the exception out in the voucher summary report. As time went on, I'd modify it as needed and republish it in house using Click once deployment. This works great in the office. Then management wanted to push this out to the fleet. I created an install CD and we sent one out to every US flagged ship in the fleet.

All was well and good until the FICA rates went back up from 4.2% to 6.2% at the beginning of the year. All I had to do was change the Employer FICA rate and redistribute the EXE. The problem was the EXE file size is over 10 megabytes. The modified EXE would have to be sent back out to each ship in the fleet and it was going to be over $5,000 to do so. Even sending a CD to a vessel's next port of call is expensive. Typically, we pay fees in excess of $250 to get a CD on board if it has to be hand carried by a shore side shipping agent. It was time to come up with a better alternative.

The Solution

The way out was to obviously move the rules into a separate library, then they could be modified and we'd only have to send the new library to the fleet. The size was small enough (50K), we could just send it by email. Still, it bothered me that if I only needed to make a change to one value on one rule in the future, I'd still have to send all the rules again, and what if management wanted to add more rules in the future? How do you write a program which will allow you to write an unknown rule in the future? 

What I needed to do was create a way to dynamically load my rules at run time. The app would have to automatically detect if new rules were added and it would have to do this with out recompiling the EXE. 

Thankfully, .NET has a concept called an "Interface." Think of an interface as a contract, or an agreement, of how a class will work. It describes the methods, properties, and events but it doesn't include the code to implement a class. I've put together a simple example to demonstrate how easy this is.

The Rule-O-Nator

The "Rule-O-Nator" is a console based app that loads an unknown number of rules at run time, and then processes those rules against an input string to determine if the rule passed or failed and retrieve detailed results of processing the rule. 

It all starts with the Interface. In the solution example you'll find a project called RuleTemplate. It contains a file called IRule.cs.

C#
namespace RuleTemplate
{
    // My Publicly Available Interface
    // to be implemented by all rules.

    public interface IRule
    {
        //Read Only Properties
        string RuleName { get; }
        bool Passed { get; }
        string RuleDescrtiption { get;}
        
        //methods
        bool CheckRule(string Input);
        string Results();
    }
}

Again, all the interface does is define the properties, methods, and events a class must implement to be compliant with the interface. We want the interface DLL file to be extremely small and we don't want the interface to be mired down by any other code. More on this at the end.  

Okay now what??? Add another project file to your solution. Add a reference to the RuleTemplate above, and add a new class. For instance: 

C#
class ContainsBlue : IRule
{
}

Visual Studio 2012 has a great "cheater" shortcut built in. If you right click on IRule, and then click on Implement Interface > Implement Interface, VS will auto-magically build out the skeleton of the class for you. You'll end up with a blank structure which looks like this: 

C#
class ContainsBlue : IRule
{
    public string RuleName
    {
        get { throw new NotImplementedException(); }
    }

    public bool Passed
    {
        get { throw new NotImplementedException(); }
    }

    public string RuleDescrtiption
    {
        get { throw new NotImplementedException(); }
    }

    public bool CheckRule(string Input)
    {
        throw new NotImplementedException();
    }

    public string Results()
    {
        throw new NotImplementedException();
    }

Now, it's just a matter of building out the rule. For example this rule will check the input and see if it contains the word "blue". I made it case insensitive by converting the input string to lower case.

C#
public class ContainsBlue : IRule 
{

    bool? passed = null; //A local variable to maintain the Passed State
    bool IRule.Passed  
    {
        get
        {
            // A little monkeying around because I allow passed to be null
            // so I can send different results if the rule hasn't been run.
            return (passed == true);
        }
    }

    string IRule.RuleName
    {
        get
        {
            return "Contains Blue Rule";
        }
    }

    string IRule.RuleDescrtiption
    {
        get
        {
            return "This rule checks to see if input contains the word \"Blue\"";
        }
    }

    bool IRule.CheckRule(string Input)
    {
        passed = Input.ToLower().Contains("blue");
        return (passed == true);
    }

    string IRule.Results()
    {
        string rtn = "";
        //passed is either null, true, or false
        if (passed == null)
        {
            rtn = "The rule hasn't been run yet.";
        }
        else
        {
            if (passed == true)
                rtn = "Yes, The input had blue in it.";
            else
                rtn = "No blue in the input.";
        }
        return rtn;
    }
}

Here's another rule which counts the number of vowels in a sentence:

C#
public class VowelCount : IRule
{
    // Internally I'm using an int to determine
    // if the rule has passed or run in this class
    int vcount = -1;

    bool IRule.Passed
    {
        get
        {
            return (vcount >= 0 );
        }
    }

    string IRule.RuleName
    {
        get
        {
            return "Count The vowels";
        }
    }

    string IRule.RuleDescrtiption
    {
        get
        {
            return "Counts the vowels in the input.";
        }
    }

    bool IRule.CheckRule(string Input)
    {
        Regex r = new Regex("(a|e|i|o|u)", RegexOptions.IgnoreCase);

        vcount = r.Matches(Input).Count;

        return (vcount >= 0);
    }

    String IRule.Results()
    {
        string rtn = "";
        //Passed is based on vcount in this rule
        if (vcount < 0 )
        {
            rtn = "The rule hasn't been run yet.";
        }
        else
        {
            if (vcount > 0 )
                rtn = "Yes, The input had " + vcount.ToString() + " vowels in it.";
            else
                rtn = "No vowels in the input.";
        }
        return rtn;
    }
}

Two things to note here: While the Passed property is published in the interface as a Boolean, internally I can implement it any way I want. Second, this Rule does something very different than the BlueRule above. Blue looks for a word in the input, Vowels returns a count of the number of vowels.

The zip file has more rules, however, I think you get the idea of what a rule will look like. 

So How do I load the rules up? 

There's one more library in the project called RulesFactory. The rules factory loads a DLL at runtime and scans the DLL for rules and loads them into a dictionary collection.

C#
public class RuleFactory
{

    public Dictionary<string, IRule> Rules = new Dictionary<string,IRule>();

    public void LoadRules(string DllPath)
    {
        // Load the DLL asmebly
        Assembly asm = Assembly.LoadFrom(DllPath);
                      
        var DllRuleSet = from t in asm.GetTypes()
                                where t.GetInterfaces().Contains(typeof(IRule)) 
                                select Activator.CreateInstance(t) as IRule;

        foreach (IRule RuleInst in DllRuleSet)
            {
                Rules.Add(RuleInst.RuleName, RuleInst);
            }
    }
}

The app glues it all together 

All our app has to do now is:

  • Load up all the rules
  • Check Strings against the rules, and 
  • Display the results

Here's a console based example:

C#
 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RuleTemplate;

namespace RuleONator
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("This is an example of dynamically " + 
              "Loaded classes using a common interface \r\n");

            //Create a Rule Factory to load the Rules

            RuleFactory f = new RuleFactory();

            //I chose to put all of my rule DLLs
            //in a subdirectory in the debug folder for this example
            //Now I can use Getfiles to load up all the DLLs,
            //and it will load up and dlls I write 
            //in the future.

            string[] dllfiles = System.IO.Directory.GetFiles(".\\Rules\\", "*.dll");

            foreach (string filename in dllfiles)
            {
                Console.WriteLine("Loading " + filename);
                f.LoadRules(filename);
            }

            //That's done, what rules did we load up?
            
            Console.WriteLine("List of Loaded Rules:");

            foreach (var entry in f.Rules)
            {
                Console.WriteLine("    " + entry.Key);
            }


            Console.WriteLine("Processing Rules:\r\n");

            //Skip to the Process Rules to see how we do this:

            ProcessRules(f, "This is a string with the word BLUE in it.");

            Console.WriteLine("");
            
            ProcessRules( f, "No Color");

            Console.Write("\r\nPress Any Key To Continue..."); Console.ReadKey();

        }

        static void ProcessRules(RuleFactory f, string Input)
        {
            Console.WriteLine("Processing ProcessRules against the folowing string:");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine( "\"" + Input + "\"");
            Console.ForegroundColor = ConsoleColor.White;

            // The rules are loaded up in the RuleFactory Dictionary.
            // All we have to do is run each rule against the input
            // and display the results.

            foreach (KeyValuePair<string, IRule> Rule in f.Rules)
            {
                //Because we're using an interface, you can refrence the properties
                //methods and events

                Console.WriteLine("    Rule: " + Rule.Value.RuleName);

                bool result = Rule.Value.CheckRule(Input);
                if (result == true)
                    Console.ForegroundColor = ConsoleColor.Green;
                else
                    Console.ForegroundColor = ConsoleColor.Red;

                Console.WriteLine("    Results: " + Rule.Value.Results());
                Console.ForegroundColor = ConsoleColor.White;
            }
        }
    }
}

And when it runs, the output looks something like: 

List of Loaded Rules:
    Contains Blue Rule
    How Big Is It?
    Count The vowels
Processing Rules:

Processing ProcessRules against the folowing string:
"This is a string with the word BLUE in it."
    Rule: Contains Blue Rule
    Results: Yes, The input had blue in it.
    Rule: How Big Is It?
    Results: Yes, It's bigger than 10 characters. Infact it's 42 chacaters.
    Rule: Count The vowels
    Results: Yes, The input had 11 vowels in it.

Processing ProcessRules against the folowing string:
"No Color"
    Rule: Contains Blue Rule
    Results: No blue in the input.
    Rule: How Big Is It?
    Results: Nope it's only 8 chacaters.
    Rule: Count The vowels
    Results: Yes, The input had 3 vowels in it.

Press Any Key To Continue...  

except with pretty colors ;o) 

Conclusion 

The most important thing you need to know about interfaces is once you've created them and deployed them in an application, you can't change them without updating every object which uses them and recompiling your entire application. Once you have deployed your app, never change the interface again. If you really have to change the interface, you are better off creating a new one and deploying it on your next version. Here's what Microsoft has to say about it if you want more info. 

Finally, please be kind. This is my first C# post and I'm looking forward to any constructive suggestions for improvement.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder
United States United States
I have a BS in Computer Science. Interests include PHP, Swift, GOLang, and any flavor of SQL.

Comments and Discussions

 
QuestionGreat post Pin
Nipesh Shah20-Jun-13 8:07
professionalNipesh Shah20-Jun-13 8:07 
AnswerRe: Great post Pin
Thomas Haller24-Jun-13 21:17
Thomas Haller24-Jun-13 21:17 
QuestionUpdating the dlls?! Pin
Thomas Haller20-Jun-13 1:06
Thomas Haller20-Jun-13 1:06 
QuestionIOC Container Pin
thebeekeeper19-Jun-13 11:51
thebeekeeper19-Jun-13 11:51 
Questionexcellent first CP article, and a few questions Pin
BillWoodruff18-Jun-13 4:21
professionalBillWoodruff18-Jun-13 4:21 
AnswerRe: excellent first CP article, and a few questions Pin
-james18-Jun-13 18:51
professional-james18-Jun-13 18:51 
QuestionDynamically loading assemblies vs ClickOnce deployment Pin
Matrix_man18-Jun-13 0:10
Matrix_man18-Jun-13 0:10 
AnswerRe: Dynamically loading assemblies vs ClickOnce deployment Pin
-james18-Jun-13 4:29
professional-james18-Jun-13 4:29 
Question10 MB for a Console Application? Pin
Alois Kraus17-Jun-13 20:56
Alois Kraus17-Jun-13 20:56 
AnswerRe: 10 MB for a Console Application? Pin
-james18-Jun-13 17:20
professional-james18-Jun-13 17:20 
Hey Alois,

Thanks for the input. Yes, you're right. The entire install package is 10MB. There are several other 3rd party dependencies.

-james
-james

"If you've got to make code changes in something you didn't write, tread lightly. Your predecessor was regarded as either a genius or a moron. The truth most likely lies somewhere in the middle..."

QuestionNIcely done, but why not MEF Pin
Darek Danielewski17-Jun-13 16:09
Darek Danielewski17-Jun-13 16:09 
AnswerRe: NIcely done, but why not MEF Pin
-james17-Jun-13 18:13
professional-james17-Jun-13 18:13 
GeneralThoughts Pin
PIEBALDconsult17-Jun-13 15:52
mvePIEBALDconsult17-Jun-13 15:52 
GeneralRe: Thoughts Pin
-james17-Jun-13 18:51
professional-james17-Jun-13 18:51 
GeneralRe: Thoughts Pin
PIEBALDconsult18-Jun-13 14:44
mvePIEBALDconsult18-Jun-13 14:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.