Tuesday, June 24, 2008

Every day comes with some new wishes and challenges. Today my team struck up with handling in a state machine workflow. They digged the web for finding a solution, unluck.
Problem: How to rethrow an exception from a state machine workflow to the workflow runtime host?
Description: I have designed one of our product as the service interface layer (WCF) invokes some of the business functionalities through workflow. We can say "workflow enabled business functionalities". This is a state machine workflow (I don't find real enterprise level usage of sequential workflow, of course you can use it at UI level). We have instrumented (exception handling and logging) well the non-workflow enabled services and business functionalities. Whenever a request comes from the clients of these services (currently, we have only one ASP.NET client) to perform a business operation, each layer do their responsibilities well. In case of any failure, they respond to the client gently with all necessary details. However, we were not able to handle exception in our "workflow enabled business functionalities". Laughing...! Initially, I was also felt that we have missed something. After a while, my team found that this is the issue with WF itself. Actually, you can handle exception within the workflow runtime, but how can you respond to your client there is a fault while executing their requested business functionality.

By its asynchronous nature of state machine workflow, even though WF provides rethrow activity, it cannot propagate upto the calling place which is the host of the workflow instance. This is common for any type of host such as Console, ASP.NET, etc.

How to reproduce:

1. Create a state machine workflow (for an example, RoseWorkflow) and add an event driven activity (onPluckFlowersEvent) on the initial state (Bud State).
2. In the onPluckFlowersEvent handling code, forcefully throw an exception (throw new Exception("You cannot pluck flowers in this state"));. The value of the properties of this external event handler activity are:
Name: handlePluckFlowersEvent
InterfaceType: RoseIsRoseWorkflow.IRoseWorkflowService
EventName: PluckFlowers
e: Activity=RoseWorkflow, Path=EventArgs
sender: Activity=RoseWorkflow, Path=Sender
For both sender and e, I've declared two properties named "EventArgs" of type ExternalDataEventArgs and "Sender" of type object.


private void handlePluckFlowersEvent_Invoked(object sender, ExternalDataEventArgs e)
{
// here _delegatedException is the member of RoseWorkflow
_delegatedException = new Exception("In this stage, you cannot pluck flowers");
throw _delegatedException;
}

3. Add fault handler activity (onPluckFlowersEventFaultHandler) for the event and set its "FaultType" property as "System.Exception".
4. If need, add normal code activity to handle the exception, like

private void onPluckFlowersEventFaultCodeHandler_ExecuteCode(object sender, EventArgs e)
{
FaultHandlerActivity faultHandler = ((Activity)sender).Parent as FaultHandlerActivity;
Console.WriteLine("Handle exception in workflow itself. Details: {0}", faultHandler.Fault.Message);
}

5. Add Throw activity, and set the following properties:
Fault: Activity=onPluckFlowersEventFaultHandler, Path=Fault
FaultType: Activity=onPluckFlowersEventFaultHandler, Path=FaultType
6. Host it into a console:


class Program
{
static void Main(string[] args)
{
using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
AutoResetEvent waitHandle = new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();};
workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine(e.Exception.Message);
waitHandle.Set();
};
ExternalDataExchangeService dataExchange = new ExternalDataExchangeService();
workflowRuntime.AddService(dataExchange);
RoseWorkflowService roseWorkflowService = new RoseWorkflowService();
dataExchange.AddService(roseWorkflowService);
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(RoseWorkflow));
instance.Start();
roseWorkflowService.PluckFlowers += new EventHandler(roseWorkflowService_PluckFlowers);
try
{
roseWorkflowService.RaisePluckFlowersEvent(instance.InstanceId);}catch(Exception e){Console.WriteLine("from host. {0},{1}", e.GetType().Name, e.Message);
}
waitHandle.WaitOne();
}
Console.WriteLine("Press to quit");
Console.ReadLine();
}
static void roseWorkflowService_PluckFlowers(object sender, ExternalDataEventArgs e)
{
Console.WriteLine("Handling PluckFlowers event");
}
}

I used RoseWorkflowService as one of my local service which implement my IRoseWorkflowService which is used in my workflow.
IRoseWorkflowService:


[ExternalDataExchange]
public interface IRoseWorkflowService
{
event EventHandler PluckFlowers;
void RaisePluckFlowersEvent(Guid instanceId);
}

RoseWorkflowService:


[Serializable]
public class RoseWorkflowService : IRoseWorkflowService
{
#region IRoseWorkflowService Members
public event EventHandler PluckFlowers;

public void RaisePluckFlowersEvent(Guid instanceId)
{
if (PluckFlowers != null)
PluckFlowers(null, new ExternalDataEventArgs(instanceId));
}
#endregion
}

Now run the application. It never reaches to host place.
Solution (Workaround): You can resolve it through by "CallExternalMethod" activity. I will explain about this on next part with downloadable source code.

0 comments: