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.
namespace RuleTemplate
{
public interface IRule
{
string RuleName { get; }
bool Passed { get; }
string RuleDescrtiption { get;}
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:
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:
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.
public class ContainsBlue : IRule
{
bool? passed = null; bool IRule.Passed
{
get
{
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 = "";
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:
public class VowelCount : IRule
{
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 = "";
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.
public class RuleFactory
{
public Dictionary<string, IRule> Rules = new Dictionary<string,IRule>();
public void LoadRules(string DllPath)
{
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:
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");
RuleFactory f = new RuleFactory();
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;
foreach (KeyValuePair<string, IRule> Rule in f.Rules)
{
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.