Writing Tracking Services for Windows Workflow Foundation

 

Alberto Arias
Microsoft Premier Support for Developers

August 2006

Applies to:
   Windows Workflow Foundation RC4
   Microsoft Visual C# Version 2.0
   Visual Studio 2005

Summary: Introduces the tracking features in Windows Workflow Foundation and demonstrates how to create a custom tracking service. Readers should be familiar with the basic concepts in Windows Workflow Foundation. (20 printed pages)

Note   The code examples in this article were written using Windows Workflow Foundation RC4. Some changes might be required to make them work on later versions of Windows Workflow Foundation.

Download the code sample, Windows Workflow Sample - WFTrackingServiceTemplate_14072006.msi.

Contents

Introduction
Tracking Infrastructure
Sample Service: Method Tracking Service
Conclusion

Introduction

One of the most interesting features that Windows Workflow Foundation (WF) provides is the ability to view the progress of a workflow during its life cycle without having to write a single line of code. For example, you can start using the SQL tracking service, a runtime service provided with WF, right out of the box. The SQL tracking service enables you to selectively track and log events that are triggered during the execution of your workflow. You can also passively query this information by using either the provided query API or SQL queries.

However, for many reasons, you might want to keep your information in a different model. The good news is that Windows Workflow Foundation has been designed with extensibility in mind. Developers can write their own services that plug into the workflow engine and listen to selected events, execute their own code, and even participate in the transaction batch.

Using tracking services is the ideal way to abstract your operational requirements from the workflow definition. In this article, I present the tracking features that are available in Windows Workflow Foundation. I describe how to write your own tracking service, and include a sample implementation of a tracking service that automatically binds tracking events to code via reflection.

Tracking Infrastructure

The tracking infrastructure is hosted by the WF runtime, and is not directly available to the developer. Its main functions are to maintain the tracking profiles and to listen and dispatch notifications to any of the tracking services, registered either programmatically or via the application configuration file.

Developers need to notify the workflow runtime of which events they want to be informed through a data structure called tracking profile.

Tracking Profiles and Track Points

Tracking profiles are collections of track points, or generally speaking, event definitions. There are three types of track points, corresponding to each of the entities that trigger events:

  • Workflow track points
  • Activity track points
  • User-defined track points

Workflow Track Points

Workflow track points are objects of type WorkflowTrackPoint, which is mainly a container of objects of type WorkflowTrackingLocation. Workflow tracking locations contain the list of workflow events that qualify for notification, as shown in Table 1.

Table 1. Workflow events contained in workflow tracking locations

Workflow Events Description
Aborted The workflow instance has aborted.
Changed A workflow change has occurred on the workflow instance.
Completed The workflow instance has completed.
Created The workflow instance has been created.
Exception An unhandled exception has occurred.
Idle The workflow instance is idle.
Loaded The workflow instance has been loaded into memory.
Persisted The workflow instance has been persisted.
Resumed A previously suspended workflow instance has resumed running.
Started The workflow instance has been started.
Suspended The workflow instance has been suspended.
Terminated The workflow instance has been terminated.
Unloaded The workflow instance has been unloaded from memory.

Activity Track Points

Following the same idea, an activity track point includes a list of ActivityTrackingLocation objects. However, more information can be specified to qualify activity events. To start, there are two lists of location objects: MatchingLocations and ExcludedLocations. As you can imagine, events are only delivered for activity events that are matched in the first list, and not in the second. ActivityTrackingLocation objects contain the optional data shown in Table 2.

Table 2. ActivityTrackingLocation properties

Activity Location Data Description
ActivityType Type of the activity to match.
ActivityTypeName Unqualified name of the activity type to match.
MatchDerivedTypes Whether activities derived from the activity type should be matched.
ExecutionStatusEvents List of ActivityExecutionStatus to match.
Conditions List of conditional expressions, based on equality/non equality of activity member data.

Table 3 shows the possible ActivityExecutionStatus enumeration values.

