Windows Workflow Foundation: Tracking Services Introduction

 

David Gristwood
Microsoft Developers and Platform Group

January 2007

Applies to:
   .NET Framework 3.0/Windows Workflow Foundation
   Microsoft Visual C# Version 2.0
   Microsoft Visual Studio 2005

Summary: This article provides an introduction to the tracking services in Windows Workflow Foundation and describes the key elements of the tracking services, how they work, and how you can use them in your own applications. (15 printed pages)

See also the related article "Windows Workflow Foundation: Tracking Services Deep Dive" by Ranjesh Jaganathan.

Contents

Introduction
Tracking Quick Start
How Tracking Works
Tracking Profiles
Writing Your Own Tracking Service
The Workflow Monitor
Conclusion
References
About the Author

Introduction

Windows Workflow Foundation offers many benefits to developers who want to model human and system workflows. Some of these benefits stem from the ability to model the workflow using a design surface and assemble modular blocks of code in the form of activities to create workflow solutions. Other benefits come from the services that can be easily plugged into the workflow, such as persistence, scheduling, and tracking—functionality that developers would have to create themselves.

Of the services available to developers to use within their workflow applications, the tracking service is arguably one of the most useful. As workflows execute, the tracking service can be configured to automatically emit information about the flow of control within the workflow, and this information can be stored in an external store, such as a SQL Server database, for querying. This allows various applications, from custom applications to business analytic tools, effectively to "look inside" workflows and query their status, or determine which are blocking on specific external events. This surfacing of tracking information is essential in creating the transparency that workflow solutions require.

Tracking Quick Start

One of the easiest ways to understand how a tracking service works is to use the SqlTrackingService that is provided by Windows Workflow Foundation. It is easy to set up and is used by a number of the SDK samples, so it is an ideal way to familiarize yourself with the concept of tracking. It is important to note that the tracking framework is agnostic to the type of store, and so it is possible to write a tracking service for any type of store, including a non-persistent one.

Before you can use the SqlTrackingService, it requires an initial configuration of several database tables, including their stored procedures, views, roles, and other logic. The "Creating the Tracking Database" section in the Windows Workflow Foundation SDK provides a step-by-step guide that takes you through the scripts you need to run to set up a suitable tracking database.

The simplest way to observe how the SqlTrackingService functions is to run the "Simple Tracking Sample" from the Windows Workflow Foundation SDK, which performs the following actions:

  • Configures the Workflow runtime to use the SqlTrackingService
  • Executes a simple workflow containing one activity
  • Retrieves and displays the tracking information from that workflow execution

To configure the workflow runtime to use the SqlTrackingService, add it as a service to the runtime, as shown in the following code example.

// Create the workflow runtime.
WorkflowRuntime workflowRuntime = new WorkflowRuntime()

