Ron Jacobs

Windows Workflow Foundation

Three Simple Rules for Better Debugging with WorkflowApplication

Three Simple Rules for Better Debugging with WorkflowApplication

Rate This
  • Comments 3
Developers often say that there is a steep learning curve with Windows Workflow Foundation (WF4).  I won’t deny that… instead, allow me to share three simple rules that will help you to get over that curve sooner and to make them easy to remember, they all begin with “U” which is appropriate because they do all begin with you.
  1. Use Tracking
  2. Unit Tests
  3. Use Timeouts
Download Sample Code
Workflow TV - Three Simple Rules for Better Debugging with WorkflowApplication

Use Tracking

Tracking is the single best way to understand what your workflow is doing.  There is just one simple problem… We didn’t make it easy for people to read tracking information.

WorkflowInstanceRecord { InstanceId = e9579393-c150-46c9-be60-42bdf4dc509e, RecordNumber = 0, EventTime = 7/5/2012 12:04:11 AM, ActivityDefinitionId = Workflow, State = Started }
ActivityScheduledRecord { InstanceId = e9579393-c150-46c9-be60-42bdf4dc509e, RecordNumber = 1, EventTime = 7/5/2012 12:04:11 AM, Activity {  }, ChildActivity { Name=Workflow, ActivityId = 1, ActivityInstanceId = 1, TypeName=ThreeRulesBetterDebugWF4.Workflow } }
ActivityStateRecord { InstanceId = e9579393-c150-46c9-be60-42bdf4dc509e, RecordNumber = 2, EventTime = 7/5/2012 12:04:11 AM, Activity { Name=Workflow, ActivityId = 1, ActivityInstanceId = 1, TypeName=ThreeRulesBetterDebugWF4.Workflow }, State = Executing }
 
 

 

As you can see in the previous example, this is a mind numbing dump of data. If you want something much more useful try Microsoft.Activities.Extensions.Tracking which includes tracking record extensions designed to make it easy for you to follow what is going on.

0: WorkflowInstance "Workflow" is Started
1: Activity [null] "null" scheduled child activity [1] "Workflow"
2: Activity [1] "Workflow" is Executing
3: Activity [1] "Workflow" scheduled child activity [1.1] "Sequence"
4: Activity [1.1] "Sequence" is Executing
5: Activity [1.1] "Sequence" scheduled child activity [1.10] "WriteLine"
6: Activity [1.10] "WriteLine" is Executing
{
    Arguments
        Text: Creating bookmark B1
        TextWriter: 
}
7: Activity [1.10] "WriteLine" is Closed
{
    Arguments
        Text: Creating bookmark B1
        TextWriter: 
}
8: Activity [1.1] "Sequence" scheduled child activity [1.8] "WaitForBookmark "B1""
9: Activity [1.8] "WaitForBookmark "B1"" is Executing
{
    Arguments
        BookmarkName: B1
}
10: WorkflowInstance "Workflow" is Idle

Now that is a trace that helps you to understand exactly what is going on, including things that you don't see in the simple record.ToString() trace above such as arguments, annotations, variables and more.

Tracking to the Trace Provider

When debugging, just add a TraceTrackingParticipant to get the tracking output written to Visual Studio Debug Output window.  Or if you are using a logging library such as NLog or Enterprise Library.

   1: using Microsoft.Activities.Extensions.Tracking;
   2:  
   3: private static void RunWorkflow()
   4: {
   5:     var host = new WorkflowApplication(WorkflowDefinition);
   6:  
   7:     // Tip: Output tracking to System.Diagnostics.Trace
   8:     host.Extensions.Add(new TraceTrackingParticipant());
   9:  
  10:     // ...
  11: }
  12:  

 

Tracking to a File

If you just want a text file with the output

   1: private static void RunWorkflow()
   2: {
   3:     var host = new WorkflowApplication(WorkflowDefinition);
   4:  
   5:     // Tip: Capture tracking to a file for help debugging
   6:     using (var fileTracker = new FileTracker("tracking.txt"))
   7:     {
   8:         host.Extensions.Add(fileTracker);
   9:         host.Run();
  10:         // Wait for it to complete and then
  11:  
  12:         // FileTracker is Disposable 
  13:     }
  14: }
  15:  

 

Unit Tests

Unit testing is an art, but it is one you can learn if you don’t give up.  I am such a believer in it I created Microsoft.Activities.UnitTesting to help you.

Given this workflow

image

What do we need to test? 