Table 3. ActivityExecutionStatus values

Activity Status Description
Canceling Specifies that the Activity is canceling.
Closed Specifies that the Activity is closed.
Compensating Specifies that the Activity is being compensated.
Executing Specifies that the Activity is running.
Faulting Specifies that the Activity is faulting.
Initialized Specifies that the Activity has been initialized.

User-Defined Track Points

You can also send user defined tracking information to the runtime infrastructure by using the Activity.TrackData method. Data that is sent using this method can be qualified in profiles by using the UserTrackPoint and UserTrackingLocation classes.

Table 4. UserTrackingLocation properties

User Location Data Description
ActivityType Type of the activity to match.
ActivityTypeName Unqualified name of the activity type to match.
MatchDerivedActivityTypes Whether activities derived from the activity type should be matched.
ArgumentType Type of the user data to match.
ArgumentTypeName Unqualified name of the user data type to match.
MatchDerivedArgumentTypes Whether user data derived from the type should be matched.
Conditions List of conditional expressions, based on equality/non equality of activity member data.
KeyName Key name associated to the user data to match.

Tracking Services

Tracking services are entities that implement the workflow tracking contract, therefore making them subscribers of the runtime tracking event system. The implementation of the contract is done through the TrackingService abstract type and every tracking service must overload the methods shown in Table 5:

Table 5. Methods implemented by tracking services

Method Description
GetProfile(Type, Version) Returns the profile for the specified type and version. Used when a workflow is loaded from the persistence service and the profile version is not in the cache.
GetProfile(Guid) Returns the instance private profile for the specified workflow. Used when a workflow has received a private profile via the WorkflowInstance.ReloadTrackingProfiles method, is loaded from the persistence service, and the profile version is not in the cache.
TryGetProfile(Type, out TrackingProfile) Initializes the profile for the input activity type. Returns false if there is no profile for the type.
TryReloadProfile(Type,Guid,out TrackingProfile) Reloads the input tracking profile for the specified instance when the WorkflowInstance.ReloadTrackingProfiles method is invoked. Returns false if there is no profile for the instance.
GetTrackingChannel(TrackingParameters) Returns the TrackingChannel object for the specified workflow type. Called every time a workflow instance is created or restored from the persistence store, if there is an associated profile.

Profile Manager

The profile manager is responsible for locating and maintaining the tracking profiles at run time. When a workflow type is created, the workflow runtime invokes the TryGetProfile method in each registered tracking service. The returned profiles are parsed and added to the profile cache. If the service implements the IProfileNotification interface, the cached profile is reused for subsequent instances of workflows of the same type.

IProfileNotification contains two event handlers: ProfileRemoved and ProfileUpdated. The profile manager subscribes to these events, enabling tracking services to have control over the contents of the cache.

Workflows might be persisted and loaded during their life cycle. When workflows are loaded from the store, the profile manager tries to locate the profile version that the workflow was using when it was persisted. If it cannot find the profile version, it invokes the GetProfile method, passing the expected version number via the Version parameter.

Simple Tracking Service Example

The following example demonstrates a simple tracking service:

class SimpleTrackingService : TrackingService
{
    protected override TrackingProfile GetProfile(Guid workflowInstanceId)
    {
        return GetProfile();
    }

    protected override TrackingProfile GetProfile(
Type workflowType, Version profileVersionId)
    {
        return GetProfile();
    }

    protected override TrackingChannel GetTrackingChannel(
TrackingParameters parameters)
    {
        return GetProfile();
    }

    protected override bool TryGetProfile(
Type workflowType, out TrackingProfile profile)
    {
        profile = GetProfile();
        return true;
    }

    protected override bool TryReloadProfile(
Type workflowType, Guid workflowInstanceId, 
out TrackingProfile profile)
    {
        profile = GetProfile();
        return true;
    }

