WF 4.0 WorkflowServiceHost Custom Tracking

In previous two-part post (here and here) I showed how to add persistence to a workflow hosted via the WorkflowServiceHost. In this post I will do some modifications on the same example to add tracking support. Now since I am a good guy and would like to make your lives easier, I will repost the full code here instead of pointing out only the differences. Let’s roll…

Below is our simple workflow (Workflow1.xaml) (use Workflow Console Application template from VS 2010):

It receives a request from a client application, using a custom activity it retrieves the workflow instance id and returns it as a response to the client. Finally a delay shape delays execution for 30 seconds.

Below is the code for the custom activity:

public sealed class CodeActivity1 : CodeActivity
{
public OutArgument WFInstanceId { get; set; }protected override void Execute(CodeActivityContext context)
{
context.SetValue(WFInstanceId, context.WorkflowInstanceId);CustomTrackingRecord customRecord = new CustomTrackingRecord(“CustomInfo”)
{
Data =
{
{“Date”, DateTime.Now.ToShortDateString()},
}
};

context.Track(customRecord);
}

}

The custom activity returns the workflow instance id and creates a new CustomTrackingRecord. CustomTrackingRecords are any custom information you want to be able to track. In this simple example I am tracking a custom variable which stores the current date…more on this in a moment.

Below is the code from the Program.cs file needed to set up the host and tracking:

class Program
{
static void Main(string[] args)
{string baseAddress = “http://localhost:8089/TestWF”;using (WorkflowServiceHost host =
new WorkflowServiceHost(new Workflow1(), new Uri(baseAddress)))
{
host.Description.Behaviors.Add(new
ServiceMetadataBehavior() { HttpGetEnabled = true });

host.AddServiceEndpoint(“IService”,new BasicHttpBinding(), baseAddress);

TrackingProfile fileTrackingProfile = new TrackingProfile();
fileTrackingProfile.Queries.Add(new WorkflowInstanceQuery
{
States = { “*” }
});
fileTrackingProfile.Queries.Add(new ActivityStateQuery()
{
ActivityName = “*”,
States = { “*” },
// You can use the following to specify specific stages:
// States = {
// ActivityStates.Executing,
// ActivityStates.Closed
//},
Variables =
{
{ “*” }
} // or you can enter specific variable names instead of “*”

});
fileTrackingProfile.Queries.Add(new CustomTrackingQuery()
{
ActivityName = “*”,
Name = “*”
});

FileTrackingParticipant fileTrackingParticipant =
new FileTrackingParticipant();
fileTrackingParticipant.TrackingProfile = fileTrackingProfile;
host.WorkflowExtensions.Add(fileTrackingParticipant);

host.Description.Behaviors.Add(new WorkflowIdleBehavior()
{
TimeToPersist = TimeSpan.FromSeconds(5),
TimeToUnload = TimeSpan.FromSeconds(20)
});

host.Open();
Console.WriteLine(“Car rental service listening at: ” +
baseAddress);
Console.WriteLine(“Press ENTER to exit”);
Console.ReadLine();
}

}
}

public class FileTrackingParticipant : TrackingParticipant
{
string fileName;
protected override void Track(TrackingRecord record,
TimeSpan timeout)
{
fileName = @”c:\tracking\” + record.InstanceId + “.tracking”;
using (StreamWriter sw = File.AppendText(fileName))
{
WorkflowInstanceRecord workflowInstanceRecord = record as WorkflowInstanceRecord;
if (workflowInstanceRecord != null)
{
sw.WriteLine(“——WorkflowInstanceRecord——“);
sw.WriteLine(“Workflow InstanceID: {0} Workflow instance state: {1}”,
record.InstanceId, workflowInstanceRecord.State);
sw.WriteLine(“\n”);
}

ActivityStateRecord activityStateRecord = record as ActivityStateRecord;
if (activityStateRecord != null)
{
IDictionary variables = activityStateRecord.Variables;
StringBuilder vars = new StringBuilder();

if (variables.Count > 0)
{
vars.AppendLine(“\n\tVariables:”);
foreach (KeyValuePair variable in variables)
{
vars.AppendLine(String.Format(
“\t\tName: {0} Value: {1}”, variable.Key, variable.Value));
}
}

sw.WriteLine(“——ActivityStateRecord——“);
sw.WriteLine(“Activity DisplayName: {0} :ActivityInstanceState: {1} {2}”,
activityStateRecord.Activity.Name, activityStateRecord.State,
((variables.Count > 0) ? vars.ToString() : String.Empty));
sw.WriteLine(“\n”);
}

CustomTrackingRecord customTrackingRecord = record as CustomTrackingRecord;
if ((customTrackingRecord != null) && (customTrackingRecord.Data.Count > 0))
{
sw.WriteLine(“——CustomTrackingRecord——“);
sw.WriteLine(“\n\tUser Data:”);
foreach (string data in customTrackingRecord.Data.Keys)
{
sw.WriteLine(” \t\t {0} : {1}”, data, customTrackingRecord.Data[data]);
}
sw.WriteLine(“\n”);
}
}
}
}

So let me explain what’s going on: in a nutshell, when you deal with WF 4.0 tracking you have to understand three concepts:

  • Tracking records: these are the information records emitted by your workflow. There are four derived classes that correspond to the four types of tracking records:
    • WorkflowInstanceQuery: events about workflow instance state. For example started, suspended, unloaded, etc…
    • ActivityStateQuery: events about activities inside the WF
    • CustomTrackingQuery: any custom information you want to track
    • BookmarkResumptionQuery: bookmark name you want to track whenever this bookmark is resumed (I won’t cover bookmarks in this post to keep the concept clear, but I will have another dedicated post for this topic)
  • Tracking profiles: act like filters over tracking records in order to track only required information
  • Tracking participants: the medium where tracking information will be written to. WF 4.0 comes with one participant which writes to ETW (event tracking for windows). Custom participants can be easily developed to write tracking data into SQL Server for example, or like I did in this example into the File System

In my code, I create an instance of class TrackingProfile and added three types of records: WorkflowInstanceQuery, ActivityStateQuery, and CustomTrackingQuery.

For the WorkflowInstanceQuery I asked the profile to track all workflow states using the “*” operator. I could also limit the number of states that I want to track (check commented code). Recall that in my workflow I have states of persist and unload (review behavior configuration and delay timespan) so you will see these states in he tracked data. For the ActivityStateQuery I asked the profile to track all states for all activities as well as any variables declared for these activities. Similarly for the CustomTrackingQuery I asked the profile to track any custom data I defined (recall that I did define custom data in my custom activity).

Now that I have defined the tracking records as well as the tracking profile; final step is to define the tracking participant. I could have used the out of the box EtwTrackingParticipant class which writes to ETW, but to make things more interesting, I have created a custom tracking participant class (FileTrackingParticipant) which derives from TrackingParticipant. Check the code for that class which simply gets the information for each of the tracked records and writes it into a file.

Back to the main program, I associate my tracking profile (fileTrackingProfile) with my tracking participant (fileTrackingParticipant) and finally add the participant to the set of the host WorkflowExtensions.

To make sure you will have a running program, below is the trivial client program code:

ServiceReference1.ServiceClient proxy = new ServiceReference1.ServiceClient();

Guid workflowId = (Guid)proxy.Operation1();

Console.WriteLine(“client done”);
Console.WriteLine(“enter key”);
Console.ReadLine();

Run the service so that it is listening to requests on “http://localhost:8089/TestWF“, now run the client console and wait until the service console window prints “Workflow Ended”. Now examine the file where tracking is written and you will see workflow events, activity events, variables, and custom information all tracked…see the below image (reformatted for presentation):

Advertisement

2 thoughts on “WF 4.0 WorkflowServiceHost Custom Tracking

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s