Tracking Rules Execution in Windows Workflow

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.