Rebuilding Workflow On The Fly, The Real Answer

Last month, I wrote a post about rebuilding workflow instances when attempting to run the workflow when using WF 3.0. Unfortunately, that post doesn’t work. Okay, I posted it before I’d fully tested it because it so obviously seemed like it would work. Bad on me, I know. But hopefully, I’ll be making it up today. As for the delay, I had a few other projects crop up which delayed solving this problem.

It turns out that when you call into an instance of the WF ExternalDataEventArgs class (or rather, one of it’s children), try to raise the event, the system will actually remove the workflow instance from persistence. Since you raise the events before you manually run the workflow, the logic I had before won’t work since an exception regarding the workflow instance being broken is thrown before I even reach the code written back then.

The best approach to employ seemed like it would be similar to the method used in RunWorkflow. I’d extend the mediator to take an action which would raise an event, like so:

void RaiseEvent(Action raise, ExternalDataEventArgs args, Func rebuilder)
{
    try {
        raise(args);
    } catch (Exception e) {
        if (rebuilder != null) 
        {
            rebuilder(args.InstanceId);
            raise(args);
        } 
        else 
        {
            throw e;
        }
    }
}

There is one major refactoring this solution required. None of our Raise Event methods took ExternalDataEventArgs, they all took subclasses of the ExternalDataEventArgs class. Since the signature on a delegate must always match, every single one of the raise event methods must be converted to take the more generic ExternalDataEventArgs, performing a cast internally. Basically turning this:

// SpecialEventArgs is a child class of ExternalDataEventArgs
void RaiseSpecialEvent(SpecialEventArgs args)
{
    if (specialEvent != null)
        specialEvent(null, args);
}

into this:

// SpecialEventArgs is a child class of ExternalDataEventArgs
void RaiseSpecialEvent(ExternalDataEventArgs args)
{
    if (specialEvent != null)
        specialEvent(null, args as SpecialEventArgs);
}

Now, these two calls:

RaiseSpecialEvent(new SpecialEventArgs(val1, va2));
Mediator.RaiseEvent(RaiseSpecialEvent, new SpecialEventArgs(val1, val2), null);

Do exactly the same thing, just the Mediator.RaiseEvent call passes through an extra layer of redirection. However, if you replace that null argument with a function that can rebuild a workflow given a valid Guid, the system will instead rebuild the workflow and raise the event properly (this should almost always work). The only downside? The code is slightly less clear, and some checks that used to happen at Compile Time are deferred to Runtime. These aren’t ideal, but for us, the ability to push out new versions of the application without having to take everything down, is a bigger win.

Luckily, a lot of these changes probably won’t be necessary in WF 4.0, at least if the PDC videos on the subject are any indication. WF 4.0 certainly looks like it will finally fulfill more of the promise that WF was supposed to fill. While I’m not terribly likely to suggest anyone pick up WF today, I suspect that will change in a year or so, when .NET 4.0 and WF 4.0 become available.