There are currently a couple of options to expose a Windows Workflow as as service.
- There is a native option to publish a developed Workflow Library project as a ASP .NET Web Service (ASMX).
- You can host it yourself (ASMX, WCF)
- William Tay is doing excellent work towards hosting a workflow inside the WCF service pipeline (WCF)
- Roman Kiss created a static WorkflowInvoker class, which does all the heavy liting for you, if you want to host your workflow inside the WCF service method (WCF)
I'm going to focus on Ad 1 and Ad 2 in this post.
Ad 1:
There's an option to host your workflow library inside a web service by using a "Publish as a Web Service" option inside Visual Studio 2005. This creates a separate ASP .NET Web Service project inside your current solution, which you can later manually or automatically publish as a web site to your IIS of choice.
The are two major downsides to this story. The first is that this gives you practically no control over how the web service is created. Second downside, while documented, is that the current implementation of System.Workflow.Runtime.WorkflowWebHostingModule works in particular ways with the workflow persistence story.
Let's assume we have to following interface defined for this web service:
interface IServiceInterface
{
void SendOrder(Order order);
Order GetOrder(Guid guidOrder);
int GetOrderStatus(Guid guidOrder);
}
What happens is (request number 1):
- You publish your workflow as a web service
- You hit the service endpoint with a browser
- Workflow instance gets created, is run and returns a result
- At this time the workflow runtime (System.Workflow.Runtime.WorkflowRuntime instance) creates a workflow instance and runs it. Since workflow completes succesfully it destroys the instance at the end of execution.
- Workflow runtime returns a cookie with the workflow instance back to the browser and since IE's default setting is to accept cookies, it is written to the client's disk
All good, right?
Actually, what happens during request number 2?
- You hit the endpoint again
- IE knows that the site has a persisted cookie, so it sends it bundled with the SOAP request
- Workflow runtime sees it and tries to load the specified workflow instance
- This instance is long gone, it does not exist in memory (it has been destroyed, remember?), so workflow runtime tries to rehydrate it from a persistence store. If there is a persistence store defined it goes there (most probably WorkflowPersistenceStore in SQL Server) and correctly identifies that the workflow instance is not present, so it fails with 'Workflow with id <GUID> not found in state persistence store.'. If the persistence store is not defined for this workflow it fails with 'The workflow hosting environment does not have a persistence service as required by an operation on the workflow instance <GUID>.'.
And all this is actually the expected behavior if you think hard enough. Workaround? Hit the endpoint with a newly loaded IE window. It works every time, since a cookie with an instance ID is not present.
Another thing to mention here is that this issue does not manifest itself if you hit the endpoint programatically using the web service proxy, unless you are using a CookieContainer class to cache the returning cookies.
Ad 2:
Hosting a Windows Workflow manually is another option, which gives you more flexibility towards the service detail tweeking.
You can host it using the following code:
[WebService(Namespace = "http://webservices.gama-system.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class WorkflowService : System.Web.Services.WebService
{
// workflow runtime
private static WorkflowRuntime workflowRuntime = new WorkflowRuntime();
[WebMethod]
public void SendOrder(Order order)
{
AutoResetEvent waitHandle = new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs e)
{
waitHandle.Set();
};
workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs e)
{
waitHandle.Set();
};
// create workflow instance with the specified parameters
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(MyWorkflow));
instance.Start();
waitHandle.WaitOne();
}
}
An important thing in the specified sample is that the System.Workflow.Runtime.WorkflowRuntime instance is static to the service implementation class. This is a requirement, since the workflow runtime can only get loaded once per appdomain. If this is not the case you will get an exception during the second invocation of the workflow.
If you are using any additional workflow runtime services, like persistence, tracking or your own communication service to communicate with the workflow you will need to track that the services get loaded once only. Here's the example:
[WebService(Namespace = "http://webservices.gama-system.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class WorkflowService : System.Web.Services.WebService
{
// workflow runtime
private static WorkflowRuntime workflowRuntime = new WorkflowRuntime();
// services added
private static bool booServicesAdded = false;
// communication service
private static CommunicationService objComm = new CommunicationService();
[WebMethod]
public void SendOrder(Order order)
{
// add communication service
if (!booServicesAdded)
{
ExternalDataExchangeService externalService =
new ExternalDataExchangeService();
workflowRuntime.AddService(externalService);
externalService.AddService(objComm);
booServiceAdded = true;
}
AutoResetEvent waitHandle = new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs e)
{
waitHandle.Set();
};
workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs e)
{
waitHandle.Set();
};
// create workflow instance with the specified parameters
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(MyWorkflow));
instance.Start();
waitHandle.WaitOne();
}
}
This adds the required services only during the first invocation of a web service. Since workflow runtime is a static class the services get persisted during all subsequent service calls. A boolean variable booServicesAdded is responsible for flag storage.