Tracking Objects and Querying For Them

A sample that I like to give during WF talks is that the tracking service can be used to track things at an event level ("Workflow started") as well as things at a very granular level ("What is the Purchase Order amount?"). 

I wanted to put together a sample showing these, and you can find the file on netfx3.com

In order to track things, we first need to specify the tracking profile.  This tells the workflow runtime which events and pieces of data we are interested in collecting while the workflow executes.  I think that the xml representation of the tracking profile is pretty readable, but there is a tool that ships in the Windows SDK designed to make this even easier.  The tool can be found after extracting the workflow samples to a directory under \Technologies\Applications\TrackingProfileDesigner.  This tool will let you open up a workflow by pointing at its containing assembly and then design a tracking profile.  It will deploy the tracking profile to the database for you, but I borrowed some code from another sample that shows the same functionality.  The tool allows you to specify workflow level events as well as activity level events, and allows you to designate what information you would want to extract and specify the annotation as well.

The output of the designer is an xml file, which we will edit further by hand.  The important parts to look at are the tracking points, that is, where and when do we extract what data.

 

 <ActivityTrackPoint>
            <MatchingLocations>
              <ActivityTrackingLocation>
                <Activity>
                  <Type>TrackingObjectsSample.CreditCheck, TrackingObjectsSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</Type>
                  <MatchDerivedTypes>false</MatchDerivedTypes>
                </Activity>
                <ExecutionStatusEvents>
                  <ExecutionStatus>Closed</ExecutionStatus>
                  <ExecutionStatus>Executing</ExecutionStatus>
                </ExecutionStatusEvents>
              </ActivityTrackingLocation>
            </MatchingLocations>
            <Annotations>
              <Annotation>data extracted from activity</Annotation>
            </Annotations>
            <Extracts>
              <ActivityDataTrackingExtract>
                <Member>IncomingOrder.OrderCustomer.Name</Member>
              </ActivityDataTrackingExtract>
              <ActivityDataTrackingExtract>
                <Member>IncomingOrder.OrderTotal</Member>
              </ActivityDataTrackingExtract>
              <ActivityDataTrackingExtract>
                <Member>IncomingOrder</Member>
              </ActivityDataTrackingExtract>
            </Extracts>
</ActivityTrackPoint>

Here we specify the tracking location, in this case the CreditCheck activity, as well as the events we wish to listen for, and finally what data we want to extract.  I like to point out in the extracts section that we can take advantage of dot-notation in order to traverse an object hierarchy to get to specific pieces of data we wish to extract.  If we don't get down to the simple types, the workflow runtime will attempt to serialize the object and store it as a binary blob (so make sure you mark the types you want tracked as serializable).  This is what is being done with IncomingOrder above.  This will allow us to bring back IncomingOrder, but we won't be able to query against that blog, hence why we might extract OrderTotal in order to generate a report of high value orders.

When we track the information it will get stored to SQL (assuming we are using the SqlTrackingService).  There are a number of views we could use to build queries on top of.  The one I want to point out is the vw_TrackingDataItem view which contains all of the tracking data items (so it is well named :-) ) The FieldTypeId references back to vw_Type where you will see the different type of objects and their respective assemblies which are being tracked.  This lets the consume know what type of object they may need to instantiate if they wish to consume the object.  But what about querying on that object in plain old SQL?  Well, there is a column in the view designed for that.  The Data_Str column will show a string representation of the tracked item, so in the case of numbers and strings and other basic types, we will be able to query.  In the case of complex types, the name of the type will appear.  This is basicly the ToString of the object being tracked.  The Data_Blob column contains the binary represenation of the objects in order to restore them into objects inside of .NET code.

So, back to the task at hand, the sample.  A simple workflow has been created that takes an Order object.  The workflow doesn't do much, although the last code activity modifies a value of the Order object.  We do this to show that the changed values can be tracked over time.  The workflow runtime uses the SqlTrackingService, and before this all starts the tracking profile is inserted into the database using the InsertTrackingProfile method that is used in the SDK (I grabbed it from the UserTrackPoints sample: \Technologies\Tracking\UserTrackPoints).  The workflow then executes.  Clicking on the "Get Tracking Info" button will then use the SqlTrackingQuery to get a SqlTrackingWorkflowInstance, an object representation of all of the available tracking information.  We then iterate over the various event sources in the SqlTrackingWorkflowInstance such as the workflow events and the activity events and place them into a TreeView.  This could easily be extend to include user track points as well.  The following bits of code which do this are below:

 

  
 SqlTrackingQuery query = new SqlTrackingQuery(connectionString);
SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance;
if (query.TryGetWorkflow(new Guid(WorkflowInstanceIdLabel.Text),
            out sqlTrackingWorkflowInstance))
{
     TreeNode tn1 = treeView1.Nodes.Add(string.Format("Activity Events ({0})", 
                 sqlTrackingWorkflowInstance.ActivityEvents.Count));
     foreach (ActivityTrackingRecord activityTrackingRecord in sqlTrackingWorkflowInstance.ActivityEvents)
     {
        // do something here
     }
}  
            

It is useful to place a breakpoint in the foreach loop in order to inspect the different aspects of the ActivityTrackingRecord and the other tracking objects that live inside of SqlTrackingWorkflowInstance.  By looking at the TrackingDataItem 's that are placed in ActivityTrackingRecord.Body we can find the field name as well as its value, which could be an object.  By browsing around in the break mode, the debugger will take care and allow you to move through the serialized object which will be present inside the TrackingDataItem.Data.

Link to sample code