    TrackingProfile GetProfile()
    {
        TrackingProfile profile = new TrackingProfile();
        profile.Version = new Version("1.0.0.0");
        WorkflowTrackPoint wftp = new WorkflowTrackPoint();
        wftp.MatchingLocation.Events.Add(TrackingWorkflowEvent.Exception);
        profile.WorkflowTrackPoints.Add(wftp);
        return profile;
    }
}

Tracking Channels

Tracking channels are the final destination for events. They are workflow instance-bound entities, providing a thread-safe environment for receiving events. When a new instance of a workflow is created, the workflow runtime requests a tracking channel from each registered transaction service that returned a profile for the workflow type.

A tracking channel is a class that inherits from TrackingChannel. It implements the methods shown in Table 6:

Table 6. Methods implemented by TrackingChannel

Method Description
InstanceCompletedOrTerminated() Invoked to notify that the workflow instance is completed or has terminated. This method is called regardless of the profile definition. No more data is sent to the tracking channel after this method is called.
Send(TrackingRecord) Invoked for each event qualified by the tracking profile. The tracking record object contains the event information.

Tracking Channel Example

The following example demonstrates a simple tracking channel implementation that workflow traces exceptions:

class SimpleTrackingChannel : TrackingChannel
{
protected override void InstanceCompletedOrTerminated()
{            
}

protected override void Send(TrackingRecord record)
{
if (record is WorkflowTrackingRecord 
&& record.EventArgs is TrackingWorkflowExceptionEventArgs)
{
TrackingWorkflowExceptionEventArgs args = 
record.EventArgs as TrackingWorkflowExceptionEventArgs;
Trace.Write(args.Exception.Message);
}
}
}

Table 7 contains the three types of tracking records:

Table 7. Tracking record classes

Tracking Records Description
WorkflowTrackingRecord Workflow event tracking information. EventArg data is attached to Exception, Suspended, and Terminated events.
ActivityTrackingRecord Activity event tracking information.
UserTrackingRecord User event tracking information.

 

Data Extracts

Data extracts are references to workflow or activity data. Extracts are created at profile creation time inside activity or user track points. They are executed at event time, making the data available to the user. This is the only available way to access workflow data without explicit interaction.

TrackingExtract objects are strings that contain the name of the member of the activity or workflow to extract. They can be defined either in user or activity tracking points, as shown in the following example:

ActivityTrackPoint activityTrackPoint = new ActivityTrackPoint();
activityTrackPoint.Extracts.Add(
    new ActivityDataTrackingExtract("Description")
    );
activityTrackPoint.Extracts.Add(
    new WorkflowDataTrackingExtract("Description")
    );

In this example, we request the Description field of both the CodeActivity activity and the containing workflow. This data is returned in the tracking record as a collection of TrackingDataItem objects. However, tracking data items only have three fields: Member, Value, and Annotations. Therefore, you cannot differentiate between activity and workflow data at this point. A workaround would be to use annotations. These are strings specified at TrackingExtract creation time that are passed along with the event information at dispatch time.

Creating and Processing Tracking Extracts

The following example demonstrates how to create a tracking extract:

ActivityTrackPoint activityTrackPoint = new ActivityTrackPoint();
activityTrackPoint.Extracts.Add(
    new ActivityDataTrackingExtract("Description")
);
TrackingDataItem item = new WorkflowDataTrackingExtract("Description");
item.Annotations.Add("Workflow description");
activityTrackPoint.Extracts.Add(item);

This example shows the processing of tracking extracts; in this case, the send method will print the contents of the tracking extract on the console:

class SimpleTrackingChannel : TrackingChannel
{
    protected override void InstanceCompletedOrTerminated()
    {        
    }
    protected override void Send(TrackingRecord record)
    {
        if (record is ActivityTrackingRecord)
        {
            ActivityTrackingRecord ar = recoord as ActivityTrackingRecord;
            foreach (TrackingDataItem item in ar.Body)
            {
                System.Console.WriteLine(item.Data);
            }
        }
    }
}

Notice that tracking extract references point to objects inside the workflow. Therefore, references to these objects can become stale or can be updated after the event has been delivered by the running workflow. It is only safe to assume that data is up to date at the time the event was delivered.

If you need to maintain a copy of the data item across events, the recommendation is to use in-memory serialization to generate a private copy of the objects that are contained within.

Participating in the Workflow Batch

The workflow batch is a collection of work items that need be processed as part of the persistence work done by the runtime. The workflow batch is executed in a transactional context and is available during the execution of activities. A work item is an object reference that contains data and enables the maintenance of durable stores consistent with the workflow state via distributed transactions.

Work items are associated with work item handlers. A work item handler is a type that implements the IPendingWork interface and does the actual work. This work is normally to perform database (or other resource) operations on the commit and completion phases. Table 8 lists the methods a work item handler must implement:

Table 8. Work item handlers

Method Description
Commit(Transaction,ICollection) Called when the runtime dictates that the batch should be committed. Receives a collection of work items associated with the handler and a transaction.
Complete(bool,ICollection) Called when the transaction has completed. Receives a collection of work items associated with the handler and whether the transaction has succeeded or failed.
MustCommit(ICollection) Returns whether the commit can be postponed to a future persistence point.

Service developers can write their own work item handlers and add objects to the workflow batch by using the WorkflowEnvironment.WorkBatch property, as demonstrated in the following example:

class TransactionalTrackingChannel : TrackingChannel, IPendingWork
{
    protected override void InstanceCompletedOrTerminated()
    {
    }

    protected override void Send(TrackingRecord record)
    {
        if (record is ActivityTrackingRecord)
        {
            // Add activity records to the workflow batch.
            WorkflowEnvironment.WorkBatch.Add(this, record);
        }
    }

    public void Commit(Transaction transaction, ICollection items)
    {
        // Process Workflow activity records.
        try
        {
            foreach (ActivityTrackingRecord record in items)
            {            
                System.Console.WriteLine(record.ActivityType);
            }
        }
        catch (Exception e)
        {
            // Unexpected exception, roll back transaction.
            transaction.Rollback(e);
        }
    }

    public void Complete(bool succeeded,ICollection items)
    {
        // Transaction has completed.
    }

    public bool MustCommit(System.Collections.ICollection items)
    {
        // Returns whether items in the collection must be committed.
    }
}

If you are thinking of using data extracts and the workflow batch, you must do some extra work. Remember that data extracts are references to data that is stored within the workflow instance. Therefore, this data might be modified before the batch is committed. The recommended solution is to make a copy of the objects by using serialization, as illustrated in this example:

public void CloneDataItems(ActivityTrackingRecord record)
{
    MemoryStream stream = new MemoryStream(0x400);
    BinaryFormatter formatter = new BinaryFormatter();

    foreach (TrackingDataItem item in record.Body)
    {
        stream.Seek(0, 0);
        formatter.Serialize(stream, item.Data);
        stream.Seek(0, 0);
        item.Data = formatter.Deserialize(stream);
    }
}

protected override void Send(TrackingRecord record)
{
    if (record is ActivityTrackingRecord)
    {
        // Clone extracts.
        CloneDataItems(record as ActivityTrackingRecord);
        // Add activity records to the workflow batch.
        WorkflowEnvironment.WorkBatch.Add(this, record);
    }
}

Sample Service: Method Tracking Service

Writing your own tracking service can be a time-consuming task. When you write a tracking service, you must provide a tracking profile and match tracking records that raise your custom functionality. To simplify the process, I have provided a sample custom tracking service that reads metadata in your code encoded in custom attributes to infer an optimized tracking profile, and matches tracking records to the methods that contain the tracking business logic.

Tracking Metadata

This sample service requires tracking functionality to be encapsulated in a class as static public methods, having a method per event of interest. The class and its methods must be attributed using the types shown in Figure 1:

Aa730873.trackserv01(en-US,VS.80).gif

Figure 1. Shows attributes in an example tracking service class and methods

The following example illustrates the use of the TrackingType attribute; this flags the class as tracking type for the specified workflow type. The WorkflowTrackingMethod and the ActivityTrackingMethod tell the method tracking service which profile it has to generate, using the specified events, and what methods to call whenever an event is received.

[TrackingType("WorkflowLibrary1.Workflow1")]
public class ConsoleTracking
{
    [WorkflowTrackingMethod(TrackingWorkflowEvent.Completed)]
    public static void WorkflowCreated(WorkflowTrackingRecord record)
    {
        System.Console.WriteLine(
String.Format("Workflow {0} {1}",
WorkflowEnvironment.WorkflowInstanceId,
record.TrackingWorkflowEvent));
    }

    [ActivityTrackingMethod("Activity",
 ActivityExecutionStatus.Executing)]
    public static void TestMethod(
[TrackingExtract("Name")] string description)
    {
        System.Console.WriteLine("Activity: " + description));
    }
}

TrackingType

The TrackingType attribute declares that the type should be parsed for tracking metadata, the workflow type name it provides metadata for, and optionally, whether events should be delivered in the workflow transaction scope:

[TrackingType("WorkflowLibrary1.Workflow1", IsTransactional=true)]
public class ConsoleTracking
{
...

WorkflowTrackingMethod

The WorkflowTrackingMethod attribute declares that the qualified method should be executed when a particular workflow event occurs. The method might declare an optional parameter of type WorkflowTrackingRecord to receive additional information about the event.

[WorkflowTrackingMethod(TrackingWorkflowEvent.Completed)]
[WorkflowTrackingMethod(TrackingWorkflowEvent.Created)]
public static void WorkflowCreated(WorkflowTrackingRecord record)
{
    System.Console.WriteLine(String.Format("Workflow {0} {1}", WorkflowEnvironment.WorkflowInstanceId, record.TrackingWorkflowEvent));
}

ActivityTrackingMethod

The ActivityTrackingMethod attribute declares that the qualified method should be executed when a particular activity event occurs. The method might declare a number of parameters qualified by TrackingExtract attributes, signaling that the parameter should be initialized with data from the workflow instance:

[ActivityTrackingMethod("Activity", ActivityExecutionStatus.Executing)]
public static void TestMethod([TrackingExtract("Name")] string description)
{
    System.Console.WriteLine("Activity: " + description));
}

The tracking service matches any activity that inherits from the defined activity type name, and executes the qualified method.

TrackingExtract

The TrackingExtract attribute defines a property name, and whether it is an activity or workflow property. Data from the workflow or activity is extracted and provided as input to the method:

[ActivityTrackingMethod("Activity", ActivityExecutionStatus.Executing)]
public static void TestMethod(
    [TrackingExtract("Name")] string description,
    [TrackingExtract("Name", IsWorkflowExtract=true)] string worflowDescription)
{
    System.Console.WriteLine(worflowDescription + ": " + description);
}

The IsWorkflowExtract property signals to the tracking service whether the extracted data comes from the containing workflow or the qualified activity.

Configuration

The sample service can be configured by using the application configuration file or programmatically, as any other runtime service. The following is an example configuration file for the tracking service:

<configuration>
  <configSections>
    <section name="workflowRuntimeConfiguration" type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <workflowRuntimeConfiguration Name="Hosting">
    <CommonParameters />
    <Services>
      <add type=" Microsoft.Tracking.MethodTrackingService, MethodTrackingService" assembly="TestTracker" />
    </Services>
  </workflowRuntimeConfiguration>
</configuration>

This example shows how to create the service programmatically:

MethodTrackingService methodTrackingService = 
    new MethodTrackingService("TestTracker");
workflowRuntime.AddService(methodTrackingService);

Generating the Runtime Profile

