images

Workflow tracking is a built-in mechanism that automatically instruments your workflows. By simply adding a tracking participant to the workflow runtime, you are able to track and record status and event data related to each workflow and each activity within a workflow.

The chapter begins with an overview of the workflow tracking functionality included with Windows Workflow Foundation (WF). Following the overview, a more detailed discussion of tracking records, profiles, and participants is presented.

The chapter contains numerous examples that demonstrate how to use the ETW (Event Tracing for Windows) tracking participant that is included with WF. A series of examples is presented that demonstrate how tracking profiles can be used to filter the type of tracking data that is passed to a tracking participant. The creation of custom tracking records is also demonstrated.

The ability to develop your own tracking participant is important since it allows you to directly consume the tracking records in any way that is needed by your application. Two different custom tracking participants are implemented in this chapter.

The chapter concludes with a demonstration of how to configure workflow tracking for declarative workflow services and how to load the tracking configuration from an App.config file.

Understanding Workflow Tracking

Visibility is one of the key benefits to using WF. So far, you’ve seen evidence of this visibility at design time. Using the workflow designer, you visually declare what steps a workflow should execute, the sequence of each step, and the conditions that must be true for each step to execute. You can later view the workflow model that you designed and quickly discern the relationships between activities. This design-time visibility eases the initial development and, more importantly, future maintenance of the workflow.

Visibility at runtime is also an important key benefit of using WF. Since workflows execute within the confines of the workflow runtime engine, they truly operate in a black box. This boundary between the host application and the runtime environment increases the need for some way to monitor the progress of the individual activities within a workflow. Without a built-in mechanism to monitor their execution, you would have to instrument each workflow using your own mechanism.

WF provides such a built-in tracking mechanism. Workflow tracking can monitor each workflow throughout its entire life cycle, tracking important events along the way. Tracking data can be gathered for the workflow as a whole or for individual activities within the workflow. You can even extract argument or variable values from running workflows. And best of all, this tracking mechanism is automatically available without any changes to your activities or workflows.

Uses of Workflow Tracking

As I already mentioned, visibility is the key reason to use workflow tracking. The ability to peek inside the workflow runtime and monitor the progress of a running workflow can be useful in a number of scenarios:

  • During development to test and validate the workflow behavior.
  • Monitoring the status of workflows in a production environment.
  • Performance monitoring for the continuous improvement of production workflow applications.
  • Extraction of workflow data for integration with other applications.

Workflow Tracking Architecture

The WF tracking mechanism was designed using a flexible publish and subscribe architecture. The workflow runtime publishes raw tracking records that can be consumed by one or more tracking participants. It uses a class derived from the abstract TrackingProvider class to manage the publishing and filtering of tracking records. Each tracking participant can use the data as it sees fit to meet the needs of that particular participant. Custom participants might be developed to persist tracking data in various forms (to the file system, to a SQL Server database, and so on), to provide simple status notifications to the host application or to pass the relevant data to another application.

Tracking profiles act as filters for the raw tracking data that is passed to each participant. Each profile contains one or more tracking queries that specify the type of tracking records to include. The tracking queries also allow you to further limit the flow of data based on attributes of each tracking record. For example, you might want to limit the tracking data to a limited set of execution states (Executing, Closed, and so on) for each activity. And you can also filter the tracking data by activity name. Figure 14-1 shows an overview of the WF tracking architecture.

images

Figure 14-1. Tracking overview

In the sections that follow, I provide additional information on the three major components of workflow tracking: tracking records, profiles, and participants. But before diving into these details, it is important to know that it is actually very easy to utilize workflow tracking in your application. You can generally follow these steps:

  1. Determine the reason that you want to use tracking. Do you want to monitor live workflows, improve performance, transmit data to another application, and so on?
  2. Based on the reason for wanting to use tracking, select a tracking participant. If necessary, develop a custom tracking participant that meets your specific needs.
  3. Create an instance of the tracking participant.
  4. Create a tracking profile, and add it to the tracking participant.
  5. Add the tracking participant to the workflow instance prior to running the instance.
  6. View or otherwise consume the tracking data.

Tracking Records

The tracking data that is produced by the workflow runtime takes the form of a tracking record. WF includes a large variety of different tracking record definitions, with each one containing the additional properties needed to track a particular type of workflow event.

But the base class for all tracking data is the TrackingRecord class. This class includes the properties that are common to all tracking records. Here are the most important properties of the TrackingRecord class:

images

Figure 14-2 shows the relationship between the other tracking records that are provided with WF.

images

Figure 14-2. Tracking record hierarchy

In the sections that follow, I provide a short description of each of these tracking records. This will provide you with a good overview of the type of data that is made available to you by workflow tracking.

WorkflowInstanceRecord

A WorkflowInstanceRecord contains additional data related to the workflow instance as a whole. One of these records is produced when the workflow state has changed. Like all of the tracking records, WorkflowInstanceRecord is derived from TrackingRecord, so all the properties listed for TrackingRecord are also supported. Here are the most important additional properties of this class:

images

The State property identifies the workflow state and is one of the string values that are defined in the WorkflowInstanceStates class:

  • Aborted
  • Canceled
  • Completed
  • Deleted
  • Idle
  • Persisted
  • Resumed
  • Started
  • Suspended
  • Terminated
  • UnhandledException
  • Unloaded
  • Unsuspended

WF also provides these four specialized classes that derive from WorkflowInstanceRecord:

images

The first three of these classes provide an additional string property named Reason. This property is used to identify the reason the workflow instance was aborted, suspended, or terminated. The WorkflowInstanceUnhandledExceptionRecord provides these two additional properties:

images

The ActivityInfo class defines basic properties that identify an activity. This class is used in several of the other tracking records whenever a specific activity is identified. Here are the properties supported by the ActivityInfo class:

images

ActivityStateRecord

An ActivityStateRecord is produced each time the state of an individual activity changes. The most important additional properties of the ActivityStateRecord are as follows:

images

The State property is one of the states defined by the ActivityState class:

  • Executing
  • Canceled
  • Faulted
  • Closed
ActivityScheduledRecord

A ActivityScheduledRecord is produced when execution of a child activity is scheduled by its parent. The most important additional properties of this class are as follows:

images

BookmarkResumptionRecord

A BookmarkResumptionRecord is produced to record the data related to the resumption of a bookmark. The additional properties for this class include the following:

images

CancelRequestedRecord

A CancelRequestedRecord is produced when a child activity is canceled by its parent. The most important additional properties for this class are as follows:

images

FaultPropagationRecord

A FaultPropagationRecord is produced when an exception is thrown within the workflow. The additional properties for this class include the following:

images

CustomTrackingRecord

The CustomTrackingRecord is produced when you explicitly add code to a custom activity to produce it. It allows you to track any meaningful events or data within your custom activities. The additional properties provided by this class are as follows:

images

The CustomTrackingRecord class is the parent of these three specialized classes that derive from it:

images

The InteropTrackingRecord class provides this additional property:

images

The SendMessageRecord class provides this additional property:

images

The ReceiveMessageRecord class provides these additional properties:

images

Tracking Profiles

Tracking profiles are used to filter the tracking records that are passed to a tracking participant and are defined using the TrackingProfile class. Without a tracking profile, all possible tracking records are passed to the participant. This might result in a much larger set of tracking data than is really necessary for your particular needs. By using a tracking profile, you can tune the type and amount of tracking data that is passed to the tracking participant.

Here are the most important properties of the TrackingProfile class:

images

There are several tracking query classes that derive from the TrackingQuery class. Each one contains additional properties that target queries against a different type of TrackingRecord:

images

Each tracking profile that you create can consist of any combination of these tracking query objects. Multiple instances of the same query type are supported. For example, you might need to select tracking data for only a small set of activities by name. To accomplish this, you might need to include an ActivityStateQuery (or one of the other queries) for each named activity.

Tracking profiles are generally created in code and added to a tracking participant. Alternatively, for declarative workflow services, they can be defined in the Web.config file and read directly at runtime.

images Note WF does not directly read tracking profiles from an App.config file for nonmessaging workflows. However, with a small amount of code, you can read a tracking profile from an App.config and pass it to a tracking participant. This is demonstrated in one of the examples presented later in this chapter.

In the sections that follow, I provide a brief overview of each query type and a list of their most important properties. You will quickly see a direct correlation between a tracking query and the tracking record that it targets.

The values that you specify for a query are used to limit the tracking records that are sent to the tracking participant. If you specify a value for more than one property of a query, all of the values must match the tracking record in order for the record to be included. For example, entering an ActivityName and a State value for an ActivityStateQuery requires that the tracking record be produced by the named activity and that the state match the one that you specified.

You can specify * (all) as a wildcard value for the query properties. For example, the WorkflowInstanceQuery includes a States property, which is a collection of WorkflowInstanceStates. To specify a value for this property, you can explicitly list the states that you want to include in the profile. Alternatively, you can specify * to include all possible states.

WorkflowInstanceQuery

A WorkflowInstanceQuery is used to select instances of the WorkflowInstanceRecord class within a profile. Here is the additional property that this query class provides:

images

ActivityStateQuery

An ActivityStateQuery is used to select instances of the ActivityStateRecord. Here are the additional properties for this class:

images

ActivityScheduledQuery

An ActivityScheduledQuery is used to select instances of the ActivityScheduledRecord. Here are the additional properties supported by this class:

images

BookmarkResumptionQuery

A BookmarkResumptionQuery is used to select instances of the BookmarkResumptionRecord. Here is the additional property provided by this class:

images

CancelRequestedQuery

A CancelRequestedQuery is used to select instances of the CancelRequestedRecord. The additional properties for this class are as follows:

images

FaultPropagationQuery

A FaultPropagationQuery is used to select instances of the FaultPropagationRecord. The additional properties for this class are as follows:

images

CustomTrackingQuery

A CustomTrackingQuery is used to select instances of the CustomTrackingRecord. Additional properties for this class include the following:

images

Tracking Participants

Tracking participants are the components in the tracking system that receive and work with the tracking records. The records that they receive are first filtered by the tracking profile.

WF includes the TrackingParticipant class that all tracking participants must use as their base class. When you need to develop your own tracking participant, you derive from this base class and provide an implementation for the abstract Track method.

images Note An example later in this chapter demonstrates how to implement a custom tracking participant.

Out of the box, WF includes a tracking participant (the EtwTrackingParticipant class) that records tracking records using the Event Tracing for Windows system. ETW is a greatly enhanced version of the standard Windows event logging system. Just like the standard event logging, ETW logs can be viewed with the Windows Event Viewer management console plug-in.

Using ETW Workflow Tracking

The easiest way to demonstrate workflow tracking is to use the ETW tracking participant that is included with WF. This allows you to immediately see the type of tracking data that is produced by the workflow runtime without the need to first develop your own tracking participant. The tracking data that is produced is viewable using the Windows Event Viewer management console plug-in.

But before you can view tracking data, you need to implement a workflow to track. To satisfy this need, you will declare a workflow that builds upon the examples in Chapter 13. In that chapter, you implemented a series of workflows that update inventory data in the AdventureWorks sample database. For the examples in this chapter, you will declare a greatly simplified workflow that updates the same inventory data. You will reuse the custom activities that you developed for Chapter 13 but will declare a new workflow. The workflow for this chapter does not require the activities that generated a test exception or the transaction or compensation activities.

You will complete these tasks to implement this example:

  1. Reference the AdventureWorksAccess project from Chapter 13.
  2. Copy selected custom activities from Chapter 13.
  3. Implement a new example workflow.
  4. Use an ETW tracking participant within the workflow hosting code.
  5. Enable the collection of workflow tracking data within the ETW system.
  6. View the tracking data after execution of the workflow.

Providing AdventureWorks Access

The custom activities that you will copy in the next step reference the AdventureWorks database tables using LINQ to SQL classes. In Chapter 13, LINQ to SQL classes were generated from the database schema and added to a project named AdventureWorksAccess.

For the examples in this chapter, you can directly reuse that project without any changes. In the steps that follow, the assumption is that you are reusing the existing project by adding it to a new solution for this chapter. Please follow these steps:

  1. Create a new empty Visual Studio solution named for this chapter.
  2. Use the Add Existing Project option to add the AdventureWorksAccess project from Chapter 13 to the solution for this chapter.
  3. Build the solution to make sure it builds correctly.

Copying the Custom Activities

Add a new project to the solution for this chapter using the Activity Library workflow project template, and name the project ActivityLibrary. You can delete the Activity1.xaml file that is generated since it won’t be used. Add these references to the ActivityLibrary project if they are not already added for you:

  • AdventureWorksAccess (project reference)
  • System.Transactions
  • System.Data.Linq
  • System.Xml.Linq

Make a copy of three of the custom activities from the ActivityLibrary project in Chapter 13, adding each copy to the newly created ActivityLibrary project for this chapter. You’ll want to make a copy rather than simply referencing the existing code since you will be modifying one of these activities in an example later in this chapter. Here are the custom activity source files to copy:

  • GetOrderDetail.cs
  • InsertTranHistory.cs
  • UpdateProductInventory.cs

Please refer to Chapter 13 for a complete listing and discussion of these activities. Build the solution before proceeding to the next step. This ensures that everything builds correctly and also adds these custom activities to the Visual Studio Toolbox.

Declaring the Workflow

Unlike most of the examples in this book, you will add this example workflow to the ActivityLibrary instead of implementing it in the host application. Doing this allows you to repackage the workflow for use within a declarative workflow service later in the chapter.

Add a new workflow to the ActivityLibrary project using the Activity Add New Item template. Name the workflow UpdateInventory. The steps needed to declare this workflow are similar to those that you followed in Chapter 13. However, many of the steps are no longer needed for this simplified version of the workflow.

Begin by adding a Flowchart activity to the empty workflow. Add this single argument to the workflow:

images

The workflow also requires this single variable:

images

Please follow these steps to complete the declaration of the UpdateInventory workflow:

  1. Add a GetOrderDetail activity to the flowchart. Set the OrderDetail property to OrderDetail and the SalesOrderId property to ArgSalesOrderId. This activity will retrieve the SalesOrderDetail rows for the requested ArgSalesOrderId and place the result in the OrderDetail variable. Drag a connection from the start of the flowchart to the top of this activity.
  2. Add a ForEach<T> activity below the GetOrderDetail activity. Set the generic type to AdventureWorksAccess.SalesOrderDetail, and set the Values property to the OrderDetail variable. Change the DisplayName to ApplyUpdates to better identify the purpose of this activity. Drag a connection from the GetOrderDetail activity to this activity.
  3. Expand the ApplyUpdates activity (the ForEach<T> that you just added), and add an UpdateProductInventory activity as the only child. Set the SalesDetail property to item. Figure 14-3 shows the completed ApplyUpdates activity.
    images

    Figure 14-3. ApplyUpdates activity

  4. Return to the root Flowchart activity, and add another ForEach<T> activity below the ApplyUpdates activity. Set the generic type to AdventureWorksAccess.SalesOrderDetail, and set the Values property to the OrderDetail variable. Change the DisplayName property to InsertHistory. Drag a connection from the ApplyUpdates activity to this new ForEach<T> activity.
  5. Expand the InsertHistory activity, and add an InsertTranHistory activity as the only child. Set the SalesDetail property to item. Figure 14-4 shows the completed InsertHistory activity.

