Published Monday, November 06, 2006 4:22 PM by mtaulty

Workflow and ASP.NET Web Services

With the Workflow Foundation, there's a wizard in Visual Studio 2005 that does a "publish as a web service" and I've had quite a bit of mail about this so I thought it might be worth unpicking what seems to go on when you use the Wizard.

Let's say I've got a simple scenario where I want to implement a Workflow for a pretend document approval process where the Workflow is started by a call to a web service and then the Workflow waits for a second web service call to say that the document "is approved".

To start things off, I'd need to define an interface for the document approval process, say;

 

  public interface IDocumentApproval
  {
    void SubmitDocument(string documentName);

    void ApproveDocument();
  }
 

and then I can go off and draw a workflow that looks something like below (the Activities are both "Web Service Input" Activities);

 

and I've just configured properties on these 2 Activities as;

 

and the 2nd one is very similar and I don't actually do anything interesting in between these 2 in my fake scenario here but no doubt a real scenario would do something.

I can then go and use the "Publish As Web Service" menu option and throw this straight over the wall into ASMX.

What does this give me?

1) Web.Config

The Web.Config has a WorkflowRuntime section that drops in both the Manual and Default Workflow Scheduler Services. You can find a lot more info on these here so I'm not going to go into any depth on that here.

The Web.Config also configures an IHttpModule into the HTTP pipeline for ASP.NET (modules are the ones that sit along the pipeline and get a chance to pre- and post- process requests whereas handlers sit at the end of the pipeline).

The module is called WorkflowWebHostingModule and a quick look with Reflector tells me that it does some tracing of key events and that it adds handlers to the AcquireRequestState and ReleaseRequestState events on the HttpApplication.

As a request comes in to the server, one of these handlers looks for a cookie called WF_WorkflowInstanceId and if it finds it then it drops that value onto the HTTP Context and calls it __WorkflowInstanceId__. The other handler does the reverse as the response leaves the server - i.e. it takes a value called __WorkflowInstanceId__ from the content and drops it into a cookie called WF_WorkflowInstanceId (if that cookie wasn't already there on the request).

2) ASMX File

The ASMX file doesn't really do anything other than point to a class that lives in the compiled assembly. In my case, this seems to have been named XXX_WebService where XXX was the name of my Workflow class.

3) A compiled assembly

The compiled assembly is where the XXX_WebService class lives. The XXX_WebService class ends up being derived from System.Workflow.Activities.WorkflowWebService which itself derives from System.Web.Services.WebService.

The big method in WorkflowWebService looks to be the Invoke method and one of the properties that it uses is the WorkflowRuntime property which lazily creates (in a thread-safe manner) a WorkflowRuntime if one isn't already available.

The logic then goes something like (this is loosely interpreted);

    1. Check to see if we're activating a workflow or talking to an existing one (mostly based on the instance Id taken from the cookie earlier on)
    2. Create the instance or grab the existing one
    3. Run the workflow using the ManualWorkflowSchedulerService
    4. Sort out any outgoing parameters to get them back into the web service response

There's a bit of handling of events like the Workflow terminating and so on but this is mostly it.

So, using this kind of mechanism is nice in that;

  1. I don't have to build a separate web service.
  2. I don't have to worry about hosting up the WorkflowRuntime.
  3. I don't have to worry about the difference between finding an existing Workflow instance and creating a new Workflow instance although that's only going to work for me as long as I've got some kind of client that's prepared to acknowledge the Cookie that gets sent back from the web service and Cookie's feel very much wedded to HTTP.

and that leads to making the whole publication process a fairly productive thing but there are some other aspects here;

  1. I'm not so sure how my WorkflowRuntime is being manipulated, the code is hidden.
  2. The ManualWorkflowSchedulerService is always used - whilst this is probably the right default choice there are perhaps some situations where you fancy using another scheduler.
  3. The web service support is intrinsically tied to ASMX web services. It'd be hard to suddenly swap in support for WCF services for instance.
  4. The web service support is intrinsically tied to HTTP. You could host ASMX style services over TCP with (say) WSE 3.0 but you can't do that with these services because of the Cookie which needs HTTP.
  5. Even if you're on HTTP and ASMX you might not want/have a Cookie container for (4).

 

I'm not trying to be exhaustive here but it looks like (as with everything) it's a trade-off and there will be a number of benefits of going this way and a number of marginal downsides of doing it.

There's another way of thinking about this and that's to model the interaction using External Data Exchange. Now, this is likely to be more work than just going with the built in WebService Input and Output Activities so it's not to be assumed that this is the default way to go but it's an additional option.

