Customizing Dovetail Carrier – Consuming Messages With Rule Sets
The next step in our plan to implement a Twitter version of Email Agent using Dovetail Carrier is to consume the direct messages coming from Twitter which we published last time. Message consumers basically…er, well they consume messages. The beauty of the message bus is that we have the ability to subscribe many consumers to the types of messages which we publish. Dovetail Carrier provides a way to compose your message consumers by assembling a set of conditions and actions called RuleSets.
This RuleSet we’ll be examining will investigate an incoming tweet, find the case id (731), and log a note the case.
A Note before I continue: we will be including the Twitter Agent code with future releases of Dovetail Carrier. Additionally, with each release of Carrier you get a lot more extension code examples as we include the source code for our baseline Dovetail Carrier extensions (currently Email Agent, and Communications.)
Introducing the Carrier RuleSet
We wanted to provide a framework which breaks up the processing of the message into a set of conditions and actions called rules. Each rule in the set is executed in order. Rules can have conditions that act as a gate for an action that can be invoked. Additionally, each rule in the set can elect to quit execution of the whole RuleSet and prevent more rules from being executed.
To simplify RuleSet definition we have created a domain specific language (DSL) for defining rules. Let’s take a look at the definition for a very simple RuleSet used by Twitter Agent to log Tweets to cases.
My First Rule Set
DefineRuleSetFor<IncomingDirectMessage>("A direct message can log a note to a case.", rule => { rule.Unless<CaseIdIsPresent>().Exit(); rule.AlwaysDo<LogNoteToCase>(); });
This rule set basically looks for a case identifier in the direct message and when found logs the direct message to the case.
DefineRuleSetFor<IncomingDirectMessage>
If you are not familiar with C# generics the angle brackets surrounding a Type represent the type of .Net object being worked with. You can read this as “Define a rule set for the message type IncomingDirectMessage.” You may remember that this direct message type is the same one we learned how to publish last time.
"A direct message can log a note to a case.",
This is a little description of the rule set which is helpful for summarizing the intent of the RuleSet. These are also used in the application log for context about the message being processed.
rule => { }
This is a lambda expression giving you a rule expression object with which you’ll define rules using a, hopefully easy to read, fluent interface. If you are a little confused and scared about lambdas. It’s just a block of code that gets executed later. Instead of worrying let’s look at more interesting things like next line of code.
rule.Unless<CaseIdIsPresent>().Exit();
The first rule in each rule set is usually a gate that checks to see if the RuleSet should continue. In this case we only want to log a note to a case if we find a reference in the Tweet to a case id. CaseIdIsPresent is a class implementing a condition interface you will need to implement.
Thankfully the condition interface is very simple.
public interface IConditionMESSAGE { bool IsSatisfied(IRuleContextMESSAGE context); }
We will take a look at the code for the CaseIdIsPresent condition in a little bit. First I want to introduce you to actions and the final rule in the set.
rule.AlwaysDo<LogNoteToCase>();
Once we’ve determined that we’ll be processing this direct message we log a note to the case. LogNoteToCase is a class implementing an action interface which, you have likely guessed, need to implement. The Action interface is also very simple.
public interface IActionMESSAGE { void Execute(IRuleContextMESSAGE context); }
Again we will look at the LogNoteToCase code in a bit. I promise. Really. First though I have one more concept I need to introduce.
When implementing the Execute method of the action we’ll need to know the case id so that we can log the Tweet to it. You likely noticed that the CaseIdIsPresent condition was in charge of detection if a legitimate case id is present. Wouldn’t it be nice to be able to communicate important information between conditions and actions and across rules? That is a nice segue into Rule Contexts.
Rule Context
Each condition and action will need some information to get their job done. Most importantly they will need the message being processed. Before each RuleSet is processed Carrier creates a brand new rule context giving it the message to be processed. Each condition and action receives the Rule Context.
It will quickly become apparent that keeping some state around between different conditions and actions is necessary. In the example above the first rule looks for a valid case id in the direct message. If it finds an id it would be handy to push that id into the rule context so that future conditions and actions can know what the case id is without having to figure it out on their own.
Passing information into and between condition and action executions is the job of the rule context.
public interface IRuleContextMESSAGE : IRuleContext { MESSAGE Message { get; set; } } public interface IRuleContext { T FindT() where T : class; bool HasT() where T : class; T PushT(T entity) where T : class; }
You likely noticed that each action and condition gets one of these passed to them. It will likely help to see the rule context in action so let’s take a look at the CaseIdIsPresent condition code.
My First Condition
A condition minimally needs to return true or false. Along the way you might find it useful to Push or Find objects into and out of the rule context.
public static class TweetExpressions { public static Regex CaseIdExpression = new Regex(@"cases(?caseidw+)|#(?caseidw+)", RegexOptions.IgnoreCase); } public class CaseIdIsPresent : IConditionIncomingDirectMessage { private readonly IDovetailSDKToolkitAdapter _dovetailSdkToolkitAdapter; public CaseIdIsPresent(IDovetailSDKToolkitAdapter dovetailSdkToolkitAdapter) { _dovetailSdkToolkitAdapter = dovetailSdkToolkitAdapter; } public bool IsSatisfied(IRuleContextIncomingDirectMessage context) { var match = TweetExpressions.CaseIdExpression.Match(context.Message.Text); if(!match.Success) return false; var caseId = match.Groups["caseid"].Value; var @case = _dovetailSdkToolkitAdapter.GetCase(caseId); if(@case == null) return false; context.Push(@case); return true; } }
This condition uses a regular expression to see if an case id is present in the Tweet. When one is found we check to see if the case exists and if so we push the case into the rule context for future conditions and actions to have access to.
The IDovetailSDKToolkitAdapter class is helper code included with Carrier which wraps our Dovetail SDK and creates a helpful little Case object with only the current useful bits.
public class Case { public string Id { get; set; } public string Title { get; set; } public bool IsOpen { get; set; } }
Nothing special there and it is easy to write your own equivalent, in fact we’ll do that for the Log Note action. I like to keep data access code sequestered to a separate class keeping the concern of the condition focus on its sole purpose of finding the case id and pushing it into the rule context.
My First Action
Actions do things. That is all. Along the way they too may use and add to the rule context to make decisions about what to do.
public class LogNoteToCase : IActionIncomingDirectMessage { private readonly ITwitterExtensionDovetailSDKToolkitAdapter _dovetailSdkToolkitAdapter; public LogNoteToCase(ITwitterExtensionDovetailSDKToolkitAdapter dovetailSdkToolkitAdapter) { _dovetailSdkToolkitAdapter = dovetailSdkToolkitAdapter; } public void Execute(IRuleContextIncomingDirectMessage context) { var @case = context.FindCase(); _dovetailSdkToolkitAdapter.LogNoteToCase(@case.Id, context.Message.Text); } }
This action gets the Case object form the rule context and uses it to log a note to the case. El Fin.
In this case I have created my own Dovetail SDK adapter because the one included with Carrier did not do log note to case.
More About RuleSets
We already showed one example of defining a simple RuleSet. Let’s dive in a bit deeper and see what else there is that we haven’t talked about.
Rule Definitions
In the example above we missed a few of the other rule definit ion options. Here is a sample of everything you can currently do.
DefineRuleSetForMESSAGETYPE("Ruleset name or description.", rule = { rule.WhenConditionIsTrue.DoAction(); rule.UnlessConditionIsFalse.DoAction(); rule.WhenWeShouldExitTheRuleSet.Exit(); rule.AlwaysDoAction(); });
When<Condition> will execute an action when the condition is true.
Inversely a Unless<Condition> will execute the action when the condition is false.
When your rule does not require a condition you can use AlwaysDo<Action> to define an action that will always run.
You can always tack an Exit() onto a rule which will stop execution of the RuleSet when the condition is true (if present.)
We do not support branching behavior of RuleSets. The idea is to keep your RuleSets small, simple, and targeted at doing one thing. If you find yourself wanting a branch within your RuleSet it likely means you should be creating another RuleSet.
The first few rules in the set will likely be gates that decide if the RuleSet that should handle the message being processed.
Database Transactions
The invocation of each RuleSet is done within the context of a database transaction. Your data access classes are highly encouraged to participate in this transaction. Thankfully we make it easy. Let’s take a look a the data access code for LogNoteToCase.
public interface ITwitterExtensionDovetailSDKToolkitAdapter { void LogNoteToCase(string caseId, string notes); } public class TwitterExtensionDovetailSDKToolkitAdapter : ITwitterExtensionDovetailSDKToolkitAdapter { private readonly IClarifyApplicationAdapter _clarifyApplicationAdapter; private readonly IDbTransactionBoundary _dbTransactionBoundary; private readonly DovetailCRMSettings _dovetailCrmSettings; public TwitterExtensionDovetailSDKToolkitAdapter(IClarifyApplicationAdapter
clarifyApplicationAdapter, IDbTransactionBoundary dbTransactionBoundary, DovetailCRMSettings dovetailCrmSettings) { _clarifyApplicationAdapter = clarifyApplicationAdapter; _dbTransactionBoundary = dbTransactionBoundary; _dovetailCrmSettings = dovetailCrmSettings; } public void LogNoteToCase(string caseId, string notes) { var logCaseNoteSetup = new LogCaseNoteSetup(caseId) { Notes = notes, UserName = _dovetailCrmSettings.EmployeeUserName }; var supportToolkit = new SupportToolkit(_clarifyApplicationAdapter.GetApplicationSession()); supportToolkit.LogCaseNote(logCaseNoteSetup, _dbTransactionBoundary.Transaction); } }
Notice the IDbTransactionBoundary interface being used by this data access class. Ignore for now how it gets pushed into the constructor. We will talk about that in a little bit. The Transaction property on this class is guaranteed to be automatically committed when the RuleSet is complete or rolled back if something throws an exception. All you need to remember to do is give it to all of your code that is doing data access. In this case we are delegating to the Dovetail SDK how notes are logged.
This is a powerful yet simple to use feature of Carrier RuleSets that frees you from the drudgery of transaction management. Now what is with all these interfaces being used by the constructors of all these classes.
Inversion Of Control (IoC)
You’ve likely been looking at the constructors in the actions, conditions and the data access class examples above and been wondering why I am pushing in dependencies of the class and not “newing” them up within the class. Dovetail Carrier is built from the ground up around a technique called the Dependency Inversion (DI) principal. This is a big enough topic for a few more blog posts but I will try to summarize.
When building classes we try to push our dependencies into the class (typically as interfaces.) This keeps our code flexible and loosely coupled which means that you can easily change the implementation of dependencies. More importantly you can easily test class behavior by swapping out dependencies for test fakes.
Wait, What Was That?
Did I lose you? Don’t worry it’s less complicated than it sounds. Basically because we are employing something called a Container the construction of any dependencies present in your class’s constructor are auto wired or created and injected for you into the class.
Behind the scenes when your RuleSet is getting ready to be executed we use a container to create your RuleSet which includes all of its conditions and actions and their dependencies. This is why we can easily separate our actions from data access keeping them loosely coupled and easy to unit test.
Remember Transactions? When your conditions and actions take a dependency on IDbTransactionBoundary the container will give your class access to the RuleSet’s active transaction.
What Is Next?
Hmm sorry if that last bit about Dependency Injection lost you. I know it may be a new concept but there is big power there. Hopefully these introductory posts have given you a sense of what it takes to create a Carrier extension.
Here are some ideas I have for follow up posts on Dovetail Carrier.
- RuleSet testing. Show off how easy it is to test your rule sets.
- A post about RuleSet registration would surely be helpful.
- Another post on Twitter Agent featuring the RuleSet we skipped over. Creating cases based on Tweets.