Figure 14-5 shows the completed workflow.

images

Figure 14-4. InsertHistory activity

images

Figure 14-5. Complete UpdateInventory workflow

Hosting the Workflow

To host the workflow, create a new Workflow Console Application project named UpdateInventoryTracking, and add it to the solution for this chapter. Delete the Workflow1.xaml file that is generated with the new project since it won’t be used. Add these references to the new project:

  • AdventureWorksAccess (project reference)
  • ActivityLibrary (project reference)

Revise the Program.cs file to use the EtwTrackingParticipant for tracking when the UpdateInventory workflow is executed. Here is the code that you need for the Program.cs file:

namespace UpdateInventoryTracking
{
    using System;
    using System.Activities;
    using System.Activities.Tracking;
    using System.Collections.Generic;
    using ActivityLibrary;

    class Program
    {
        static void Main(string[] args)
        {
            UpdateInventory wf = new UpdateInventory();
            wf.ArgSalesOrderId = 43687;
            WorkflowInvoker instance = new WorkflowInvoker(wf);
            instance.Extensions.Add(new EtwTrackingParticipant());
            instance.Invoke();
        }
    }
}

In this first example, a new instance of the EtwTrackingParticpant is created and added to the Extensions property of the WorkflowInvoker instance. Since a tracking profile has not been added to the EtwTrackingParticipant, no filtering of tracking records will be performed. This means that all potential tracking records will be produced. In subsequent examples, you will modify this code by adding a tracking profile to control the type of tracking data that is produced.

The same sales order ID that was used for the examples in Chapter 13 is also used here. This example uses a WorkflowInvoker for simplicity. You can also add a tracking participant to a WorkflowApplication instance in a similar way.

Enabling ETW Workflow Tracking

If you build the solution and run the UpdateInventoryTracking project now, the workflow should execute correctly and update the AdventureWorks database. However, no tracking data will be produced because, by default, the ETW log that controls workflow tracking data is not enabled. To enable workflow tracking, you need to follow these steps:

  1. Open the Windows Event Viewer management console plug-in. The easiest way to open this application is to do so directly by entering eventvwr.msc on the command line.
  2. Navigate to the category that is used for workflow tracking. All workflow tracking records are managed and found under the Applications and Services Logs category. Expand this category, and you should see a category named Microsoft. Under it is another subcategory named Windows.
  3. Right-click the Windows category (under Microsoft), and select the View option. Make sure that the Show Analytic and Debug Logs option is selected. If this option is disabled, you won’t be able to view and manage the workflow tracking data.
  4. The workflow tracking data is located two category levels under the Windows category. First you need to expand the Application Server-Applications category, and under it you should find the Analytic category. The workflow tracking records are managed under the Analytic category.

Figure 14-6 shows a partial view of the Event Viewer category tree that was just described.

images

Figure 14-6. Log tree view within Event Viewer

To enable ETW workflow tracking on systems running Vista or later, you need to right-click the Analytic category, and select the Enable Log option. The option to enable the log is also available in a control panel on the right side of the Event Viewer. This panel also contains options to disable, save, and clear the log.

Once this log is enabled, it will continue to record workflow tracking data when an EtwTrackingParticipant instance is added as an extension to a workflow instance.

images Tip Once you have enabled logging, leave the Windows Event Viewer open since you will be using it to view the workflow tracking records.

Testing the Workflow

Now that you have enabled ETW logging, you should be able to run the UpdateInventoryTracking project and produce working tracking records. Here are the results when I run the project, proving that the workflow itself is operating as expected:


Product 768: Reduced by 1

Product 765: Reduced by 2

Product 768: Added history for Qty of 1

Product 765: Added history for Qty of 2

Viewing the Tracking Data

To view the tracking data, you can return to the Windows Event Viewer and refresh the Analytic log under the Application Server-Applications category. Figure 14-7 shows the log after running the UpdateInventoryTracking project.

images

Figure 14-7. Sample Analytic log

The log viewer allows you to examine the values for each tracking record by double-clicking an entry in the list or by selecting the detail tab found in the lower half of the form. Once you select the detail tab, you can view the tracking record in basic or XML format.

Here is an example of the first tracking record after I saved it to an XML file:


  <Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>

    <System>

      <Provider Name='Microsoft-Windows-Application Server-Applications'

          Guid='{c651f5f6-1c0d-492e-8ae1-b4efd7c9d503}'/>

      <EventID>100</EventID>

      <Version>0</Version>

      <Level>4</Level>

      <Task>0</Task>

      <Opcode>0</Opcode>

      <Keywords>0x20000000000e0040</Keywords>

      <TimeCreated SystemTime='2009-10-22T00:41:24.010Z'/>

      <EventRecordID>0</EventRecordID>

      <Correlation/>

      <Execution ProcessID='3280' ThreadID='12' ProcessorID='0' KernelTime='245'  

          UserTime='168'/>

      <Channel>Microsoft-Windows-Application Server-Applications/Analytic</Channel>

      <Computer>VistaBase</Computer>

      <Security UserID='S-1-5-21-2006389094-1177327066-4125713969-1000'/>

    </System>

    <EventData>

      <Data Name='InstanceId'>{B5A38A3C-FD82-4833-8D11-4DB46012A4C6}</Data>

      <Data Name='RecordNumber'>0</Data>

      <Data Name='EventTime'>2009-10-22T00:41:23.801Z</Data>

      <Data Name='ActivityDefinitionId'>UpdateInventory</Data>

      <Data Name='State'>Started</Data>

      <Data Name='Annotations'>&lt;items /&gt;</Data>

      <Data Name='ProfileName'></Data>

      <Data Name='HostReference'></Data>

      <Data Name='AppDomain'>UpdateInventoryTracking.exe</Data>

    </EventData>

    <RenderingInfo Culture='en-US'>

      <Message>TrackRecord= WorkflowInstanceRecord,

          InstanceID = {B5A38A3C-FD82-4833-8D11-4DB46012A4C6}, RecordNumber = 0,

          EventTime = 2009-10-22T00:41:23.801Z,

          ActivityDefinitionId = UpdateInventory, State = Started,

          Annotations = &lt;items /&gt;, ProfileName = </Message>

      <Level>Information</Level>

      <Task></Task>

      <Opcode>Info</Opcode>

      <Channel>Analytic</Channel>

      <Provider></Provider>

      <Keywords>

        <Keyword>WF Tracking</Keyword>

        <Keyword>End-to-End Monitoring</Keyword>

        <Keyword>Health Monitoring</Keyword>

        <Keyword>Troubleshooting</Keyword>

      </Keywords>

    </RenderingInfo>

  </Event>

This saved entry is organized into three major XML elements. The System element contains the data that is common to all ETW log entries. The EventData element contains the workflow tracking data, and the RenderingInfo element contains an interpreted version of the raw data. The RenderingInfo element was generated by selecting “Display information for this language” when I saved the log entries to an XML file. In this case, it was rendered for U.S. English.

By examining this data, you quickly determine that it is a WorkflowInstanceRecord for the UpdateInventory workflow and that the current workflow state is Started. In the table that follows, I outline the complete set of workflow tracking records that were produced for this simple example:

images

images

I’ve obviously had to omit most of the detail that was produced for each of these records. But if you take the time to view these records on your own system, you’ll gain a better appreciation of the type of data that is produced for each record type. Particularly interesting is the fact that many of these records include a serialized form of the arguments that are passed to each activity.

images Tip It may be helpful to clear the ETW log before you run each subsequent example in this chapter. Doing this allows you to more easily locate the records from the most recent test. Before a log can be cleared, it must first be disabled.

Using Tracking Profiles

In the examples that follow, you will construct a tracking profile to filter the tracking records that are produced by the UpdateInventory workflow. The tracking profile will be built incrementally, allowing you to see the results as each change is made to the profile. It will begin with a single tracking query but will be enhanced in subsequent examples to include additional tracking records.

Including Selected Workflow Instance States

This first version of the tracking profile will select only WorkflowInstanceRecords that have a state of Started or Completed. A simple profile like this can be used to track when each workflow begins and ends—but that’s just about all that it provides. Modify the Program.cs file of the UpdateInventoryTracking project to look like this:

namespace UpdateInventoryTracking
{
    using System;
    using System.Activities;
    using System.Activities.Tracking;
    using System.Collections.Generic;
    using ActivityLibrary;

    class Program
    {
        static void Main(string[] args)
        {
            UpdateInventory wf = new UpdateInventory();
            wf.ArgSalesOrderId = 43687;
            WorkflowInvoker instance = new WorkflowInvoker(wf);

            EtwTrackingParticipant tp = new EtwTrackingParticipant();
            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                    new WorkflowInstanceQuery
                    {
                        States =
                        {
                            WorkflowInstanceStates.Started,
                            WorkflowInstanceStates.Completed,
                        }
                    }
                }
            };

            instance.Extensions.Add(tp);
            instance.Invoke();
        }
    }
}

If you build the solution and run the UpdateInventoryTracking project, you should see only these tracking records produced:

images

Including All Workflow Instance States

Instead of selecting only one or two workflow instance states, you might want to see all state changes for the workflow. To accomplish this, you can modify the profile to look like this:

namespace UpdateInventoryTracking
{

    class Program
    {
        static void Main(string[] args)
        {

            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                    new WorkflowInstanceQuery
                    {
                        States = {"*"}
                    }
                }
            };

        }
    }
}

By specifying the wildcard symbol (*), you should see all possible workflow state changes. When you run the project again, the tracking results should look like this:

images

The three Idle state changes are caused by the asynchronous execution of the GetOrderDetail and UpdateProductInventory custom activities.

Adding Selected Activity States

You might also want to know when all activities within the workflow begin and end. To accomplish this, you can modify the profile to include an ActivityStateQuery like this:

namespace UpdateInventoryTracking
{

    class Program
    {
        static void Main(string[] args)
        {

            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                    new WorkflowInstanceQuery
                    {
                        States = {"*"}
                    },
                    new ActivityStateQuery
                    {
                        States =
                        {
                            ActivityStates.Executing,
                            ActivityStates.Closed
                        }
                    }
                }
            };

        }
    }
}

This time, when you run the UpdateInventoryTracking project, the results should look like this:

images

images

Targeting Selected Activities

In the previous tracking profiles, you saw how to limit the kind of tracking records that are produced for all activities. You can also construct a tracking profile that targets one or more named activities. You may find this useful during the initial development of the workflow or for a targeted approach to performance tuning once the workflow is in production.

The profile that follows retrieves the activity state data for only a selected list of activities. It also demonstrates how to extract named arguments for an activity and how to add an annotation to a query.

namespace UpdateInventoryTracking
{

    class Program
    {
        static void Main(string[] args)
        {

            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                    new WorkflowInstanceQuery
                    {
                        States =
                        {
                            WorkflowInstanceStates.Started,
                            WorkflowInstanceStates.Completed,
                        }
                    },

The two queries that follow use the wildcard indicator for the Arguments property. This causes all arguments for the activity to be extracted and included in the tracking record.

                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "*" }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "InsertTranHistory",
                        States = {"*"}
                    },

The next two queries both reference the same named activity. The first one targets the Executing state, while the second specifies the Closed state. For the Executing state, the SalesDetail argument is extracted and included in the tracking record. This particular query also demonstrates the use of an annotation. An annotation is simply additional text that is included in the tracking record to provide a meaningful description for the activity, argument, or state.

                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "SalesDetail" },
                        QueryAnnotations =
                        {
                            {"Threading Model", "Asynchronous update"}
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Closed }
                    }
                }
            };

        }
    }
}

When you run the project, the tracking results should look like this:

images

Adding Selected Scheduled Records

In this next example, the previous profile is enhanced to also include the ActivityScheduledRecord when a specific named child activity is scheduled.

namespace UpdateInventoryTracking
{

    class Program
    {
        static void Main(string[] args)
        {

            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                   new WorkflowInstanceQuery
                    {
                        States =
                        {
                            WorkflowInstanceStates.Started,
                            WorkflowInstanceStates.Completed,
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "*" }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "InsertTranHistory",
                        States = {"*"}
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "SalesDetail" },
                        QueryAnnotations =
                        {
                            {"Threading Model", "Asynchronous update"}
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Closed }
                    },
                    new ActivityScheduledQuery
                    {
                        ChildActivityName = "UpdateProductInventory"
                    }
                }
            };

        }
    }
}

The results are similar to the previous example but they now include the selected scheduled records:

images

Including Custom Tracking Records

WF also allows you to create custom tracking records within your custom activities. These tracking records can be used to track any meaningful event or data that is not already provided by one of the standard tracking records.

Creating Custom Tracking Records

To demonstrate how to create custom tracking records, you will modify the GetOrderDetail custom activity that can be found in the ActivityLibrary project. This is one of the custom activities that you copied from the example code in Chapter 13. You will need to add a using statement for the System.Activities.Tracking namespace in order to complete the changes.

images Note You can find a full description of this activity in Chapter 13.

Modify the GetOrderDetail.cs file to include the additional tracking code shown here:

using System;
using System.Activities;
using System.Collections.Generic;
using System.Linq;
using AdventureWorksAccess;

using System.Activities.Tracking;

namespace ActivityLibrary
{
    public sealed class GetOrderDetail : AsyncCodeActivity
    {
        public InArgument<Int32> SalesOrderId { get; set; }
        public OutArgument<List<SalesOrderDetail>> OrderDetail { get; set; }

        protected override IAsyncResult BeginExecute(
            AsyncCodeActivityContext context, AsyncCallback callback,
            object state)
        {
            Func<Int32, List<SalesOrderDetail>> asyncWork =
                orderId => RetrieveOrderDetail(orderId);
            context.UserState = asyncWork;
            return asyncWork.BeginInvoke(
                SalesOrderId.Get(context), callback, state);
        }

        protected override void EndExecute(
            AsyncCodeActivityContext context, IAsyncResult result)
        {
            List<SalesOrderDetail> orderDetail =
                ((Func<Int32, List<SalesOrderDetail>>)
                    context.UserState).EndInvoke(result);

After the query against the AdventureWorks database has completed, a custom tracking record is created. In this example, the row count from the query is included as additional tracking data. The Track method of the activity context is used to record the custom tracking data.

            if (orderDetail != null)
            {
                OrderDetail.Set(context, orderDetail);

                //add custom tracking
                CustomTrackingRecord trackRec =
                    new CustomTrackingRecord("QueryResults");
                trackRec.Data.Add("Count", orderDetail.Count);
                context.Track(trackRec);
            }
        }