To work this way, the web service interface needs to change to take account of the fact that it needs to return some kind of identifier to the Workflow that gets created and to accept that identifier back in the one and only subsequent call. I can either return the identifier itself or some correlating token but in this case I'll use the identifier itself and attribute the interface for ASMX as in;

 

  [WebServiceBinding(
    Namespace="urn:mtaulty-com", ConformsTo=WsiProfiles.BasicProfile1_1)]
  public interface IDocumentApproval
  {
    [WebMethod]
    Guid SubmitDocument(string documentName);

    [WebMethod]
    void ApproveDocument(Guid instanceId);
  }
 

If I change the Workflow to work this way then we need a new External Data Exchange interface to sit in between the Workflow and the host (ASP.NET in this case) something like;

 

  [Serializable]
  public class DocumentApprovedEventArgs : ExternalDataEventArgs
  {
    public DocumentApprovedEventArgs(Guid id) : base(id)
    {
      this.WaitForIdle = true;
    }
  }

  public delegate void DocumentApprovedEventHandler(object sender,
    DocumentApprovedEventArgs args);

  [ExternalDataExchange]
  public interface IDocumentExchange
  {
    event DocumentApprovedEventHandler DocumentApproved;
  }

 

and now I can model the Workflow in terms of that;

 

Note that there's no need to wait for a web service call at the start of the Workflow because we'll explicitly start the Workflow from the first call to our web service but now, there's a need to get this all hosted up inside of an ASP.NET web service.  

So, to write a web service...First, I need a little class the implements my data exchange service;

 

public class ExchangeService : IDocumentExchange
{
  public void FireDocumentApproved(Guid instanceId)
  {
    DocumentApprovedEventArgs args =
      new DocumentApprovedEventArgs(instanceId);

    DocumentApproved(null, args);
  }
  public event DocumentApprovedEventHandler DocumentApproved;
}

 

Then that can be used from within the service itself;

 

public class Service : IDocumentApproval
{
  public Service()
  {
  }

  public Guid SubmitDocument(string documentName)
  {
    WorkflowInstance instance = Runtime.CreateWorkflow(typeof(Approval));
    instance.Start();

    scheduler.RunWorkflow(instance.InstanceId);

    return (instance.InstanceId);
  }

  public void ApproveDocument(Guid instanceId)
  {
    exchangeService.FireDocumentApproved(instanceId);

    scheduler.RunWorkflow(instanceId);
  }


  private static WorkflowRuntime Runtime
  {
    get
    {
      WorkflowRuntime r = null;

      lock (lockObject)
      {
        if (runtime == null)
        {
          runtime = new WorkflowRuntime();
          exchangeService = new ExchangeService();

          ExternalDataExchangeService dataService = 
            new ExternalDataExchangeService();

          runtime.AddService(dataService);

          dataService.AddService(exchangeService);

          scheduler =
            new ManualWorkflowSchedulerService();

          runtime.AddService(scheduler);

          runtime.StartRuntime();
        }
        r = runtime;
      }
      return (r);
    }
  }
  static Service()
  {
    lockObject = new object();
  }
  private static WorkflowRuntime runtime;
  private static ExchangeService exchangeService;
  private static ManualWorkflowSchedulerService scheduler;
  private static object lockObject;
}
Note - this code is perhaps a bit rough and ready as I didn't spend very long here  (not sure I like the idea of creating big things like WorkflowRuntimes under a lock for a start) but it at least maybe gives the idea that we've moved from the original mechanism using the Activities that explicitly support web services to something more generic (the External Data Exchange service) and in doing so;
  1. More code got written. Productivity went down. We had to create the runtime, add services and know the difference between creating a Workflow instance and using one that already existed :-(
  2. We're fully aware of where our WorkflowRuntime is coming from :-)
  3. We could switch from the ManualWorkflowScheduler if we wanted/needed to so we've got more control :-)
  4. The Workflow is no longer tied to ASMX, HTTP nor Cookies - we could re-deploy this with WCF or something else :-)
  5. In writing code, I probably introduced N bugs :-(

 

So, once again, there's upsides and downsides but (and this is the power of Workflow Foundation) you're not boxed into a corner and you have choice(s) as to how you go about building things.

Note: I think we'd need a persistence service to make this a bit more realistic in the web scenario and we'd also perhaps need to give some thought to shutting the runtime down if the web application got shut down.

# New and Notable 123 @ Wednesday, November 08, 2006 9:52 AM

A very good day to leave the country and find a new one... WCF/Indigo/SOA/Workflow/.NET Framework 3 Here

Sam Gentile

# ASP.NET, WF and sending data backwards and forwards @ Wednesday, December 20, 2006 12:34 AM

I saw some questions on an internal alias about "how to get ASP.NET to talk to Workflows" in terms of...

Mike Taulty's Blog

# New and Notable 123 @ Saturday, October 20, 2007 12:33 PM

A very good day to leave the country and find a new one... WCF/Indigo/SOA/Workflow/.NET Framework 3 Here

Sam Gentile