There is a protocol of bookmarks that must be followed for this workflow to function correctly.  Our tests should verify that the protocol is followed from the workflow side and from the host side.  Using the Given / When / Then pattern helps me to be clear about what I’m testing and helps me to focus the test on just one aspect of the behavior.

   1: /// <summary>
   2: ///   Given
   3: ///   * A Workflow run until the second idle with bookmark "B2" 
   4: ///   When
   5: ///   * Workflow resumes bookmark "B2"
   6: ///   Then
   7: ///   * It will run and complete
   8: /// </summary>
   9: [TestMethod]
  10: public void WorkflowResumedB2WillComplete()
  11: {
  12:     // Arrange
  13:     var activity = new Workflow();
  14:     var host = WorkflowApplicationTest.Create(activity);
  15:     try
  16:     {
  17:         // Run to the first bookmark
  18:         host.TestWorkflowApplication.RunEpisode(Program.Bookmark1, Global.Timeout);
  19:  
  20:         // Run to the second bookmark
  21:         host.TestWorkflowApplication.ResumeEpisodeBookmark(Program.Bookmark1, 1, Program.Bookmark2, Global.Timeout);
  22:  
  23:         // Act
  24:         // Use Microsoft.Activities.Extensions episode support
  25:         // Resume bookmark "B2" and run until complete
  26:         // Tip: Use the timeout for better debugging
  27:         var result = host.TestWorkflowApplication.ResumeEpisodeBookmark(Program.Bookmark2, 2, Global.Timeout);
  28:  
  29:         // Assert
  30:         Assert.IsNotNull(result);
  31:         Assert.IsInstanceOfType(result, typeof(WorkflowCompletedEpisodeResult));
  32:         var completedResult = (WorkflowCompletedEpisodeResult)result;
  33:         Assert.AreEqual(ActivityInstanceState.Closed, completedResult.State);
  34:     }
  35:     finally
  36:     {
  37:         host.Tracking.Trace();
  38:     }
  39: }

Use Timeouts

When everything goes right, you don’t need timeouts.  So you create programs that will one day hang because on that particular day something didn’t go right.  I’m as guilty as anyone when it comes to this.  Recently I’ve been reviewing the code I’ve written for Microsoft.Activities.Extensions and Microsoft.Activities.UnitTesting and I came up with these rules.

    • Any class that has one thread waiting on an asynchronous operation from another thread you must use a timeout and handle timeouts gracefully.
    • Classes should have a Default Timeout that will be used if one is not supplied by the caller
    • Timeouts should be consistent across the library
    • Timeouts should self-adjust when the debugger is attached

To implement this pattern, in the sample I have a class named Global which contains these shared global properties. 

   1: /// <summary>
   2: ///   Global readonly static values
   3: /// </summary>
   4: internal static class Global
   5: {
   6:     #region Static Fields
   7:  
   8:     /// <summary>
   9:     ///   The default timeout used when a debugger is attached
  10:     /// </summary>
  11:     private static TimeSpan defaultDebugTimeout = TimeSpan.FromSeconds(10);
  12:  
  13:     /// <summary>
  14:     ///   The default timeout used
  15:     /// </summary>
  16:     private static TimeSpan defaultTimeout = TimeSpan.FromSeconds(1);
  17:  
  18:     #endregion
  19:  
  20:     #region Properties
  21:  
  22:     /// <summary>
  23:     ///  Gets or sets the default timeout used when a debugger is attached
  24:     /// </summary>
  25:     /// <remarks>
  26:     /// Allows users of this library to set the default
  27:     /// </remarks>
  28:     internal static TimeSpan DefaultDebugTimeout
  29:     {
  30:         get
  31:         {
  32:             return defaultDebugTimeout;
  33:         }
  34:  
  35:         set
  36:         {
  37:             defaultDebugTimeout = value;
  38:         }
  39:     }
  40:  
  41:     /// <summary>
  42:     ///  Gets or sets the default timeout
  43:     /// </summary>
  44:     /// <remarks>
  45:     /// Allows users of this library to set the default
  46:     /// </remarks>
  47:     internal static TimeSpan DefaultTimeout
  48:     {
  49:         get
  50:         {
  51:             return defaultTimeout;
  52:         }
  53:  
  54:         set
  55:         {
  56:             defaultTimeout = value;
  57:         }
  58:     }
  59:  
  60:     /// <summary>
  61:     ///   Gets Timeout used for wait operations
  62:     /// </summary>
  63:     /// <remarks>
  64:     ///   TODO: Notice how the Timeout adjust when the debugger is attached
  65:     /// </remarks>
  66:     internal static TimeSpan Timeout
  67:     {
  68:         get
  69:         {
  70:             return Debugger.IsAttached ? defaultDebugTimeout : defaultTimeout;
  71:         }
  72:     }
  73:  
  74:     #endregion
  75: }

How many times have you decided to debug your program only to get TimeoutException because you were slowly stepping through the code?  With this approach when I do an operation that needs a timeout, I get a consistent timeout that automatically adjusts when a debugger is attached.

// Use Microsoft.Activities.Extensions to run until idle with a bookmark
host.RunEpisode(Bookmark1, Global.Timeout);

// Whenever you use WaitOne you MUST use a timeout
if (!idleEvent.WaitOne(Global.Timeout))
{
throw new TimeoutException();
}

Summary

Any time you create multi-threaded programs you have crossed over into advanced territory.  Whenever you use WorkflowApplication you are writing a multi-threaded program and you cannot escape the need for these three simple rules.

Any other rules that you have?  Just leave a comment below.

Ron Jacobs
blog: http://blogs.msdn.com/rjacobs
Twitter: @ronljacobs

  • Use trace in GetMetaData methodes...

    it allow to know what is really shared with context

  • Hi Ron - great article.

    Does the UnitTesting framework support .NET 4.5?  Last I tried it it didn't (it chokes on C# expressions).

  • It should work on 4.5 - if there is something that is failing please open an issue at http://wf.codeplex.com so I can fix it.

Page 1 of 1 (3 items)