Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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.
Introduction
Tracking Infrastructure
Sample Service: Method Tracking Service
Conclusion
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.
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 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 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. |
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. |
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 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. |
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.
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 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. |
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 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.
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.
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); } }
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.
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:
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)); } }
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 { ...
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)); }
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.
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.
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);
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.
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>
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:
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.
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.
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.
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.