I will describe how to create a Rules Engine Framework using C#.  It will take a few posts to describe the entire process but it's very cool and your going to want to read them all.  In Part 1, I am going to describe the Rules Provider.  To find out more about Providers click here.

Source Code for This Article

Requirements

  • Microsoft Visual Studio .NET 2005
  • Lance's Provider Framework Toolkit Item Template.  If this link ever goes away or becomes unsupported then email me and I will release mine.  I wrote the exact same thing but mine was not a Visual Studio Item Template but I can make it work that way easily enough.

Backstory

I was asked to develop an application that could process inbound emails and apply rules to those messages.  The application would need to be fault taulerant, provide logging, and the rules would need to be dynamic.  I have developed some BizTalk 2004 and 2006 applications and one thing I really liked about the their approach to rules was that they were compiled and not translated.  This approach yields a rule that can deliver high performance and yet remains highly flexible.  And because of my experience with BizTalk I developed some Windows Workflow Foundation rules which worked very well but most developers didn't feel comfortable working with them.  So for those of you who inevitably comment on other solutions well I considered other approaches than writing my own. 

My Rules Provider will allow Rules to be plugged into an applications app.config with little-to-none coding effort.  The Rules developer would have to implement a Rule by inheriting from a common interface (IRule) and that is all that would be required of him.  The Rule Provider takes care of loading the Rules from the app.config.

So now on to Part 1.

Create a Windows Service Solution

Follow my steps for creating a Windows Service in Visual Studio .NET 2005.

The Rules Provider

First, we need an App.config file to hold our Rules.

  1. Right click the EmailProcessor project and Add | New Item

app.config3 

Second, we will generate a Rules Provider using Lance's Provider Framework Toolkit by once again...

  1. Right click the EmailProcessor project and select Add | New Item.
  2. In the Add New Item dialog you will choose Custom Provider-based Framework and name it Rules.

ruleProviderCreation[3] 

Your Solution Explorer will contain the new files generated by the toolkit and it should appear as follows.

SolutionExplorer1[1] 

There are instructions and a sample web.config if you are interested.  Lance's framework has simplified what Microsoft released by making it generate the name of the Provider; otherwise, they are exaclty the same.

At this point we are ready to begin customizing the Provider to include the values we want for our Rules.  Let's first design the Rules into the Provider.

Rule Definition

We need the Rules to be executed seamlessly by the Provider and to get this we need to handle all Rules the same.  This will require an interface for the Rules and an interface for the Rule parameters.  Here is the code for IRule.

using System;
using System.Net.Mail;

namespace EmailProcessor
{
    public interface IRule
    {
        void Execute(MailMessage mailMessage);
    }
}

Code for IParam.

using System;

namespace EmailProcessor
{
    public interface IParam
    {
    }
}

We will also need a concrete Rule that implements IRule.  RulesProvider.cs is the place where this needs to happen.  By implementing IRule in RulesProvider we will be able to iterate across all the providers and call Execute without having to know which Rule is executing.  Modify RulesProvider.cs to use the following code.

using System;
using System.Configuration.Provider;
using System.Net.Mail;

namespace EmailProcessor
{
    public abstract class RulesProvider : ProviderBase, IRule
    {
        private string _connectionString;

        public string ConnectionString
        {
            get { return _connectionString; }
            set { _connectionString = value; }
        }

        private IParam _param;

        public IParam Param
        {
            get { return _param; }
            set { _param = value; }
        }

        public abstract void Execute(MailMessage mailMessage);
    }
}

You will notice a couple of things that have been added.  The interface for Parameters and the Execute method.  IParam is going to store a reference to a concrete parameter object by the concrete rule when it executes.  The Execute method is the entry point for the rule and is where you will do whatever you like to the MailMessage.

Now we need to implement the concrete Rule SpamRuleProvider.  For this example I will modify CustomRulesProvider.cs.  I am going to rename it to be SpamRuleProvider.cs and its job will be to check the subject line for keywords that identify mail as spam and when found it moves the message to a temporary folder.  Your Rule could be anything but my Rule will work well in demonstrating a couple of points - configurable parameters and dynamic rules.  You will need to copy the following code and remove CustomRulesProvider.cs.

using System;
using System.Configuration;
using System.Configuration.Provider;
using System.Net.Mail;

namespace EmailProcessor
{
    public class SpamRuleProvider : RulesProvider
    {
        SpamRuleParam spamRuleParam = new SpamRuleParam();

        public override void Execute(MailMessage mailMessage)
        {
            string[] words = Array.FindAll(mailMessage.Subject.Split(' '), IsSpamWord);

            if (words.Length > 0)
            {
                SmtpClient smtpClient = new SmtpClient();
                smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
                smtpClient.PickupDirectoryLocation = @"C:\temp";
                smtpClient.Send(mailMessage);
            }
        }

        private bool IsSpamWord(string word)
        {
            bool isSpamWord = false;

            foreach (string spamWord in spamRuleParam.SpamWords)
            {
                if (!isSpamWord)
                {
                    isSpamWord = (string.Compare(word, spamWord, true) == 0);
                }
            }

            return isSpamWord;
        }

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            // Validate Input Parameters        
            if ((config == null) || (config.Count == 0))
            {
                throw new ArgumentNullException("You must supply a valid configuration dictionary.");
            }

            if (string.IsNullOrEmpty(name))
            {
                name = "SpamRuleProvider";
            }

            // setup description
            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "SpamRule checks for spam messages.");
            }
            // Description = config["description"];

            // Let ProviderBase perform the basic initialization
            base.Initialize(name, config);

            // setup connectionString
            string connectionStringName = config["connectionStringName"];
            if (string.IsNullOrEmpty(connectionStringName))
            {
                throw new ProviderException("You must specify a connectionStringName attribute.");
            }

            ConnectionStringsSection cs = (ConnectionStringsSection)ConfigurationManager.GetSection("connectionStrings");
            if (cs == null)
            {
                throw new ProviderException("An error occurred retrieving the connection strings section.");
            }

            if (cs.ConnectionStrings[connectionStringName] == null)
            {
                throw new ProviderException("The connection string could not be found in the connection strings section.");
            }
            else
            {
                ConnectionString = cs.ConnectionStrings[connectionStringName].ConnectionString;
            }

            if (string.IsNullOrEmpty(ConnectionString))
            {
                throw new ProviderException("The connection string is invalid.");
            }
            config.Remove("connectionStringName");

            // setup spamWords
            if (string.IsNullOrEmpty(config["spamWords"]))
            {
                throw new ProviderException("You must specify a spamWords attribute.");
            }
            spamRuleParam = new SpamRuleParam(config["spamWords"]);

            if (spamRuleParam.SpamWords.Count == 0)
            {
                throw new ProviderException("The spamWords is invalid.");
            }
            config.Remove("spamWords");

            // Check to see if unexpected attributes were set in configuration
            if (config.Count > 0)
            {
                string extraAttribute = config.GetKey(0);
                if (!String.IsNullOrEmpty(extraAttribute))
                {
                    throw new ProviderException("The following unrecognized attribute was found in " + Name + "'s configuration: '" + extraAttribute + "'");
                }
                else
                {
                    throw new ProviderException("An unrecognized attribute was found in the Rules provider's configuration.");
                }
            }

            Param = spamRuleParam;
        }
    }
}

You can compare the two files to see exactly what I added but here's a short summary.  I initialized the variables that I needed to be used by the concrete rule SpamRuleProvider.cs - those include the SpamRuleParam object which is where the parameters for this rule reside.  Having mentioned that now let me show you the SpamRuleParam.cs class.

using System;
using System.Collections.Generic;

namespace EmailProcessor
{
    public class SpamRuleParam : IParam
    {
        private List<string> _spamWords = new List<string>();

        public List<string> SpamWords
        {
            get { return _spamWords; }
            set { _spamWords = value; }
        }

        public SpamRuleParam()
        {
            
        }

        public SpamRuleParam(string spamWords)
        {
            _spamWords.AddRange(spamWords.Split(' '));
        }
    }
}

There are only a couple of steps left before we will be able to run this code and watch the rules execute.  We need to modify RulesManager.cs by adding the following code to the bottom of it.

public static string ConnectionString
{
    get { return Provider.ConnectionString; }
}

public static string Description
{
    get { return Provider.Description; }
}

private static IParam _param;
public static IParam Param
{
    get { return Provider.Param; }
}

This code simply exposes what gets configured in the App.config to the Rules.  The App.config is the center of all the processing.  Each Rule you create will be added to the app.config and when the application runs it will load these values into the RuleProvider which in turn creates a Params object and calls Execute for each Rule.  Here is the app.config.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="RulesManager"
             type="EmailProcessor.RulesConfiguration, EmailProcessor"
         allowDefinition="MachineToApplication" />
  </configSections>

  <connectionStrings>
    <add name="DefaultConnectionString" connectionString="unused" />
  </connectionStrings>

  <RulesManager defaultProvider="SpamRuleProvider">
    <providers>

      <add name="SpamRuleProvider"
           type="EmailProcessor.SpamRuleProvider, EmailProcessor"
           connectionStringName="DefaultConnectionString"
           description="SpamRule checks for spam messages." 
           spamWords="viagra nigeria" />

    </providers>
  </RulesManager>

</configuration>

Most of this is taken care of by generating the RulesProvider using Lance's toolkit.  What you should notice in the app.config is that each provider has a name and a type attribute.  This allows for Rules to be dynamic.  You would simply create a new Rule in your own namespace and dll and drop it into the folder where this is executing and create an entry in the app.config for your rule and bang it's now executing your rule.  This is the cool part - your code will be flexible and dynamic.  You should also notice the the keyword spamWords="viagra nigeria" in the SpamRuleProvider.  This maps to the values in SpamRuleParam.cs so you could have any values you want in the parameters by passing them into your own concrete parameter object.

And finally - the Service1.cs class needs to be modified so that it loads these rules when the service gets started.  Here is the code for the Start method.

public void Start()
{
     foreach (RulesProvider rulesProvider in RulesManager.Providers)
     {
         MailMessage mailMessage = new MailMessage();
         mailMessage.From = new MailAddress("user@unknown.com", "Unknown User");
         mailMessage.To.Add("niceguy@nospam.com");
         mailMessage.Subject = "I am a viagra salesman from nigeria";
         mailMessage.Body = "Message body goes here.";

         rulesProvider.Execute(mailMessage);
     }
}

You can now put a breakpoint on the foreach in the Start method and hit F5.  You will see it run the SpamRuleProvider and write an Email Message to the C:\temp folder.

I am going to write more on this topic soon...  I will show how to daisy chain the Rules together so that the changes made by a Rule are applied to subsequent Rules.