        private List<SalesOrderDetail> RetrieveOrderDetail(Int32 salesOrderId)
        {
            List<SalesOrderDetail> result = new List<SalesOrderDetail>();
            using (AdventureWorksDataContext dc =
                new AdventureWorksDataContext())
            {
                var salesDetail =
                    (from sd in dc.SalesOrderDetails
                     where sd.SalesOrderID == salesOrderId
                     select sd).ToList();

                if (salesDetail != null && salesDetail.Count > 0)
                {
                    result = salesDetail;
                }
            }
            return result;
        }
    }
}
Modifying the Profile

Next, you need to modify the previous tracking profile to also include custom tracking records. Here is the revised profile:

namespace UpdateInventoryTracking
{

    class Program
    {
        static void Main(string[] args)
        {

            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                    new WorkflowInstanceQuery
                    {
                        States =
                        {
                            WorkflowInstanceStates.Started,
                            WorkflowInstanceStates.Completed,
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "*" }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "InsertTranHistory",
                        States = {"*"}
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "SalesDetail" },
                        QueryAnnotations =
                        {
                            {"Threading Model", "Asynchronous update"}
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Closed }
                    },
                    new ActivityScheduledQuery
                    {
                        ChildActivityName = "UpdateProductInventory"
                    },
                    new CustomTrackingQuery
                    {
                        ActivityName = "*",
                        Name = "*"
                    }
                }
            };

        }
    }
}

The results are similar to the previous example with the addition of the custom tracking record:

images

Developing a Custom Tracking Participant

The out-of-the-box ETW tracking participant provides an easy way to view and manage workflow tracking data. However, ETW may not always be the right choice for workflow tracking. You may want to persist the tracking data in a format of your own choosing, perhaps writing it to the file system or to a SQL Server database. Or, you may not need to persist the tracking data at all. You might want to use workflow tracking as a communication mechanism and pass the data to another application in real time.

To provide you with the flexibility you need, WF supports an easy way to implement your own custom tracking participant. The steps to accomplish this are indeed very simple:

  1. Develop a new class that derives from the base TrackingParticipant class provided with WF.
  2. Provide an implementation for the abstract Track method.

The Track method is invoked by the WF runtime each time one of the workflow tracking records is available and ready to be processed. If a tracking profile was provided, the records have already been filtered by the time the Track method is invoked. In your implementation of the Track method, you have the flexibility to handle the tracking records in any way that makes sense for your application. The TrackingParticipant class also defines BeginTrack and EndTrack virtual methods. You can override these methods to implement asynchronous processing of tracking records.

Once the custom tracking participant has been developed, it is added to the workflow instance as an extension, in exactly the same way as the standard EtwTrackingParticipant class.

To demonstrate how to develop a custom tracking participant, you will implement a tracking participant that persists tracking records to the file system as separate XML files. You will then use this participant to record the tracking records that are produced by the UpdateInventory workflow.

Implementing the Tracking Record Serializer

The XML files that are persisted by the custom tracking participant will contain a serialized form of each tracking record. The actual serialization is done using a bit of reflection and some LINQ to XML code. I chose this approach because my goal was to write one set of code that would be able to easily serialize the most important data from all of the possible workflow tracking records. I wanted to avoid writing hard-coded logic to handle each and every tracking record individually.

images Note My first attempt at implementing a quick way to serialize the tracking records was to turn to the XmlSerializer class. However, I quickly discovered that the tracking records lack a parameterless constructor, which is a requirement when using the XmlSerializer. The next best approach was to write the code that you see here to perform my own XML serialization.

The XML serialization code is implemented in its own class, separate from the tracking participant. Add a new class (a normal C# class, not a workflow class) to the ActivityLibrary project, and name it TrackingRecordSerializer. Here is the implementation for this class:

using System;
using System.Activities.Tracking;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;

namespace ActivityLibrary
{
    public static class TrackingRecordSerializer
    {

The Serialize method is the only public member of this class. It is invoked from the tracking participant (implemented in the next step) as each record is being processed. The method creates the outermost root elements of the XML document and then calls a private SerializeObject method to handle the serialization of the record.

        public static String Serialize(TrackingRecord tr)
        {
            if (tr == null)
            {
                return String.Empty;
            }

            XElement root = new XElement(tr.GetType().Name);
            XDocument xml = new XDocument(root);

            SerializeObject(root, tr);
            return xml.ToString();
        }

The private SerializeObject method controls the serialization of each property of the tracking record. A design assumption of this class is that I only want to serialize public properties of each object. Special handling is provided for IDictionary properties. This was necessary in order to handle several of the tracking record properties (Annotations, Arguments, Variables). You will need to enhance this code if the data that you are tracking includes other collection types such as IList.  

        private static void SerializeObject(
            XElement parent, Object o)
        {
            PropertyInfo[] properties = o.GetType().GetProperties();
            foreach (PropertyInfo property in properties)
            {
                if (IsPropertyWeWant(property))
                {
                    if (property.PropertyType.IsGenericType)
                    {
                        if (property.PropertyType.Name == "IDictionary`2")
                        {
                            SerializeDictionary(property, parent, o);
                        }
                    }
                    else
                    {
                        Object value = property.GetValue(o, null);
                        parent.Add(new XElement(property.Name, value));
                    }
                }
            }
        }

The IsPropertyWeWant method is invoked for each property before it is serialized. In its current form, it is designed to omit certain LINQ-related properties. This was necessary for this particular example because the LINQ to SQL classes that were generated for the AdventureWorksAccess project include several of these special association properties that you wouldn’t normally want to serialize.

        private static bool IsPropertyWeWant(PropertyInfo property)
        {
            if (property.IsDefined(
                typeof(System.Data.Linq.Mapping.AssociationAttribute), true))
            {
                return false;
            }
            else
            {
                return true;
            }
        }

The SerializeDictionary and SerializeKeyValuePair methods are used when serializing an IDictionary. The goal of this code is to perform a serialization of the properties for each entry in the dictionary.

        private static void SerializeDictionary(
            PropertyInfo property, XElement parent, Object o)
        {
            XElement element = new XElement(property.Name);
            parent.Add(element);

            Object value = property.GetValue(o, null);
            if (value is IDictionary<String, String>)
            {
                foreach (var kvPair in (IDictionary<String, String>)value)
                {
                    SerializeKeyValuePair(element, kvPair.Key, kvPair.Value);
                }
            }
            else if (value is IDictionary<String, Object>)
            {
                foreach (var kvPair in (IDictionary<String, Object>)value)
                {
                    SerializeKeyValuePair(element, kvPair.Key, kvPair.Value);
                }
            }
        }

        private static void SerializeKeyValuePair(
            XElement element, Object key, Object value)
        {
            if (value == null)
            {
                return;
            }

            Type type = value.GetType();
            if (type.IsPrimitive || type == typeof(String))
            {
                element.Add(new XElement("item",
                    new XAttribute("key", key),
                    new XAttribute("value", value)));
            }
            else
            {
                XElement valueElement = new XElement("value");
                //recursive call to serialize the value object
                SerializeObject(valueElement, value);
                element.Add(new XElement("item",
                    new XAttribute("key", key), valueElement));
            }
        }
    }
}

images Note Please remember that this serialization code is not a requirement for implementing your own custom tracking participant. It is required by this particular example since I made the design decision to serialize the tracking records to XML.

Implementing the Custom Tracking Participant

Now that the serialization logic has been implemented, you can turn your attention to the tracking participant itself. This tracking participant uses an in-memory queue and a separate thread to serialize and persist each tracking record.

Add a new C# class to the ActivityLibrary, and name it FileTrackingParticipant. Here is the complete implementation for this class:

using System;
using System.Activities.Tracking;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;

namespace ActivityLibrary
{
    public class FileTrackingParticipant : TrackingParticipant
    {
        private Queue<TrackingRecord> _records = new Queue<TrackingRecord>();
        private AutoResetEvent _recordsToProcess = new AutoResetEvent(false);
        private Thread _processingThread;
        private Boolean _isThreadRunning;

        public FileTrackingParticipant()
        {
            _processingThread = new Thread(ProcessingThreadProc);
            _isThreadRunning = true;
            _processingThread.Start();
        }

The Track method is invoked for each record that is produced by the WF runtime. In this implementation, the records are immediately added to an in-memory queue. An AutoResetEvent is set in order to signal to the processing thread that one or more records are available to be processed. This approach isn’t absolutely necessary, since you could more easily serialize and persist the tracking record directly in the Track method. However, using a queue and a separate thread allows the Track method to complete more quickly and offloads the real work to a separate thread.

        protected override void Track(TrackingRecord record, TimeSpan timeout)
        {
            lock (_records)
            {
                _records.Enqueue(record);
            }
            _recordsToProcess.Set();
        }

Since this class immediately creates and starts a separate thread, a method was needed to stop the thread once all processing was complete. This Stop method will be called by the workflow host application.

        public void Stop()
        {
            _isThreadRunning = false;
            _processingThread.Join(5000);
        }

        private void ProcessingThreadProc()
        {
            while (_isThreadRunning)
            {
                if (_recordsToProcess.WaitOne(2000))
                {
                    Int32 count = 0;
                    lock (_records)
                    {
                        count = _records.Count;
                    }

                    while (count > 0)
                    {
                        TrackingRecord record = null;
                        lock (_records)
                        {
                            record = _records.Dequeue();
                            count = _records.Count;
                        }

                        if (record != null)
                        {
                            PersistRecord(record);
                        }
                    }
                }
            }
        }

The private PersistRecord method is invoked by the processing thread for each tracking record. Each record is written to a separate file, with the file name being generated from the EventTime and RecordNumber of each tracking record. A call to the static Serialize method of the TrackingRecordSerializer class is made to serialize each record to XML.

        private void PersistRecord(TrackingRecord tr)
        {
            try
            {
                String path = Path.Combine(
                    Environment.CurrentDirectory, "tracking");
                String fileName = String.Format("{0}.{1}",
                    tr.EventTime.ToString("yyyyMMdd.HHmmss.fffffff"),
                    tr.RecordNumber);
                String fullPath = Path.Combine(path, fileName + ".xml");

                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                using (FileStream stream =
                    new FileStream(fullPath, FileMode.Create))
                {
                    XmlWriterSettings settings = new XmlWriterSettings();
                    settings.Encoding = Encoding.UTF8;
                    using (XmlWriter writer = XmlWriter.Create(stream, settings))
                    {
                        writer.WriteRaw(TrackingRecordSerializer.Serialize(tr));
                    }
                }
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "PersistRecord Exception: {0}", exception.Message);
            }
        }
    }
}

Testing the Tracking Participant

To test the new tracking participant, you can modify the Program.cs file of the UpdateInventoryTracking project to use the new FileTrackingParticipant like this:

namespace UpdateInventoryTracking
{
    using System;
    using System.Activities;
    using System.Activities.Tracking;
    using System.Collections.Generic;
    using ActivityLibrary;

    class Program
    {
        static void Main(string[] args)
        {
            FileTrackingParticipant tp = new FileTrackingParticipant();

            UpdateInventory wf = new UpdateInventory();
            wf.ArgSalesOrderId = 43687;
            WorkflowInvoker instance = new WorkflowInvoker(wf);

            tp.TrackingProfile = new TrackingProfile
            {
                Name = "MyTrackingProfile",
                Queries =
                {
                    new WorkflowInstanceQuery
                    {
                        States =
                        {
                            WorkflowInstanceStates.Started,
                            WorkflowInstanceStates.Completed,
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "*" }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "InsertTranHistory",
                        States = {"*"}
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Executing },
                        Arguments = { "SalesDetail" },
                        QueryAnnotations =
                        {
                            {"Threading Model", "Asynchronous update"}
                        }
                    },
                    new ActivityStateQuery
                    {
                        ActivityName = "UpdateProductInventory",
                        States = { ActivityStates.Closed }
                    },
                    new ActivityScheduledQuery
                    {
                        ChildActivityName = "UpdateProductInventory"
                    },
                    new CustomTrackingQuery
                    {
                        ActivityName = "*",
                        Name = "*"
                    }
                }
            };

            instance.Extensions.Add(tp);
            instance.Invoke();

            tp.Stop();
        }
    }
}

The only significant changes are the creation of the FileTrackingParticipant instead of the EtwTrackingParticipant and the call to the Stop method once the workflow has completed.

When you run the UpdateInventoryProject, you should see that a racking subfolder has been created under the indebug folder for the project and that the new folder contains a number of XML files. Here is a list of the files created when I run this test:


20091021.204148.8879005.0.xml

20091021.204148.8879005.2.xml

20091021.204149.0594477.6.xml

20091021.204149.0644637.8.xml

20091021.204149.0654669.9.xml

20091021.204149.1457229.11.xml

20091021.204149.1487325.12.xml

20091021.204149.1497357.13.xml

20091021.204149.1988925.15.xml

20091021.204149.2029053.18.xml

20091021.204149.2430333.19.xml

20091021.204149.2470461.21.xml

20091021.204149.2771421.22.xml

20091021.204149.2801517.24.xml

If you take a look at the files that were generated, you’ll see that they do indeed contain the tracking records. For example, the contents of the 20091021.204149.0654669.9.xml file look like this:


<?xml version="1.0" encoding="utf-8"?><ActivityStateRecord>

  <Activity>Name=UpdateProductInventory, ActivityId = 1.9, ActivityInstanceId = 5, TypeName=ActivityLibrary.UpdateProductInventory</Activity>

  <State>Executing</State>

  <Variables />

  <Arguments>

    <item key="SalesDetail">

      <value>

        <SalesOrderID>43687</SalesOrderID>

        <SalesOrderDetailID>256</SalesOrderDetailID>

        <CarrierTrackingNumber>61FA-475A-AC</CarrierTrackingNumber>

        <OrderQty>1</OrderQty>

        <ProductID>768</ProductID>

        <SpecialOfferID>1</SpecialOfferID>

        <UnitPrice>419.4589</UnitPrice>

        <UnitPriceDiscount>0.0000</UnitPriceDiscount>

        <LineTotal>419.458900</LineTotal>

        <rowguid>9bfb4b65-3927-4084-ba66-2678173a69d0</rowguid>

        <ModifiedDate>2001-07-01T00:00:00</ModifiedDate>

      </value>

    </item>

  </Arguments>

  <InstanceId>eaf2cd93-cd29-46ce-908d-c9d7226b1806</InstanceId>

  <RecordNumber>9</RecordNumber>

  <EventTime>2009-10-21T20:41:49.0654669-04:00</EventTime>

  <Level>Info</Level>

  <Annotations>

    <item key="Threading Model" value="Asynchronous update" />

  </Annotations>

</ActivityStateRecord>

Developing a Nonpersisting Tracking Participant

In the previous example, you developed a custom tracking participant that serialized and persisted each tracking record to an XML file. However, it is important to note that persistence is not a requirement of a tracking participant. You might not require persistence of tracking data and instead want to consume it immediately within your application.

In this short example, you will develop a much simpler custom tracking participant that forwards the tracking records to the host application for processing.

Implementing the Tracking Participant

This custom tracking participant defines a public delegate that is used to pass the tracking records to the host application. The host application can assign code to the delegate in order to handle the tracking data.

Add a new C# class to the ActivityLibrary project, and name it EventTrackingParticipant. Here is the complete code for this class:

using System;
using System.Activities.Tracking;

namespace ActivityLibrary
{
    public class EventTrackingParticipant : TrackingParticipant
    {
        public Action<TrackingRecord> Received { get; set; }

        protected override void Track(TrackingRecord record, TimeSpan timeout)
        {
            if (Received != null)
            {
                Received.BeginInvoke(record, BeginInvokeCallback, Received);
            }
        }

        private void BeginInvokeCallback(IAsyncResult ar)
        {
            ((Action<TrackingRecord>)ar.AsyncState).EndInvoke(ar);
        }
    }
}

Testing the Tracking Participant

To test this new tracking participant, you can modify the Program.cs file of the UpdateInventoryTracking project as shown here:

namespace UpdateInventoryTracking
{
    using System;
    using System.Activities;
    using System.Activities.Tracking;
    using System.Collections.Generic;
    using ActivityLibrary;

    class Program
    {
        static void Main(string[] args)
        {
            EventTrackingParticipant tp =
                new EventTrackingParticipant();
            tp.Received = tr =>
                Console.WriteLine("{0:D2} {1:HH:mm:ss.ffffff} {2}",
                    tr.RecordNumber,
                    tr.EventTime,
                    tr.GetType().Name);

            UpdateInventory wf = new UpdateInventory();
            wf.ArgSalesOrderId = 43687;
            WorkflowInvoker instance = new WorkflowInvoker(wf);

            instance.Extensions.Add(tp);
            instance.Invoke();
        }
    }
}

Before executing the workflow, code is assigned to the Received delegate of the custom EventTrackingParticipant class. In this example, the assigned code simply writes a line to the console for each tracking record that it receives. However, you could easily use this data to update a progress indicator in the application, notify an external application of the workflow’s progress, and so on. A tracking profile is not provided for this example, so all possible tracking records should be processed.

When I run the UpdateInventoryTracking project, I see these results:


00 20:41:57.741140 WorkflowInstanceRecord

01 20:41:57.741140 ActivityScheduledRecord

02 20:41:57.741140 ActivityStateRecord

04 20:41:57.752175 ActivityStateRecord

05 20:41:57.752175 ActivityScheduledRecord

03 20:41:57.741140 ActivityScheduledRecord

06 20:41:57.755185 ActivityStateRecord

07 20:41:57.756188 WorkflowInstanceRecord

08 20:41:57.819390 CustomTrackingRecord

09 20:41:57.819390 ActivityStateRecord

10 20:41:57.820393 ActivityScheduledRecord

11 20:41:57.821396 ActivityStateRecord

12 20:41:57.821396 ActivityScheduledRecord

13 20:41:57.823402 ActivityStateRecord

14 20:41:57.824406 WorkflowInstanceRecord

Product 768: Reduced by 1

15 20:41:57.862527 ActivityStateRecord

16 20:41:57.863530 ActivityScheduledRecord

Product 765: Reduced by 2

17 20:41:57.863530 ActivityStateRecord

18 20:41:57.863530 WorkflowInstanceRecord

19 20:41:57.890617 ActivityStateRecord

20 20:41:57.895633 ActivityStateRecord

21 20:41:57.896636 ActivityScheduledRecord

22 20:41:57.897639 ActivityStateRecord

23 20:41:57.897639 ActivityScheduledRecord

Product 768: Added history for Qty of 1

24 20:41:57.899646 ActivityStateRecord

25 20:41:57.921716 ActivityStateRecord

26 20:41:57.921716 ActivityScheduledRecord

Product 765: Added history for Qty of 2

27 20:41:57.922719 ActivityStateRecord

28 20:41:57.937767 ActivityStateRecord

29 20:41:57.938770 ActivityStateRecord

30 20:41:57.938770 ActivityStateRecord

31 20:41:57.938770 ActivityStateRecord

32 20:41:57.938770 WorkflowInstanceRecord

images Note Remember that you can use multiple tracking participants at the same time. If you want to see this in action yourself, you can add the EtwTrackingParticipant or the FileTrackingParticipant to this example and run it again.

Using Workflow Tracking with a Declarative Service Application

As you might expect, workflow tracking can also be enabled for workflows that use WCF messaging. This includes declarative services that are hosted by IIS. The workflow tracking concepts are the same for declarative service applications. The one major difference is the way tracking is enabled and how profiles are defined. Unless a messaging workflow is self-hosted using the WorkflowServiceHost, you don’t have an opportunity to construct a tracking participant and profile in code. Instead, workflow tracking must be declared in the Web.config file.

In this next example, you will construct a declarative service that packages the UpdateInventory activity as a WCF-enabled workflow that can be hosted by IIS.

Declaring the InventoryService Workflow

Begin this example by adding a new WCF Workflow Service Application to the solution for this chapter. Name the new application UpdateInventoryService. You can delete the Service1.xamlx file since it won’t be used. Add these references to the project:

  • AdventureWorksAccess (project reference)
  • ActivityLibrary (project reference)

Add a new WCF Workflow Service to the project, and name it InventoryService. You can now open the InventoryService.xamlx file in the workflow designer, and follow these steps to complete the declaration of the workflow:

  1. Delete the root Sequence activity that was generated for you. I find it easier to start from scratch instead of modifying the template-generated service.
  2. Add a new ReceiveAndSendReply activity template to the empty workflow.
  3. Add an Int32 variable named SalesOrderId that is scoped by the Sequence activity.
  4. Set the properties for the Receive activity. Set the CanCreateInstance property to true, set the OperationName to Update, and change the ServiceContractName to IInventoryService. Define the content for the activity by adding a single Int32 parameter named salesOrderId with the value set to the SalesOrderId variable that you previously defined. Add System.Int32 to the collection of KnownTypes if it is not already included.
  5. Set the properties for the SendReplyToReceive activity. Define the content for the activity by adding a single Boolean parameter named result. Return a value of True for this parameter.
  6. Drag an instance of the UpdateInventory activity from the Toolbox to the location between the Receive and Send activities. This activity is the complete workflow that you have been using throughout this chapter. However, in this case, you are executing it as an activity within this declarative service workflow. Set the ArgSalesOrderId property to the SalesOrderId variable.

Figure 14-8 is the completed InventoryService workflow.

images

Figure 14-8. InventoryService.xamlx

Configuring Tracking in the Web.config

For an application like this that is not self-hosted, the tracking participant and profile must be configured in the Web.config file. For this example, you will define a tracking profile that is similar to those that you have previously defined in this chapter. The UpdateInventoryService project should already have a Web.config file. You simply need to update it with the entries shown here:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
  </system.web>

The tracking participant is defined as a service behavior. This is also where the tracking profile (defined next in the file) is referenced.

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <etwTracking profileName="MyTrackingProfile"/>
          <serviceDebug includeExceptionDetailInFaults="False" />
          <serviceMetadata httpGetEnabled="True"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