The MethodTrackingService class contains the implementation of the tracking service. The runtime will contact this tracking service each time a workflow instance is created to obtain the tracking profiles and references to the TrackingChannel.

Profile generation is performed by the TrackingClassParser helper private class, by reflecting through the input Type metadata previously described.

Aa730873.trackserv02(en-US,VS.80).gif

Figure 2. Shows TrackingClassParser helper class and MethodTrackingService

This is an example of an XML serialized tracking profile generated by the TrackingClassParser:

<?xml version="1.0" encoding="ibm850" standalone="yes"?>
<TrackingProfile xmlns="https://schemas.microsoft.com/winfx/2006/workflow/trackin
gprofile" version="1.0.0.0">
  <TrackPoints>
    <WorkflowTrackPoint>
      <MatchingLocation>
        <WorkflowTrackingLocation>
          <TrackingWorkflowEvents>
            <TrackingWorkflowEvent>Created</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Completed</TrackingWorkflowEvent>

          </TrackingWorkflowEvents>
        </WorkflowTrackingLocation>
      </MatchingLocation>
      <Annotations>
        <Annotation>Transactional</Annotation>
      </Annotations>
    </WorkflowTrackPoint>
    <ActivityTrackPoint>
      <MatchingLocations>
        <ActivityTrackingLocation>
          <Activity>
            <TypeName>Activity</TypeName>
            <MatchDerivedTypes>true</MatchDerivedTypes>
          </Activity>
          <ExecutionStatusEvents>
            <ExecutionStatus>Executing</ExecutionStatus>
          </ExecutionStatusEvents>
        </ActivityTrackingLocation>
      </MatchingLocations>
      <Annotations>
        <Annotation>Transactional</Annotation>
      </Annotations>
      <Extracts>
        <ActivityDataTrackingExtract>
          <Member>Name</Member>
        </ActivityDataTrackingExtract>
        <WorkflowDataTrackingExtract>
          <Member>Name</Member>
          <Annotations>
            <Annotation>Workflow</Annotation>
          </Annotations>
        </WorkflowDataTrackingExtract>
      </Extracts>
    </ActivityTrackPoint>
  </TrackPoints>
</TrackingProfile>

Dispatching Events

Event dispatching is performed via the MethodTrackingChannel class. During construction, this class generates lookup tables to map tracking records to class methods. This information is stored in method lists contained in WorkflowTrackingMethodTable and ActivityTrackingMethodTable types for workflow and activity events respectively:

Aa730873.trackserv03(en-US,VS.80).gif

Figure 3. MethodTrackingChannel and the method lookup tables.

During workflow execution, WorkflowTrackingMethodTable and ActivityTrackingMethodsTable match tracking records to method lists. When a match is found, the methods of the list are invoked by using the tracking record data; however, if the type was declared as transactional via the IsTransactional property of the TrackingType attribute, data extracts are cloned, and the record is added to the workflow batch for later processing during the commit phase of the workflow batch.

Conclusion

Workflow tracking services enable you to seamlessly decouple operational functionality from the workflow business logic. This provides increased flexibility and reusability by separating the different aspects of the application. Windows Workflow Foundation has been designed to be extensible, providing you with a flexible interface to interact with the workflow runtime engine and create your own tracking services.

Whether you use a service out of the box or create one of your own, workflow tracking services enable real workflow application integration.

For More Information

There are a number of sites you can use to obtain more information about Windows Workflow Foundation. See https://msdn.microsoft.com/workflow and https://www.windowsworkflow.net for more details.

There is also a good introductory book on Windows Workflow available online that was written by several key members of the Microsoft team. See Presenting Windows Workflow Foundation for details.

 

About the author

Alberto Arias works for Microsoft in the UK, where he is an Application Development Consultant specialized in enterprise application design. He's been working with the .NET Framework since its inception. Check out the Premier Support for Developers (PSfD) team blog at https://blogs.msdn.com/psfd.

© Microsoft Corporation. All rights reserved.