Workflows do not live in isolation. The typical workflow will need to receive data from the outside world, and let those of us in the outside world know when we need to make a decision, like approving an expense report from our team member's trip to Las Vegas. Windows Workflow (WF) offers a variety of mechanisms for communication with the outside world. The WebServiceInput activity and InvokeWebService activity, for example, are both components in the base activity library of WF. We can use these two activities to communicate with remote services using WSDL based contracts.
For local, in-process communications we have the CallExternalMethod activity and HandleExternalEvent activity. The CallExternalMethod activity allows a workflow to invoke a method on a local service registered with the host. The HandleExternalEvent activity lets the workflow to listen for events raised by the its host. In this article, we are going to focus on the HandleExternalEvent activity.
Not Your Typical Event
I have to let the reader in on a secret. In learning Windows Workflow with Beta 2.2, I've managed to foul up events in every possible way. Well, perhaps not every possible way, I'm sure I'll find a few more techniques as time goes on. During the course of this article I'm going to let you know about the problems I ran into, and how to troubleshoot and solve those problems.
First, it helps to have a firm grasp of how events reach a workflow. Events are one mechanism, a host we can use to tell the workflow when something interesting happens, like when an expense report is approved (or rejected), or when a check has finally arrived in the mail.
Unlike the events we use in Windows Forms and ASP.NET, where events travel directly from the publisher to the subscriber, events in workflow can take a longer path. A workflow instance lives inside the motherly embrace of the workflow runtime, and we have to follow a protocol before the runtime will let the workflow instance come out and play. This is partly because the workflow might have been waiting a long time for the event to arrive, and the WF runtime might have serialized the workflow into a database for long-term storage - this feature is known as passivation. We can't hold a workflow instance in memory for three months waiting for an account to close.
The first part of the protocol is defining a contract to describe the events and types involved.
[ExternalDataExchange]
public interface IPaymentProcessingService
{
event EventHandlerPaymentProcessed;
}
[Serializable]
public class PaymentProcessedEventArgs : ExternalDataEventArgs
{
public PaymentProcessedEventArgs(Guid instanceId, double amount)
: base(instanceId)
{
_amount = amount;
}
private double _amount;
public double Amount
{ get { return _amount; }
set { _amount = value; }
}
}
In the code above we've defined an interface containing an event we want to raise, and the event arguments. The following features are important to note:
- We've decorated the interface with the ExternalDataExchangeAttribute
- The event args class derives from ExternalDataEventArgs
- The event args class is serializable.
When we register a communication service with WF, the runtime will look for interfaces with the ExternalDataExchangeAttribute, and throw an exception if it does not find any (an InvalidOperationException with the message "Service does not implement an interface with the ExternalDataExchange attribute"). When WF finds the attribute, it creates proxy listeners for the events. These proxies can catch the events and then route them to the correct workflow instance, possibly waking up the instance after a long slumber inside a database table.
The Event Args
Notice the ExternalDataEventArgs class requires a Guid parameter, which identifies the workflow instance we want to reach with the event. If the event args class does not derive from ExternalDataEventArgs, we will see an error when we compile a workflow that attempts to receive the arguments as an event parameter. Activities have the ability to validate themselves and ensure we've set all the properties they need to function correctly at runtime.
When we drop a HandleExternalEvent activity in the workflow designer, we need to specify the interface and event name that the activity will listen for. If we don't derive from the correct class, the error will read: "validation failed: The event PaymentProcessed has to be of type EventHandler where T derives from ExternalDataEventArgs" (I think the error meant to say "of type EventHandler
The Implementation
We have a contract, an event args class, and all the metadata we need for workflow to hear out event. As Captain Hazelwood once said, what could possibly go wrong?
Let's start with an implementation of our payment processing contract. The code has a subtle problem, and will cause an exception.
class PaymentProcessingService : IPaymentProcessingService
{
public void ProcessPayment(Guid id, double amount)
{
// ... do some work work work
// ... then raise an event to let everyone know
PaymentProcessedEventArgs args;
args = new PaymentProcessedEventArgs(id, amount);
EventHandlerevh;
evh = PaymentProcessed;
if (evh != null)
evh(this, args); // boom!
}
public event EventHandler
PaymentProcessed;
}
The exception is an EventDeliveryFailedException, and the Message property will read like "Event PaymentProcessed on interface type IPaymentProcessingService for instance ID [GUID] cannot be delivered". The message doesn't yield any obvious clues, and we need to dig deeper to find more information.
If we look at the InnerException property, we get closer to an answer. The inner exception is an InvalidOperationException with the Message of "EventArgs not serializable". This exception is a little confusing, because we did make our EventArgs serializable! Notice in Figure 2 that the current exception ($exception in the 2005 debugger) is wrapping an inner exception, with the precise cause of failure.
The value of the message is cut off, but says "Type PaymentProcessingService is not marked as serializable". It appears every parameter going into the event must be serializable, including the sender parameter! We pass this reference, which points to our payment processing service. The workflow instance doesn't actually need a reference to our payment service (if it needs to invoke a method on the service, it can use the CallExternalEvent activity), so we can fix this problem by leaving the sender parameter as null or Nothing.
EventHandlerevh;
evh = PaymentProcessed;
if (evh != null)
evh(null, args);
If you are seeing an event delivery failure, drill into the inner exceptions to find the exact type causing the problem. Your event args might contain an object graph with an non-serializable type inside.
Setting Up The Workflow Runtime
I did jump ahead just a bit, because before the event even throws the exception we saw above, we need to have our payment service running and configured with the workflow runtime. First, we need to layer the ExternalDataExchangeService into the runtime. The ExternalDataExchangeService manages the host's local communication services, like our payment processing service. Then we add our payment processing service into the list of external services.
WorkflowRuntime workflowRuntime = new WorkflowRuntime();
ExternalDataExchangeService dataExchangeService;
dataExchangeService = new ExternalDataExchangeService();
workflowRuntime.AddService(dataExchangeService);
PaymentProcessingService paymentProcessing;
paymentProcessing = new PaymentProcessingService();
dataExchangeService.AddService(paymentProcessing);
// ...
Here is a version of the above code with a bug that took me quite some time to track down:
PaymentProcessingService paymentProcessing;
paymentProcessing = new PaymentProcessingService();
workflowRuntime.AddService(paymentProcessing); // this is WRONG!!!!!!!!
We have to add our service to the ExternalDataExchangeService, and not directly to the workflow runtime. My service would fire an event, but nothing would happen. Looking at the event in the debugger showed a null value, meaning nobody was subscribing to the event. It's the ExternalDataExchangeService that reflects on the incoming service, looking for metadata like the ExternalDataExchange attribute, and subscribing to events. At runtime the events are proxied to the workflow, as shown in Figure 3.
Summary
Raising events to a workflow can be finicky. If the events appear to fire into the empty vacuum of space, make sure the ExternalDataExchanceService and the local communications services are properly configured and added to the runtime. If the workflow runtime is throwing exceptions, the exception will most likely wrap the juicy details in its InnerException property (which in turn might have an InnerException). Hopefully, these two tips can save someone some time.
About the author
Scott Allen lives outside of Baltimore, and is a Microsoft MVP and founder of OdeToCode.com. In the last 14 years, Scott has shipped commercial software in the embedded, Windows and web platforms. You can reach Scott at scott@OdeToCode.com, or at his blog: http://www.OdeToCode.com/blogs/scott/.