The definition of the tracking profile follows a format that is logically the same as the profiles that you defined in code. The profile consists of one or more queries that each support their own particular set of properties.

    <tracking>
      <profiles>
        <trackingProfile name="MyTrackingProfile">
          <workflow activityDefinitionId="*">
            <workflowInstanceQueries>
              <workflowInstanceQuery>
                <states>
                  <state name="Started"/>
                  <state name="Completed"/>
                </states>
              </workflowInstanceQuery>
            </workflowInstanceQueries>
            <activityStateQueries>
              <activityStateQuery activityName="UpdateInventory">
                <states>
                  <state name="Executing"/>
                </states>
                <arguments>
                  <argument name="*"/>
                </arguments>
              </activityStateQuery>
              <activityStateQuery activityName="UpdateProductInventory">
                <states>
                  <state name="Executing"/>
                  <state name="Closed"/>
                </states>
                <arguments>
                  <argument name="SalesDetail"/>
                </arguments>
                <annotations>
                  <annotation name="Threading Model" value="Asynchronous update"/>
                </annotations>
              </activityStateQuery>
            </activityStateQueries>
            <activityScheduledQueries>
              <activityScheduledQuery childActivityName="UpdateProductInventory"/>
            </activityScheduledQueries>
            <customTrackingQueries>
              <customTrackingQuery activityName="*" name="*"/>
            </customTrackingQueries>
            <faultPropagationQueries>
              <faultPropagationQuery faultSourceActivityName ="*"
                faultHandlerActivityName="*"/>
            </faultPropagationQueries>
          </workflow>
        </trackingProfile>
      </profiles>
    </tracking>
  </system.serviceModel>
</configuration>

Testing the Workflow Service

You can follow these steps to test workflow tracking for the workflow service:

  1. Since this example is using the EtrTrackingParticipant, make sure that the Analytic log has been enabled.
  2. Run the UpdateInventoryService project without debugging (Ctrl-F5). This starts the ASP.NET development server. You should see an icon for the development server in the system tray.
  3. Running the project should also start your default browser, opened to the project directory.
  4. Right-click the InventoryService.xamlx link in the browser, and copy the link location.
  5. Start the WcfTestClient utility (distributed with Visual Studio and found under the Common7IDE folder). Select the Add Service option from the File menu, and paste the link location that you copied from the browser. At this point, the WcfTestClient retrieves the metadata that defines the workflow service.
  6. Once the metadata for the service has been retrieved, you should see the Update operation on the left side of the client. Double-click the Update operation, and enter 43687 as the salesOrderId parameter. Click the Invoke button to execute the workflow service.

Figure 14-9 shows the WcfTestClient after the workflow service has been invoked.

images

Figure 14-9. Invoking InventoryService.xamlx with WcfTestClient

You should now be able to view the ETW workflow tracking records using the Windows Event Viewer as you did earlier in the chapter. The results should be similar to the previous examples. However, the log will also include additional WCF-related entries in addition to the workflow tracking records.

Loading Tracking Profiles from App.config

The ability to configure tracking profiles in the Web.config file is convenient since it avoids the need to modify and rebuild profiles that are defined in code. However, although this works for declarative service workflows, WF does not provide an out-of-the-box way to load tracking profiles from an App.config file for non-WCF workflows. But with a small amount of code, you can load tracking profiles from the App.config file.

In the short example that follows, you will develop a class that loads a named tracking profile from an App.config file. You will then define a tracking profile in an App.config file and use this class to load it.

Implementing a Tracking Profile Loader

Add a new C# class to the ActivityLibrary project, and name it TrackingProfileLoader. This class uses the ConfigurationManager class, which requires that you add a reference to the System.Configuration assembly to the project.

Here is the complete implementation of this class:

using System;
using System.Activities.Tracking;
using System.Configuration;
using System.Linq;
using System.ServiceModel.Activities.Tracking.Configuration;

namespace ActivityLibrary
{
    public class TrackingProfileLoader
    {
        public TrackingProfile Profile { get; set; }

        public TrackingProfileLoader(String profileName)
        {
            LoadConfig(profileName);
        }

After retrieving the tracking section from the App.config file, the named tracking profile is located and made available from a public Profile property.

        private void LoadConfig(String profileName)
        {
            TrackingSection ts =
                (TrackingSection)ConfigurationManager.GetSection(
                    "system.serviceModel/tracking");
            if (ts != null && ts.TrackingProfiles != null)
            {
                TrackingProfile profile =
                    (from tp in ts.TrackingProfiles
                     where tp.Name == profileName
                     select tp).SingleOrDefault();
                if (profile != null)
                {
                    Profile = profile;
                }
            }

            if (Profile == null)
            {
                throw new ArgumentException(String.Format(
                    "Tracking Profile {0} not found in app.config",
                    profileName));
            }
        }
    }
}

Defining the Tracking Profile in the App.config file

If the UpdateInventoryTracking application doesn’t already have an App.config file, add one now. Modify the App.config file so that it has all of the entries shown here:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
  </startup>
  <system.serviceModel>
    <tracking>
      <profiles>
        <trackingProfile name="MyTrackingProfile">
          <workflow activityDefinitionId="*">
            <workflowInstanceQueries>
              <workflowInstanceQuery>
                <states>
                  <state name="Started"/>
                  <state name="Completed"/>
                </states>
              </workflowInstanceQuery>
            </workflowInstanceQueries>
            <activityStateQueries>
              <activityStateQuery activityName="UpdateProductInventory">
                <states>
                  <state name="Executing"/>
                </states>
                <arguments>
                  <argument name="SalesDetail"/>
                </arguments>
                <annotations>
                  <annotation name="Threading Model" value="Asynchronous update"/>
                </annotations>
              </activityStateQuery>
            </activityStateQueries>
            <customTrackingQueries>
              <customTrackingQuery activityName="*" name="*"/>
            </customTrackingQueries>
          </workflow>
        </trackingProfile>
      </profiles>
    </tracking>
  </system.serviceModel>
</configuration>

Testing the Tracking Profile Loader

You can now modify the Program.cs file in the UpdateInventoryTracking project to use the new TrackingProfileLoader class as shown here:

namespace UpdateInventoryTracking
{
    using System;
    using System.Activities;
    using System.Activities.Tracking;
    using System.Collections.Generic;
    using ActivityLibrary;

    class Program
    {
        static void Main(string[] args)
        {
            TrackingProfileLoader config =
                new TrackingProfileLoader("MyTrackingProfile");

            FileTrackingParticipant tp = new FileTrackingParticipant();
            tp.TrackingProfile = config.Profile;

            UpdateInventory wf = new UpdateInventory();
            wf.ArgSalesOrderId = 43687;
            WorkflowInvoker instance = new WorkflowInvoker(wf);

            instance.Extensions.Add(tp);
            instance.Invoke();

            tp.Stop();
        }
    }
}

In this example, the code uses the FileTrackingParticipant that was developed earlier in the chapter. However, you could just as easily use the EtwTrackingParticipant if you prefer. When you run the UpdateInventoryTracking project, you should see several new records created in the indebug racking folder for the project.

Summary

This chapter focused on workflow tracking. Workflow tracking is the built-in mechanism that allows you to automatically instrument your workflows. This chapter demonstrated how to use the EtwTrackingParticipant that is included with WF. This tracking participant enables viewing and management of the tracking records using the Windows Event Viewer.

A number of examples explored the use of tracking profiles to filter the type and amount of tracking data that is passed to a tracking participant. Included in these examples was a demonstration of how to create custom tracking records from within a custom activity.

Two different custom tracking participants were implemented in examples in this chapter. The first one persisted tracking records to XML files, and the second was used to pass tracking records directly to the host application.

The use of workflow tracking with declarative workflow services was demonstrated in another example. The chapter concluded with a custom class that allows you to read a tracking profile from an App.config file for non-WCF applications.

In the next chapter, you will learn how to enhance the design experience by developing your own custom activity designers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.175.253