Windows Workflow Tracking and the TrackingExtract functionality

I was writing a
custom tracking service for the Windows Workflow engine for my client that will
be used to track the rules that fired.  One of the things that I wanted to do
was to track the values of the properties that my rules would touch.  I would
then create an xml document which would be stored in a row in my tracking
database. 

 

The challenge
was to find a way to get the internal tracking functionality to grab these
property values.  In addition, I only wanted the properties that I was
interested in and not every property on the class.  I found that the tracking
infrastructure provides this functionality in the WorkFlowDataExtract class. 

 

I did a search
using my favorite search engine to see if there was anything documented on this
subject.  I was surprised at the lack of information on this functionality. 
There was one post on the WF

Forums, which is a great resource and I highly recommend looking at these
posts for information, but, unfortunately, it didn't expand on this class. 
Therefore, this seemed like the perfect opportunity for a blog entry.

 

First,
lets look at what the class does.  There are actually two classes that provide
TrackingExtract functionality.  They are the WorkflowDataTrackingExtract
class and the ActivityDataTrackingExtract class.  These classes take the
name of a property or field that should be extracted from the root activity of
the workflow and sent to the tracking service when a tracking point is matched. 

 

The difference
between these two classes is that the ActivityDataTrackingExtract class is bound
directly to a specific activity whereas the WorkflowDataTrackingExtract can be
assigned anywhere across the workflow.  Remember though that these classes are
'bound' to the tracking point and can be used in either the Extracts properties
of either the UserTrackPoint or ActivityTrackPoint classes.

 

The data that is
extracted is placed in either the ActivityTrackingRecord or the
UserTrackingRecord. 

 

Use the Member
method to specify the field or property to extract.  You can also associate
additional information with the extracted data by using the Annotations
functionality.

 

So, I wanted to
accept a list of properties that were important to the workflow developer, in
this case I used the List<string> generic type, which was passed in when
instantiating the RulesTrackingService.  This list gets created in the
program.cs file in the Main method as listed in the following code:

 

static
void
Main()

    {

       
WorkflowRuntime workflowRuntime =
new
WorkflowRuntime();

       
string connectionString =
"Initial Catalog=TrackingStore;Data Source=localhost; Integrated Security=SSPI;";

       
List<string>
trackedProperties = new
List<string>();

trackedProperties.Add("orderValue");

trackedProperties.Add("discount");

       
workflowRuntime.AddService(new
RulesTrackingService(connectionString,trackedProperties));

       
...............

    }

 

Once the
RulesTrackingService gets instantiated there are two interesting pieces of code. 
The first is the GetTrackingChannel method.  This returns a
RuleTrackingChannel object (which inherited TrackingChannel) which is where you
will provide the code to operate on the tracking data.  In my case, I took
the object data and serialized it into XML so that I could place it in a row in
the database. The second is the the GetProfile method.  Within this method,
I have a foreach loop where I cycle through the List<> and for each entry I
create a new WorkflowDataTrackingExtract object passing the property on the
constructor and then add the extract object to the userTrackPoint.Extracts
collection as shown in the code below.

 

public
class
RulesTrackingService : TrackingService

{

...........

...........

   
public RulesTrackingService(string
connectionString, List<string>
TrackedProperties)

    {

       
this._connectionString
= connectionString;

trackedProperties = TrackedProperties;

}

   
protected
override
TrackingChannel GetTrackingChannel(TrackingParameters parameters)

    {

   
if (trackedProperties !=
null)

    {

       
return
new
RuleTrackingChannel(parameters,
this._connectionString,
trackedProperties);

}

   
else

    {

       
return
new
RuleTrackingChannel(parameters,
this._connectionString);

}

}

   
private
static TrackingProfile GetProfile()

    {

       
TrackingProfile profile = new
TrackingProfile();

       
profile.Version = new
Version("1.0.0");

       
UserTrackPoint userTrackPoint =
new UserTrackPoint();

       
UserTrackingLocation userLocation =
new
UserTrackingLocation();

       
..........

       
..........

       
userTrackPoint.MatchingLocations.Add(userLocation);

       
foreach (string trackedProp
in trackedProperties)

        {

           
WorkflowDataTrackingExtract wkflowExtract =
new
WorkflowDataTrackingExtract(trackedProp);

           
userTrackPoint.Extracts.Add(wkflowExtract);

        }

       
profile.UserTrackPoints.Add(userTrackPoint);

       
return profile;

    }

}

 

The data that is
returned from the tracking point for each of the tracked properties represents
the data before the rule runs.  This was great for the 'before' snapshot
but I also needed to see the 'after' snapshot.  For this functionality, I
added a Code activity onto my workflow after the Policy activity.  In this
activity I pass the object (in this case it is crr) that the rules operate on to
the TrackData property which creates a UserTrackingRecord as shown in the code
below.

 

private
void
codeActivity1_ExecuteCode(object
sender, EventArgs e)

{

    this.TrackData("WholeObject",
crr);

}

 

Earlier I talked
about the TrackingChannel functionality.  In this class I check to see if
the tracking object is my custom type or if it is a base UserTrackingRecord
(which is what the .TrackData creates).  If it is the UserTrackingRecord I
again serialize and place this in a different table in my database.  I
place it in a different table since there will be one of these for each policy
whereas there will be many records for the rules tracking records.  I place
keys on the tables so that they can be related and selected at a later time to
actually investigate what occurred in the rules processing.

 

I now have a
tracking service that tracks each rule that fires, including the before
shapshot, as well an entry for the data after all of the rules have fired on the
object.