Udostępnij za pośrednictwem


What if you don’t want your workflows to just handle the exception and move on?

Workflows are very cool! You can just drag some activities onto your IDE, rearrange them as needed and in no time you have a fully functional sequence diagram workflow or a state machine workflow. Starting with .NET Framework 3.5, you can also implement your WCF operations through activities in workflows, and that is just as great as WCF itself.

However, there’s this little downside when your favorite development environment starts getting to such a higher stage and getting away from the basic building blocks available to most of the development languages. I’m talking about failing activities (for instance, the temporary unavailability of a SQL Server instance and the exception that would raise) and how the Windows Workflow Foundation deals with those:

  • Either you don’t handle it in your code and the workflow runtime will terminate your workflow instance;
  • Or you handle your exception, leaving your workflow ready to execute the next scheduled activity.

The main problem is that your workflow may be a complex one, full of mission-critical activities such as those implementing WCF operations, it may have been running for weeks, and having it move on as a response to an unpredictable external event might not be the desired outcome at that stage. You’d probably want it getting back to that same activity, as it needs to succeed for the sake of your workflow semantics integrity.

Of course, you can always use a WhileActivity for that matter and have some sort of condition for loop termination, but again, if your workflow is a complex one, you will find yourself overburdening it with loops all over the designer and thinking that was not exactly what you were expecting to do when you were considering the Workflow Foundation option.

Well, there’s also the possibility of rebranding your workflow as a state diagram. As a state diagram, you can SetState back into the same state upon failure and the problem is solved. You’ll be in trouble, though, if you need to incorporate those parallel activities which branch early in the workflow and converge back as a requirement for workflow closure. You would need to have multiple workflows, which would then use InvokeWorkflow and then you’d have to wire up the correlation when they finish.