// Add the tracking service.
workflowRuntime.AddService(new SqlTrackingService("Initial 
Catalog=Tracking;Data Source=localhost;Integrated Security=SSPI;"));

// Load a workflow into the runtime.
WorkflowInstance workflowInstance = workflowRuntime.CreateWorkflow(typeof(SimpleTrackingWorkflow));
Guid instanceId = workflowInstance.InstanceId;

// Start the runtime, and then start the workflow instance.
workflowRuntime.StartRuntime();
workflowInstance.Start();

Alternatively, the tracking service can be added declaratively through the app.config file. Simply add an extra entry to the services section, specifying the type as SqlTrackingService and the appropriate connectionString for SQL Server.

<Services>
    <add type="System.Workflow.Runtime.Tracking.SqlTrackingService, 
System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, 
PublicKeyToken=31bf3856ad364e35" connectionString="Initial 
Catalog=TrackingS;Data Source=localhost;Integrated Security=SSPI;" />
</Services>

As soon as the tracking service is configured, no additional coding is required; the workflow runtime infrastructure does all the work.

The SDK sample loads a simple workflow, SimpleTrackingWorkflow, and then lets it execute.

Bb264459.wwf_tsintro01(en-US,VS.80).gif

As soon as workflow has finished executing, it queries and displays information from the tracking store. Running the sample should produce output similar to the following.

Instance Level Events:
EventDescription : Created  DateTime : 08/05/2006 18:02:58
EventDescription : Started  DateTime : 08/05/2006 18:02:58
EventDescription : Completed  DateTime : 08/05/2006 18:02:58

Activity Tracking Events:
StatusDescription : Executing  DateTime : 08/05/2006 18:02:58 Activity 
Qualified  ID : SimpleTrackingWorkflow
StatusDescription : Executing  DateTime : 08/05/2006 18:02:58 Activity 
Qualified  ID : code1
StatusDescription : Closed  DateTime : 08/05/2006 18:02:58 Activity 
Qualified  ID : code1
StatusDescription : Closed  DateTime : 08/05/2006 18:02:58 Activity 
Qualified ID : SimpleTrackingWorkflow

As you can see from the tracking-service output, the workflow's life cycle was very simple; it was created, started, and then completed. The workflow, SimpleTrackingWorkflow, is itself an activity as far as the workflow runtime is concerned, with one child activity, named 'code1.'

The workflow tracking service captures three types of events, the first of which is known as workflow events, which occur when a specific instance of a workflow changes its state. Over its lifetime, a workflow instance will go through a number of states, from starting, through being suspended, or going idle, all of which can be tracked.

The second type of events that can be tracked is known as activity-execution status events. As each of the activities that makes up the workflow gets the chance to execute, it goes through a number of states—namely, initialized, executing, faulting, canceling, compensating, and closed.

A third type of event that can be tracked, known as a user event (sometimes referred to as a custom event in some of the earlier Workflow documentation), is an event that is explicitly generated by the application. It provides a very useful mechanism to surface application-specific data, such as an invoice number or customer ID that is specific to that workflow instance, which other applications might want to track. This application-specific data can include objects, as well as simple strings.

This tracking information for a specific instance of a workflow can be easily retrieved programmatically by using the SqlTrackingQuery, SqlTrackingQueryOptions, SqlTrackingWorkflowInstance, and appropriate TrackingRecord derived classes provided by the framework.

    // System.Workflow.Runtime.Tracking

    public class SqlTrackingWorkflowInstance
    {
        public IList<ActivityTrackingRecord> ActivityEvents { get; }
        public bool AutoRefresh { get; set; }
        public DateTime Initialized { get; set; }
        public IList<SqlTrackingWorkflowInstance> InvokedWorkflows { get; 
}
        public Guid InvokingWorkflowInstanceId { get; set; }
        public WorkflowStatus Status { get; set; }
        public IList<UserTrackingRecord> UserEvents { get; }
        public Activity WorkflowDefinition { get; }
        public bool WorkflowDefinitionUpdated { get; }
        public IList<WorkflowTrackingRecord> WorkflowEvents { get; }
        public Guid WorkflowInstanceId { get; set; }
        public long WorkflowInstanceInternalId { get; set; }
        public Type WorkflowType { get; set; }
        public void Refresh();
    }

    public class WorkflowTrackingRecord : TrackingRecord
    {
        public WorkflowTrackingRecord();
        public WorkflowTrackingRecord(TrackingWorkflowEvent 
trackingWorkflowEvent, DateTime eventDateTime, int eventOrder, EventArgs 
eventArgs);
        public override TrackingAnnotationCollection Annotations { get; }
        public override EventArgs EventArgs { get; set; }
        public override DateTime EventDateTime { get; set; }
        public override int EventOrder { get; set; }
        public TrackingWorkflowEvent TrackingWorkflowEvent { get; set; }
    }

    public class ActivityTrackingRecord : TrackingRecord
    {
        public ActivityTrackingRecord();
        public ActivityTrackingRecord(Type activityType, string 
qualifiedName, Guid contextGuid, Guid parentContextGuid, 
ActivityExecutionStatus executionStatus, DateTime eventDateTime, int 
eventOrder, EventArgs eventArgs);
        public Type ActivityType { get; set; }
        public override TrackingAnnotationCollection Annotations { get; }
        public IList<TrackingDataItem> Body { get; }
        public Guid ContextGuid { get; set; }
        public override EventArgs EventArgs { get; set; }
        public override DateTime EventDateTime { get; set; }
        public override int EventOrder { get; set; }
        public ActivityExecutionStatus ExecutionStatus { get; set; }
        public Guid ParentContextGuid { get; set; }
        public string QualifiedName { get; set; }
    }

    public class UserTrackingRecord : TrackingRecord
    {
        public UserTrackingRecord();
        public UserTrackingRecord(Type activityType, string qualifiedName, 
Guid contextGuid, Guid parentContextGuid, DateTime eventDateTime, int 
eventOrder, string userDataKey, object userData);
        public Type ActivityType { get; set; }
        public override TrackingAnnotationCollection Annotations { get; }
        public IList<TrackingDataItem> Body { get; }
        public Guid ContextGuid { get; set; }
        public override EventArgs EventArgs { get; set; }
        public override DateTime EventDateTime { get; set; }
        public override int EventOrder { get; set; }
        public Guid ParentContextGuid { get; set; }
        public string QualifiedName { get; set; }
        public object UserData { get; set; }
        public string UserDataKey { get; set; }
    }

The Windows Workflow Foundation SDK documentation provides a more detailed description of each of these classes, and it is worth spending a little time looking at the documentation for these classes, because they are used quite frequently in tracking.

The "Simple Tracking Sample" application uses these classes to request the tracking information via the GUID that represents the workflow's instance ID. Once an instance of the SqlTrackingWorkflowInstance class is obtained, it is queried for its TrackingRecord event information.

    // Create a new SqlTrackingQuery object.
    SqlTrackingQuery sqlTrackingQuery = new 
SqlTrackingQuery(connectionString);

    // Query the SqlTrackingQuery for a specific workflow instance ID.
    SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance;
    sqlTrackingQuery.TryGetWorkflow(instanceId, out 
sqlTrackingWorkflowInstance);
            
    // Check whether there is a matching workflow with this ID.
    if (sqlTrackingWorkflowInstance != null)
    {
        // Examine the workflow events. 
        Console.WriteLine("\nWorkflow Events:\n");

        foreach (WorkflowTrackingRecord workflowTrackingRecord in 
sqlTrackingWorkflowInstance.WorkflowEvents)
        {
            Console.WriteLine("EventDescription : {0}  DateTime : {1}", 
workflowTrackingRecord.TrackingWorkflowEvent, 
workflowTrackingRecord.EventDateTime);
        }

        // Examine the activity events.

        foreach (ActivityTrackingRecord activityTrackingRecord in 
sqlTrackingWorkflowInstance.ActivityEvents)
        {
            Console.WriteLine("StatusDescription : {0}  DateTime : {1} 
Activity Qualified ID : {2}", activityTrackingRecord.ExecutionStatus, 
activityTrackingRecord.EventDateTime, 
activityTrackingRecord.QualifiedName);
        }
    }

The "Tracking Using User Track Points" sample extends the tracking process one stage further and generates a custom event from within the workflow, so that application-specific data, along with activity-execution information, can be tracked and stored. This application data is logged within the OnTrackingData event handler through a call to the TrackData ** method, as shown in the following code example taken from the sample.

// Implementation of the OnTrackingData event handler.
   private void OnTrackingData (object sender, EventArgs e)
   {
Console.WriteLine("Executing the workflow");
      this.TrackData("Tracking the execution of the code activity");
   }

The SQL tracking information is stored across a number of SQL Server tables in the tracking database. If the SqlTrackingQuery APIs do not meet your specific requirements, you are free to access the database schema through a set of available SQL Server views, which provide detailed information. Examples of this might include querying across a variety of different workflows for specific conditions, using tools such as SQL Server Reporting Services.

How Tracking Works

Having looked at the tracking service in action, let's delve a little deeper, and explore the constituent elements that make up the tracking service.

The following architectural diagram shows how the workflow tracking infrastructure works.

Bb264459.wwf_tsintro02(en-US,VS.80).gif

The key elements of the infrastructure are:

  • Tracking runtime. The runtime is responsible for the initialization of the tracking, such as setting up the appropriate tracking profile, and once a workflow instance is running, it picks up events, such as status change, and passes them on to the tracking channel.
  • Tracking channel. The tracking channel is used to deliver information from the runtime to the appropriate tracking-service store. For the SqlTrackingService, every workflow instance has a separate tracking channel for each registered service, which provides developers with an architecturally simple model that makes it easy to develop services that do not need to worry about locking.
  • Tracking service. The tracking service is the core element of the tracking infrastructure, and is responsible for supplying tracking channels and tracking profiles to the workflow runtime engine.
  • Tracking profile. The tracking profile determines what information will be tracked and, as such, acts as a filter to reduce the amount of information generated, ultimately making it easier to manage tracking data.

The initialization sequences around profiles and tracking channels, shown in the diagram are described in the section describing how to write your own tracking channel.

Tracking Profiles

During its run time, as already discussed, a workflow instance emits tracking events to the runtime tracking infrastructure. The runtime-tracking infrastructure uses a TrackingProfile to filter these tracking events and returns to the tracking service tracking records that are based on this filtering. This model provides developers and administrators a flexible way to determine how much data, if any, they want to track without making code changes.

The tracking profile defines a list of "track points," which are the events that will be passed on to the tracking service to be persisted. There are track points that are specific to each of the three event types—workflow, activity execution, and user events. When a track point is matched, the runtime-tracking infrastructure returns the data that is associated with the tracking event to the tracking service over the TrackingChannel that is associated with that service. The data is returned in either an ActivityTrackingRecord, a WorkflowTrackingRecord, or a UserTrackingRecord, depending on the type of track point that was matched.

Tracking profiles can be created and manipulated programmatically using the Workflow TrackingProfile class, which is the technique used by many of the SDK tracking samples. Alternatively, a tracking profile can be serialized to XML by using the TrackingProfileSerializer class, which provides both a convenient format for profile storage and the ability to author profiles in a non-programmatic fashion.

The following serialized tracking profile provides a set of tracking points that cover all the workflow events (for example, <TrackingWorkflowEvent>Created</TrackingWorkflowEvent>) and activity-execution events (for example, <ExecutionStatus>Initialized</ExecutionStatus>). In effect, there is no filtering being applied with this tracking profile.

<TrackingProfile 
xmlns="https://schemas.microsoft.com/winfx/2006/workflow/trackingprofile"
  version="3.0.0.3">
  - <TrackPoints>
    - <WorkflowTrackPoint>
      - <MatchingLocation>
        - <WorkflowTrackingLocation>
          - <TrackingWorkflowEvents>
            <TrackingWorkflowEvent>Created</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Completed</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Idle</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Suspended</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Resumed</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Persisted</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Unloaded</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Loaded</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Exception</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Terminated</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Aborted</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Changed</TrackingWorkflowEvent>
            <TrackingWorkflowEvent>Started</TrackingWorkflowEvent>
          </TrackingWorkflowEvents>
        </WorkflowTrackingLocation>
      </MatchingLocation>
    </WorkflowTrackPoint>
    - <ActivityTrackPoint>
      - <MatchingLocations>
        - <ActivityTrackingLocation>
          - <Activity>
            <Type>System.Workflow.ComponentModel.Activity, 
System.Workflow.ComponentModel, Version=3.0.0.0, Culture=neutral, 
PublicKeyToken=31bf3856ad364e35</Type>
            <MatchDerivedTypes>true</MatchDerivedTypes>
          </Activity>
          - <ExecutionStatusEvents>
            <ExecutionStatus>Initialized</ExecutionStatus>
            <ExecutionStatus>Executing</ExecutionStatus>
            <ExecutionStatus>Canceling</ExecutionStatus>
            <ExecutionStatus>Closed</ExecutionStatus>
            <ExecutionStatus>Compensating</ExecutionStatus>
            <ExecutionStatus>Faulting</ExecutionStatus>
          </ExecutionStatusEvents>
        </ActivityTrackingLocation>
      </MatchingLocations>
    </ActivityTrackPoint>
  </TrackPoints>
</TrackingProfile>

The following code extract, taken from the "Query Using SQL Tracking Service" sample, shows how to programmatically create a set of activity tracking locations and add them to a new tracking profile. If this code is viewed in conjunction with the ActivityTrackPoint section of the serialized tracking profile that was shown earlier, there is near one-to-one mapping between the code and the XML.

// Create a new tracking profile.
TrackingProfile profile = new TrackingProfile();

// Create a new activity track point.
ActivityTrackPoint activityTrack = new ActivityTrackPoint();

// Create a new activity tracking location.
ActivityTrackingLocation activityLocation = new 
ActivityTrackingLocation(typeof(Activity));
activityLocation.MatchDerivedTypes = true;

// Enumerate through the ActivityExecutionStatus enumeration values and 
add each to the activity tracking location.
IEnumerable<ActivityExecutionStatus> statuses = 
Enum.GetValues(typeof(ActivityExecutionStatus)) as 
IEnumerable<ActivityExecutionStatus>;
foreach (ActivityExecutionStatus status in statuses)
{
    activityLocation.ExecutionStatusEvents.Add(status);
}

// Add activity tracking location and track point to complete the profile.
activityTrack.MatchingLocations.Add(activityLocation);
profile.ActivityTrackPoints.Add(activityTrack);

In this example, all the possible activity-execution statuses are tracked by enumerating through the ActivityExecutionStatus enumeration values and adding them as ExecutionStatusEvents.

    public enum ActivityExecutionStatus
    {
        Initialized = 0,
        Executing = 1,
        Canceling = 2,
        Closed = 3,
        Compensating = 4,
        Faulting = 5,
    }

Although the programmatic support for tracking profiles is powerful, they do require a good understanding of the object model, which is not always intuitive for developers who are new to tracking profiles. In contrast, the "Tracking Profile Designer Sample" SDK application sample provides a visual tool to create tracking profiles from the actual workflow definitions, using the workflow designer surface. Using this approach, it is possible to track activities, for example, simply by clicking on them and selecting the appropriate event to track.

Click here for larger image

(Click on the picture for a larger image)

The tracking-profile output from the earlier sample, which tracks only executing events on the Parallel activity (denoted by the red pushpin on the activity), is the following.

<TrackingProfile 
xmlns="https://schemas.microsoft.com/winfx/2006/workflow/trackingprofile" 
version="1.0.0">
  <TrackPoints>
    <ActivityTrackPoint>
      <MatchingLocations>
        <ActivityTrackingLocation>
          <Activity>
            <Type>System.Workflow.Activities.ParallelActivity, 
System.Workflow.Activities, Version=3.0.0.0, Culture=neutral, 
PublicKeyToken=31bf3856ad364e35</Type>
            <MatchDerivedTypes>false</MatchDerivedTypes>
          </Activity>
          <ExecutionStatusEvents>
            <ExecutionStatus>Executing</ExecutionStatus>
          </ExecutionStatusEvents>
        </ActivityTrackingLocation>
      </MatchingLocations>
    </ActivityTrackPoint>
  </TrackPoints>
</TrackingProfile>

Writing Your Own Tracking Service

For most scenarios, a database such as SQL Server makes an excellent choice as a persistence store for tracking information, with high-performance storage and excellent query capabilities. However, there may be times when the tracking information should be delivered to a remote location, through Web service message queuing, or for consumption by another application, such as real-time monitor. In these cases, it is a relatively straightforward task to write a custom tracking service to meet such requirements. It can also be an instructive process to understand how the tracking infrastructure works.

As you can see from the earlier architectural diagram, to write your own tracking service, you must implement both a tracking service and a tracking channel. The service is responsible for interacting with the workflow runtime and handling tracking profiles, while the channel must handle the events generated by the workflow runtime and send them to the appropriate destination.

The "Console Tracking Service" SDK application sample is an example of a very simple tracking service that outputs text to the console, and implements both a console-tracking service and a console channel.

The ConsoleTrackingService class, which derives from TrackingService, defines the interface that the tracking service must implement to interact with the workflow runtime infrastructure. The workflow runtime requests a TrackingProfile for a specific workflow instance (or workflow type) by calling one of the overloaded GetProfile or TryGetProfile methods. The sample console service implements only one profile, irrespective of the type of workflow being run, and that profile is a very basic one that provides tracking points to cover all workflow, activity execution, and user events, which it creates dynamically in code. This contrasts with the SqlTrackingService, which stores tracking profiles in the database, and stores then on a per-workflow basis.

    public class TrackingProfile
    {
        public TrackingProfile();
        public ActivityTrackPointCollection ActivityTrackPoints { get; }
        public UserTrackPointCollection UserTrackPoints { get; }
        public Version Version { get; set; }
        public WorkflowTrackPointCollection WorkflowTrackPoints { get; }
    }

The workflow runtime also calls GetTrackingChannel on the ConsoleTrackingService object to request a TrackingChannel object. The sample console service provides an implementation of TrackingChannel in the form of the TrackingChannelSample class, which implements just one method, Send, whose job it is to process the TrackingRecord (WorkflowTrackingRecord, ActivityTrackingRecord, and UserTrackingRecord) that is supplied by the runtime. The console-tracking channel simply outputs the record information, depending on the type of record, to the console.

When it is run, the application should produce output that is similar to the following.

         *** Workflow Tracking Record***
Status: Created

_** Workflow Tracking Record**_
Status: Started

_** Activity Tracking Record**_
QualifiedName: SampleWorkflow
Type: 
Microsoft.Samples.Workflow.ConsoleTrackingServiceSample.SampleWorkflow
Status: Executing

_** Activity Tracking Record**_
QualifiedName: code1
Type: System.Workflow.Activities.CodeActivity
Status: Executing

_** User Activity Record**_
QualifiedName: code1
ActivityType: System.Workflow.Activities.CodeActivity
Args: Hello - this is a UserTrackPoint

_** Activity Tracking Record**_
QualifiedName: code1
Type: System.Workflow.Activities.CodeActivity
Status: Closed

_** Activity Tracking Record**_
QualifiedName: SampleWorkflow
Type: 
Microsoft.Samples.Workflow.ConsoleTrackingServiceSample.SampleWorkflow
Status: Closed

_** Workflow Tracking Record**_
Status: Completed

The Workflow Monitor

The "Workflow Monitor" SDK application sample really brings the whole tracking story together. It provides a graphic tool for examining the workflow tracking information stored in the SQL tracking store. Selecting one of the workflows bring up all the activity-execution details; and, if the workflow assembly can be accessed (through the Global Assembly Cache or the workflow monitor's own directory), it also displays the workflow graphically in the hosted workflow designer, with tick glyphs to show which activities have already been executed. The sample also supports workflow tracking in a real-time monitor mode, where it polls the tracking store and updates the tracking information in real time.

Click here for larger image

(Click on the picture for a larger image)

As mentioned earlier, to track applications using this tool, SQL Workflow Tracking must be enabled within the workflow that is being monitored.

WorkflowRuntime workflowRuntime = new WorkflowRuntime())

// Add the following three lines for tracking.
SqlTrackingService sqlTrackingService = new SqlTrackingService("Initial 
Catalog=Tracking;Data Source=localhost;Integrated Security=SSPI;");
sqlTrackingService.IsTransactional = false;
workflowRuntime.AddService(sqlTrackingService);
...
workflowRuntime.StartRuntime();

It is important to set the sqlTrackingService.IsTransactional flag to false, which indicates that the runtime should handle events irrespective of transactions; otherwise, you will not see the workflows in real time.

Conclusion

As this article has shown, the tracking service is one of the most powerful services that developers using Windows Workflow Foundation have at their disposal. Whenever you think about wanting to "look inside" a workflow, to see how its execution is proceeding, or if it is blocking on some external event, the tracking service is the best way to do this.

It is easy to set up, and the tracking-profile capabilities allow you to select which data is persisted. By surfacing application-specific information, you can easily extract the data that you need.

The SqlTrackingService, which uses SQL Server and is provided with Windows Workflow Foundation, should satisfy most users' needs, with its high-performance storage and excellent query capabilities. However, the tracking-service model is extensible, so that you can write your own tracking service, with its own store and channel, if required.

References

[1] "Windows Workflow Foundation: Tracking Services Deep Dive"

[2] Workflow on MSDN
https://msdn.microsoft.com/workflow

[3] WF MSDN Forum
https://forums.microsoft.com/msdn/showforum.aspx?forumid=122&siteid=1

[4] WF Community Site
https://wf.netfx3.com

About the Author

David Gristwood has been at Microsoft for over 12 years, and currently works in the United Kingdom in the Developers and Platform Group. He has written a number of MSDN and technical articles, and is a regular speaker at Microsoft events, such as TechEd. You can visit his blog at //blogs.msdn.com/david_gristwood.

© Microsoft Corporation. All rights reserved.