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
8: Activity [1.1] "Sequence" scheduled child activity [1.8] "WaitForBookmark "B1""
9: Activity [1.8] "WaitForBookmark "B1"" is Executing
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.
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:
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 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
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: }
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.
To implement this pattern, in the sample I have a class named Global which contains these shared global properties.
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);
13: /// <summary>
14: /// The default timeout used
15: /// </summary>
16: private static TimeSpan defaultTimeout = TimeSpan.FromSeconds(1);
17:
18: #endregion
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
37: defaultDebugTimeout = value;
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 bookmarkhost.RunEpisode(Bookmark1, Global.Timeout);// Whenever you use WaitOne you MUST use a timeoutif (!idleEvent.WaitOne(Global.Timeout)){ throw new TimeoutException();}
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.