The last option is the one I’m bringing with this post, which heavily relies on the wonderful extensibility principle which guides Microsoft throughout its history and also on reflection, as it allowed me to understand the mechanics behind each distinct kind of activity. Essentially, I’ve developed a custom activity which derives from CompositeActivity (here's a great article about how to create a custom CompositeActivity), resembles the SequenceActivity and knows how to implement looping as the WhileActivity. How cool is that? Well, in theory that is a cool thing, but wait until you see the code and understand how much I had to struggle against ActivityExecutionStatus and ActivityExecutionResult , as the failing activity will never get back to a “healthy” status and the code needs to live with that…

Also, I've included a DependencyProperty which you may wire up to a property and have it set on soft errors (those business errors which do not necessarily throw an exception), and under those circumstances, the activity will also jump its execution back to the first child.

Here it goes, use it and reuse it if you like it (it even has a designer of its own, so that you can distinguish this activity from other activities):

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing.Drawing2D;

namespace RestorableActivity
{
[Designer(typeof(RestorableDesigner), typeof(IDesigner))]
public partial class RestorableActivity
: CompositeActivity, IActivityEventListener<ActivityExecutionStatusChangedEventArgs>
{
public RestorableActivity()
{
InitializeComponent();
}

private bool finished;
private Activity current;

public static DependencyProperty MustRepeatProperty =
DependencyProperty.Register
("MustRepeat", typeof(bool),
typeof(RestorableActivity));

[DescriptionAttribute("Indicates whether the current activity needs to be re-executed")]
[CategoryAttribute("Activity")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public bool MustRepeat
{
get
{
return ((bool)(base.GetValue(RestorableActivity.MustRepeatProperty)));
}
set
{
base.SetValue(RestorableActivity.MustRepeatProperty, value);
}
}

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
if (executionContext == null)
throw new ArgumentNullException("executionContext");
if (base.EnabledActivities.Count == 0)
return ActivityExecutionStatus.Closed;
base.EnabledActivities[0].RegisterForStatusChange(Activity.ClosedEvent, this);
executionContext.ExecuteActivity(base.EnabledActivities[0]);
current = base.EnabledActivities[0];
return ActivityExecutionStatus.Executing;
}

protected override ActivityExecutionStatus HandleFault
(ActivityExecutionContext executionContext, Exception exception)
{
ActivityExecutionStatus status = base.HandleFault(executionContext, exception);
if (status == ActivityExecutionStatus.Closed)
{
foreach (Activity ac in base.EnabledActivities)
{
ActivityExecutionContext aec;
aec = executionContext.ExecutionContextManager.GetExecutionContext(ac);
if (aec != null)
switch (aec.Activity.ExecutionStatus)
{
case ActivityExecutionStatus.Closed:
executionContext.ExecutionContextManager.CompleteExecutionContext(aec);
break;
case ActivityExecutionStatus.Faulting:
aec.CloseActivity();
executionContext.ExecutionContextManager.CompleteExecutionContext(aec);
break;
default:
aec.CloseActivity();
break;
}
}

if (!finished)
{
ActivityExecutionContext context2 =
executionContext.ExecutionContextManager.CreateExecutionContext
(EnabledActivities[0]);
context2.Activity.RegisterForStatusChange(Activity.ClosedEvent, this);
context2.ExecuteActivity(context2.Activity);
current = context2.Activity;
}
}

CleanUpExecutionContext(executionContext);
if (executionContext.ExecutionContextManager.ExecutionContexts.Count > 0)
return ActivityExecutionStatus.Faulting;
executionContext.CloseActivity();
return ActivityExecutionStatus.Closed;
}

private bool TryScheduleNextChild(ActivityExecutionContext executionContext)
{
if (executionContext == null)
throw new ArgumentNullException("executionContext");
IList<Activity> enabledActivities = base.EnabledActivities;

if (enabledActivities.Count == 0)
return false;
int num = 0;
for (int i = enabledActivities.Count - 1; i >= 0; i--)
if (enabledActivities[i].Name == current.Name)
{
if (i == (enabledActivities.Count - 1))
return false;
num = i + 1;
break;
}

CleanUpExecutionContext(executionContext);
ActivityExecutionContext context2 =
executionContext.ExecutionContextManager.CreateExecutionContext(
enabledActivities[num]);
context2.Activity.RegisterForStatusChange(Activity.ClosedEvent, this);
context2.ExecuteActivity(context2.Activity);
current = context2.Activity;
return true;
}

void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent
(object sender, ActivityExecutionStatusChangedEventArgs e)
{
if (sender == null)
throw new ArgumentNullException("sender");
if (e == null)
throw new ArgumentNullException("e");
ActivityExecutionContext executionContext = sender as ActivityExecutionContext;
if (executionContext == null)
throw new ArgumentException("Error", "sender");

e.Activity.UnregisterForStatusChange(Activity.ClosedEvent, this);

RestorableActivity activity = executionContext.Activity as RestorableActivity;
if (activity == null)
throw new ArgumentException("sender");

if (MustRepeat)
{
MustRepeat = false;
CleanUpExecutionContext(executionContext);
ActivityExecutionContext context2 =
executionContext.ExecutionContextManager.CreateExecutionContext
(EnabledActivities[0]);
context2.Activity.RegisterForStatusChange(Activity.ClosedEvent, this);
context2.ExecuteActivity(context2.Activity);
current = context2.Activity;
}

else if ((activity.ExecutionStatus == ActivityExecutionStatus.Canceling) ||
((activity.ExecutionStatus == ActivityExecutionStatus.Executing) &&
!this.TryScheduleNextChild(executionContext)))
{
CleanUpExecutionContext(executionContext);
executionContext.CloseActivity();
}
else if (activity.ExecutionStatus == ActivityExecutionStatus.Faulting)
{
Activity lastAc = EnabledActivities.Last<Activity>();
ActivityExecutionContext aec =
executionContext.ExecutionContextManager.GetExecutionContext(e.Activity);

if (aec != null)
finished = ((e.Activity.Name.Equals(lastAc.Name)) &&
(e.ExecutionResult == ActivityExecutionResult.Succeeded) &&
(e.ExecutionStatus == ActivityExecutionStatus.Closed) &&
(aec.Activity.ExecutionStatus == ActivityExecutionStatus.Closed) &&
(aec.Activity.ExecutionResult != ActivityExecutionResult.Faulted));
else
finished = ((e.Activity.Name.Equals(lastAc.Name)) &&
(e.ExecutionResult == ActivityExecutionResult.Succeeded) &&
(e.ExecutionStatus == ActivityExecutionStatus.Closed));

if (finished)
{
CleanUpExecutionContext(executionContext);
executionContext.CloseActivity();
}
else
if (e.ExecutionResult != ActivityExecutionResult.Faulted)
TryScheduleNextChild(executionContext);

}
}

private void CleanUpExecutionContext(ActivityExecutionContext executionContext)
{
ActivityExecutionContextManager aecm = executionContext.ExecutionContextManager;

if (aecm.ExecutionContexts.Count > 0)
{
for (int i = aecm.ExecutionContexts.Count - 1; i >= 0; i--)
{
aecm.ExecutionContexts[i].Activity.UnregisterForStatusChange
(Activity.ClosedEvent, this);

switch (aecm.ExecutionContexts[i].Activity.ExecutionStatus)
{
case ActivityExecutionStatus.Closed:
aecm.CompleteExecutionContext(aecm.ExecutionContexts[i]);
break;
case ActivityExecutionStatus.Executing:
aecm.ExecutionContexts[i].CloseActivity();
aecm.CompleteExecutionContext(aecm.ExecutionContexts[i]);
break;
default:
aecm.ExecutionContexts[i].CloseActivity();
break;
}
}
}
}
}

public class RestorableDesigner : SequenceDesigner
{
protected override void Initialize(Activity activity)
{
base.Initialize(activity);
}

protected override void OnPaint(ActivityDesignerPaintEventArgs e)
{
base.OnPaint(e);
Rectangle frameRect = new Rectangle
(this.Location.X, this.Location.Y, this.Size.Width - 5, this.Size.Height - 5);
e.Graphics.DrawPath(new Pen(Color.Red, 2), RoundedRect(frameRect));
}

private GraphicsPath RoundedRect(Rectangle frame)
{
GraphicsPath path = new GraphicsPath();
int radius = 7;
int diameter = radius * 2;
Rectangle arc = new Rectangle(frame.Left, frame.Top, diameter, diameter);
path.AddArc(arc, 180, 90);
arc.X = frame.Right - diameter;
path.AddArc(arc, 270, 90);
arc.Y = frame.Bottom - diameter;
path.AddArc(arc, 0, 90);
arc.X = frame.Left;
path.AddArc(arc, 90, 90);
path.CloseFigure();
return path;
}
}
}

Some quick notes about this code:

  • I’m sure it’s not bullet-proof and it certainly hasn’t been tested against every distinct pattern of usage;
  • Ensure you configure a fault handler for each RestorableActivity instance in your workflows. As I told you above, it seems not to be possible to sanitize an activity once it is faulting, so you’ll get a fault on activity closure.
  • If you find it possible to handle every exception and consider the soft error option, then by all means, go ahead with that. Again, the activity execution will keep healthy.
  • And finally, this activity does not inherit from IEventActivity , which means that it cannot be the first child of activities such as the ListenActivity or the EventHandlingScopeActivity. If your critical activity is a ReceiveActivity and it is the first child of one of these activities branches, this custom activity is not for you.

 In summary, if you can cope with a few constraints, this is really good stuff, don't you agree?

 By the way, here’s how it looks like in the Visual Studio IDE:

RestorableActivity in Action

  Good luck with your workflows development and till next time,

    Manuel Oliveira