In my previous blog entry on Tracking and the TrackingExtract functionality I talked about the ability to track the rules that fired.  As part of this functionality I needed to capture the rules that fired as well as to capture an XML representation of the object state (this is where the TrackingExtract functionality came in) to the database so that reporting systems could look at the data.  When I first started researching to find out if this functionality existed I came across a great tracking sample on Moustafa Ahmed's blog.  This sample shows how you can use the standard tracking infrastructure to track the rules. 

Unfortunately, the standard tracking functionality didn't do exactly what I was looking for.  First, the data is written to the UserEvent table in the UserData_Blob field and shows up as 0x0001000000FFFFFFFF01000000000000000C020000005D53797374656D2E576F726B66......., which, represented the serialized object, but was somewhat unreadable. Next, I would need to deserialize this data and cast it to a RuleActionTrackingEvent type in order to work with it for reporting.  And lastly, I wanted to store additional data that was not part of the UserEvent table.

So, I needed to augment the standard functionality with my functionality.  I started by creating two new tables that would live in the tracking database.  The first was the RulesTrackingData table and the second was the UserTrackingData table.  They were defined as

CREATE TABLE [dbo].[RulesTrackingData](
    [id] [bigint]
IDENTITY(1,1) NOT NULL,
    [ComputerName] [varchar](25)
NOT NULL,
    [ActivityType] [varchar](50)
NOT NULL,
    [InstanceID] [varchar](40)
NOT NULL,
    [EventDateTime] [varchar](50)
NOT NULL,
    [PolicyName] [varchar](50)
NOT NULL,
    [RuleName] [varchar](40)
NOT NULL,
    [ConditionResult] [varchar](10)
NOT NULL,
    [TrackingRecord] [varchar](500)
NOT NULL
)

and

CREATE TABLE [dbo].[UserTrackingData](
    [id] [bigint]
IDENTITY(1,1) NOT NULL,
    [ComputerName] [varchar](25)
NOT NULL,
    [ActivityType] [varchar](50)
NOT NULL,
    [InstanceID] [varchar](40)
NOT NULL,
    [EventDateTime] [varchar](50)
NOT NULL,
    [TrackingRecord] [varchar](500)
NOT NULL

)

The RulesTrackingData table will hold the tracking data for each rules that runs while the UserTrackingData will hold the state of the object after all of the rules have run.

After the database had been setup it was time to work on the code.  I created a custom tracking service for the Workflow Runtime.  As part of my RuleTrackingService I created a RuleTrackingChannel class which inherited from TrackingChannel and a RuleTrackingService class which inherited from TrackingService.  To find out more about the RuleTrackingService take a look at my previous blog entry on Tracking and the TrackingExtract functionality.  Right now we are just going to focus on the RuleTrackingChannel class since this is where we get to grab the tracking data and write it the way we want to the database. 

So the code looked like this:

public class RuleTrackingChannel : TrackingChannel

{
............
 

public RuleTrackingChannel(........)
{
}
//This is what it all comes down to!
protected override void Send(TrackingRecord record)
{

    if (record is UserTrackingRecord)

{
DetermineTrackingRecordType((UserTrackingRecord)record);
}

}

private void DetermineTrackingRecordType(UserTrackingRecord userTrackingRecord)
{

    if (userTrackingRecord.UserData is RuleActionTrackingEvent)

{
WriteRuleTrackingRecord((RuleActionTrackingEvent)userTrackingRecord.UserData, userTrackingRecord);
}

    else

{
WriteUserTrackingRecord(userTrackingRecord);
}

}

// This method writes a record to the database for each rule that fires. This record represents the data before the rule fired.
private void WriteRuleTrackingRecord(RuleActionTrackingEvent ruleActionTrackingEvent, UserTrackingRecord userTrackingRecord)
{

string PropXml = string.Empty;
if (this._trackedProperties != null)

{
PropXml = CreateXmlFragment(userTrackingRecord, "RuleTrackingSnapshot");
}

//At some point we might want to change this to a stored proc
SqlCommand command = new SqlCommand();
command.Connection =
new SqlConnection(_connectionString);
command.Connection.Open();
SqlDataAdapter adapter =
new SqlDataAdapter();
StringBuilder commandString =
new StringBuilder();

commandString.Append ("INSERT INTO TrackingStore..RulesTrackingData ");
commandString.Append("(ComputerName, ActivityType, InstanceID, EventDateTime, PolicyName, RuleName, ConditionResult, TrackingRecord)");
commandString.Append(String.Format("VALUES ('{0}','{1}','{2}','{3}','{4}','{5}','{6}','{7}')", System.Environment.MachineName, userTrackingRecord.ActivityType.FullName.ToString(),
this._instanceID ,System.DateTime.Now, userTrackingRecord.QualifiedName.ToString(), ruleActionTrackingEvent.RuleName.ToString(), ruleActionTrackingEvent.ConditionResult.ToString(), PropXml));

command.CommandText = commandString.ToString();
command.ExecuteNonQuery();
command.Connection.Close();
}
 

private string CreateXmlFragment(UserTrackingRecord userTrackingRecord, string rootNodeName)
{

XmlDocument doc = new XmlDocument();
XmlDocumentFragment doctype;
doctype = doc.CreateDocumentFragment();
doc.AppendChild(doctype);
doc.AppendChild(doc.CreateElement(rootNodeName));
XmlNode root = doc.DocumentElement;

for(int i = 0; i < userTrackingRecord.Body.Count; i++)
{

XmlElement elem = doc.CreateElement(userTrackingRecord.Body[i].FieldName.ToString());
elem.InnerText = userTrackingRecord.Body[i].Data.ToString();
root.AppendChild(elem);

}

    return doc.InnerXml;

}

 

In the Send method I make sure that I am only grabbing UserTrackingRecord objects and then I check to determine if I have a RuleActionTrackingEvent object.  If so then I am ready to start writing to the RulesTrackingData table.  In the WriteRuleTrackingRecord method I create the XML snapshot of the object state and then create my SQL insert statement.  Once the Send method is called I can start to grab data from the RuleActionTrackingEvent object and the UserTrackingRecord object to fill in the table with the data we need.  I have included the computer name as one of the fields since the rules run on the client side I need to make sure that I can determine where the rules ran.  Since this is a snapshot of code, I have left out some other functionality and did not take time to refactor the code for this example and could have consolidated it a bit.  Anyways, once this code runs and data is written into the RuleTrackingData table it will look like the following (I will let you compare the fields with the table structure above (I put a : as a field separator to make it easier to read)):

4 : WFTEST : System.Workflow.Activities.PolicyActivity : 4091dc04-43fe-4dde-8b80-ba837c9cc9a0 : 8/30/2006 7:03:37 AM : simpleDiscountPolicy : ResidentialDiscountRule : True : <RuleTrackingSnapshot><orderValue>600</orderValue><discount>2</discount></RuleTrackingSnapshot>
 

As you look at all of the data written to the database you should pay close attention to the ConditionResult field (which gets populated from  ruleActionTrackingEvent.ConditionResult) .  This will show 'true' for each rule in which the predicates match and the condition is executed.  This column will show 'false' for a rule where the predicates do not match and the else condition was executed.  For a rules in which the predicates do not match and there is no else condition then a record will not be written to the database table.  In addition, the data that is available to track contains the state of the object before the rules execute and therefore the database contains the before snapshot.  When many rules run it becomes quite easy to select the rules and look to see what rules had what affect.  The problem is that we also needed to have a snapshot of the object after all of the rules ran.  In the code able there is a call to a method that I didn't include called WriteUserTrackingRecord.  This method is very similar to the WriteRuleTrackingRecord method but only contains enough data to join to the data in the RulesTrackingData table and to contain an XML representation of the end state.  What makes this different is that in order to get this final state I needed to place a Code Activity after my Policy Activity.  In the Code Activity I included the following line of code:

this.TrackData("WholeObject", crr);

where crr is the object that I passed into the rules engine.

The TrackData takes the object and passes it as a UserTrackingRecord which when I receive it I call the WriteUserTrackingRecord method.  Since I am taking this snapshot after the policy runs I will get one of these database entries whereas I will get as many database entries as the number of rules that run in the RulesTrackingData table.