Contents
Notes
This article was written using:
- .NET Framework 3.0 (RTM)
- VS 2005 Extensions for Windows Workflow Foundation (RTM)
The Microsoft Windows Workflow Foundation (WF) is part of the Windows platform. It is a core component of the next generation .Net Framework - version 3.0. The WF enables decoupling an application into the business workflows driven by activities. The workflows can be loaded into the host process such as Windows NT Service, IIS, WinForm, Console, etc. and attached to the WF Runtime core by the Hosting layer. This layer provides persistence, tracking, scheduling, transaction and communication support. Of course, there is also a capability for creating an extension - custom hosting service, for instance, adding a custom communication service between the host process and workflow based on the interface/event contract.
The workflows can talk to each other locally or remotely via a communication service using an event/delegate fashion pattern. The message exchange pattern (MEP) is based on the Request and Response events passing application specific data contract. This article describes a design and implementation of the workflow hosting connectivity (plumbing) to the Windows Communication Foundation (WCF) Service for WS-Transfer operation contract. It is an extension of my WS-Transfer for WCF article, where the WCF service is encapsulated into the generic service layer and physical adapter for handling a specific resource. In this case, our resource is a workflow instance of the WF.
Note, the workflows included in this project are for usage demonstration and test purposes only.
Having a WS-* driven Workflow model layer, we can plug the Workflows to the "WS-* Service Bus
" like it is shown in the following picture:
The WCF Services, with a configurable workflow adapter, enable creating a logical (distributed) model with encapsulating a business logic into the workflows and activities. Note, the above picture shows services only. The WCF/WF clients can be created in a similar way and easily plugged into the bus. Of course, any "legacy" WS-* service/client can also be connected to the bus using a workflow model layer in a transparent manner.
This article will describe the WS-Transfer Service only, but the concept and the implementation is similar and straightforward for other services such as WS-Eventing, WS-Enum, etc. Let's start with a concept of plumbing two infrastructures such as WCF and WF. In order to understand this pattern, I will assume that you are familiar with my article WS-Transfer for WCF and you have at least some experience (knowledge) with Microsoft WCF and WF Technologies.
The concept of the implementation is based on encapsulating a WCF Service layer driven by WS-Transfer stack from the resource (physical) layer by using the Indigo paradigm. In our case the physical resource is represented by a workflow instance of the WF.
The following picture shows the highest level of the connectivity between the WCF and WF:
As you can see, the above model has been decoupled into two layers such as communication and business processing. Both layers are independent and have their own technology. The service layer is responsible for sending and receiving messages with a WS-* stack (in our case WS-Transfer) to and from the specific resource. This is a generic service layer with capability of the service behavior extension made programmatically or administratively via a config file. The other layer - resource layer, is a business specific layer represented by a business workflow model. Both layers can run in their own behavior, synchronously or asynchronously based on the application needs.
The following part of the config file shows the service behavior extension for Memory Storage Workflow operations:
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="WxfServiceExtension" >
<wsTransferAdapter
name="PublicStorage"
type="RKiss.WSTransfer.Adapters.WFAdapter, WFAdapter"
TransactionScopeRequired="true"
TransactionAutoComplete="true"
Topic="Imaging"
WorkflowTypeCreate=
"WFLibTest.MemoryStorage.WorkflowCreate, WFLibTest"
WorkflowTypeGet="WFLibTest.MemoryStorage.WorkflowGet, WFLibTest"
WorkflowTypePut="WFLibTest.MemoryStorage.WorkflowPut, WFLibTest"
WorkflowTypeDelete=
"WFLibTest.MemoryStorage.WorkflowDelete, WFLibTest" />
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="wsTransferAdapter"
type="RKiss.WSTransfer.ServiceAdapterBehaviorElement, WSTransfer,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
</system.serviceModel>
</configuration>
In the above snippet, each WS-Transfer operation (Create, Get, Put and Delete) has been configured for own workflow transactional processing related to the same Topic="Imaging
". The name of the storage is "PublicStorage
". The plumbing adapter - a connectivity between the WCF Service and WF is implemented in the WFAdapter assembly.
The WS-Transfer adapter for workflow processing has built-in the following properties:
Property Name | Default value | Comment |
name | null | name of the adapter |
type | null | type of the adapter class |
TransactionScopeRequired | "false " | transactional scope is required |
TransactionAutoComplete | "true " | transactional scope complete |
Topic | null | topic of the resource |
WorkflowType | null | type of the generic workflow class |
WorkflowTypeCreate | null | type of the workflow class for Create resource |
WorkflowTypeGet | null | type of the workflow class for Get resource |
WorkflowTypePut | null | type of the workflow class for Put resource |
WorkflowTypeDelete | null | type of the workflow class for Delete resource |
Note, the collection of the properties is passed into the workflow instance during its initializing phase.
Hosting WF Runtime Services
The WF Runtime Services can be attached (must be only one per appDomain) to the WCF hosting process using the ServiceHost
Extension mechanism. The following code snippet shows an example for WSTransferService
hosted by the Console program:
using (System.ServiceModel.ServiceHost host =
new System.ServiceModel.ServiceHost(typeof(WSTransferService)))
{
WFServiceHostExtension extension =
new WFServiceHostExtension("WorkflowRuntimeConfig",
"LocalServicesConfig");
host.Extensions.Add(extension);
host.Open();
Console.WriteLine("Press any key to stop server...");
Console.ReadLine();
host.Close();
}
where, the WFServiceHostExtension
is the host extension class that is implementing an IExtension
interface for WF Runtime Services:
void IExtension<ServiceHostBase>.Attach(ServiceHostBase owner)
{
if(_workflowServicesConfig == null)
_workflowRuntime = new WorkflowRuntime();
else
_workflowRuntime = new WorkflowRuntime(_workflowServicesConfig);
_workflowRuntime.ServicesExceptionNotHandled +=
new EventHandler<SERVICESEXCEPTIONNOTHANDLEDEVENTARGS>
(workflowRuntime_ServicesExceptionNotHandled);
_exchangeServices =
_workflowRuntime.GetService<EXTERNALDATAEXCHANGESERVICE>();
if (_exchangeServices == null)
{
if (_localServicesConfig == null)
_exchangeServices = new ExternalDataExchangeService();
else
_exchangeServices = new ExternalDataExchangeService(_localServicesConfig);
_workflowRuntime.AddService(_exchangeServices);
}
_workflowRuntime.StartRuntime();
}
void IExtension<ServiceHostBase>.Detach(ServiceHostBase owner)
{
_workflowRuntime.StopRuntime();
}
Note, the "WorkflowRuntimeConfig
" is the name of the config section, where WF Runtime Services are located and required for specific workflow processing, for instance: System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService
allows to run a workflow activities synchronously within the same thread as WCF service operation.
The following config snippet shows the workflow config sections for adding runtime and local services:
<configSections>
<section name="WorkflowRuntimeConfig"
type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection,
System.Workflow.Runtime,
Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<section name="LocalServicesConfig"
type="System.Workflow.Activities.ExternalDataExchangeServiceSection,
System.Workflow.Activities,
Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<WorkflowRuntimeConfig >
<Services>
<add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService,
System.Workflow.Runtime,
Version=3.0.00000.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"/>
</Services>
</WorkflowRuntimeConfig>
<LocalServicesConfig>
<Services>
<add type="RKiss.WSTransfer.Adapters.WSTransferLocalService, WFAdapter,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Services>
</LocalServicesConfig>
WF Local Communication Services (LCS)
The Local Communication Services represent an implementation of the communication interfaces registered with the WorkflowRuntime
that enable data exchange between the workflow and host process layer based on the .NET event/delegate fashion pattern. The interface must be decorated by ExternalDataExchange
attribute in order to be found by the WF Runtime to provide correct event intercepting.
Let's look at this layer in detail using a Reflector. In order to bring the custom LCS into the plumbing business, we have to process its attaching to the WorkflowRuntime
using the built-in ExternalDataExchangeService
service.
The following code snippet shows the implementation of the AddService
method. Note, the inline comments have been added after Reflector:
public void AddService(object service)
{
if (service == null)
{
throw new ArgumentNullException("service");
}
if (base.Runtime == null)
{
throw new InvalidOperationException
("Error_ExternalRuntimeContainerNotFound");
}
this.InterceptService(service, true);
base.Runtime.AddService(service);
}
The actual plumbing (the event subscribing) is done in the InterceptService
method. The following code snippet was captured by the reflector and manually commented lines shows its implementation:
private void InterceptService(object service, bool add)
{
bool flag1 = false;
Type[] typeArray1 = service.GetType().GetInterfaces();
Type[] typeArray2 = typeArray1;
for (int num2 = 0; num2 < typeArray2.Length; num2++)
{
Type type1 = typeArray2[num2];
object[] objArray1 =
type1.GetCustomAttributes
(typeof(ExternalDataExchangeAttribute), false);
if (objArray1.Length != 0)
{
flag1 = true;
EventInfo[] infoArray1 = type1.GetEvents();
if (infoArray1 != null)
{
EventInfo[] infoArray2 = infoArray1;
for (int num3 = 0; num3 < infoArray2.Length; num3++)
{
EventInfo info1 = infoArray2[num3];
WorkflowMessageEventHandler handler1 = null;
int num1 = type1.GetHashCode() ^ info1.Name.GetHashCode();
lock (this.sync)
{
if (!this.eventHandlers.ContainsKey(num1))
{
handler1 = new WorkflowMessageEventHandler
(type1,info1,base.Runtime);
this.eventHandlers.Add(num1, handler1);
}
else
{
handler1 = this.eventHandlers[num1];
}
}
this.AddRemove(service, handler1.Delegate, add, info1.Name);
}
}
}
}
if (!flag1)
{
throw new InvalidOperationException("Error_ServiceMissing ...");
}
}
The concept of the interceptor is based on capturing all correct events in the LCS and subscribing them. The LCS communication interface contract is found by a custom ExternalDataExchange
attribute. For these selected interfaces, the interceptor is walking through each event. The interceptor creates a WorkflowMessageEventHandler
object and them subscribes handler's delegate to the event. At this time, the event/delegate plumbing is done and ready for use in the runtime.
As mentioned above, the WF Runtime interceptor is responsible for injecting a communication layer for abstracting and delivering a source event to the workflow event sink such as HandleExternalEvent
Activity. The following picture shows the major sequences between the Host and Workflow instance:
The host (in our case it is a WCF service - adapter) raises an event, for instance, CreateRequest
with application specific arguments and its delegate will invoke an event handler on the WorkflowMessageEventhandler
object. The handler is asking for a workflow instance from WorkflowRuntime
based on the event source parameter. The event source parameter is a workflow instance id.
After that, the handler will create a MethodMesage
and invoke an EnqueueItem
method on the workflow instance. This method will process an event enqueuing into the proper queue given by the WorkflowQueuingService
. For this job, the workflow instance will load a WorkflowExecutor
from the WorkflowRuntime
. Once the event (message) is stored in a queue, it can be dequeued by a subscribed listener such as HandleExternalEvent
activity.
The response method from the Workflow is easier than the above request method. The WF has a built-in CallExternalMethod
activity, which will raise an event on the communication interface in the LCS. The host application needs to implement an event handler for receiving response arguments from the workflow using a standard event/delegate fashion pattern.
WCF/WF Plumbing
The above description shows a communication mechanism to/from a workflow instance. We need a somewhat similar layer on the WCF service side. The following picture shows a plumbing model of the workflow and service:
As you can see, the above plumbing requires to plug-in a specific adapter for a service behavior extension. In our case this adapter is a WS-Transfer oriented with a capability of raising an event to the workflow instance and waiting for its response in the blocking manner. The picture shows an example of the Sequential Workflow with an input activity (receiver of the event), application specific activity (storing resource into the memory) and output activity to send a response message to the Adapter. The workflow can be extended for additional activities based on the business needs. Note, the HandleExternalEvent
and CallExternalMethod
activities are required for proper communication with the service adapter based on the Request/Response message exchange pattern. Plumbing all layers together, the WS-Transfer message ends in the configurable workflow event sink where it is dispatched. Based on the workflow activity, the result is passed back to the service using an invoking activity. Note, the adapter is waiting for this result in the blocking manner.
Implementation
It is necessary to create the communication interfaces for message exchange between the adapter and workflow as a first step of the implementation. We can have a common event source (Request) for all WS-Transfer interface request methods. Please see the following code snippet:
[ExternalDataExchange]
public interface IWSTransferLocalServiceIn
{
event EventHandler<RequestEventArgs> Request;
}
The next step is to create an application specific EventArgs
object for passing a request parameters to the event sink, based on the delegate signature such as a source object and EventArgs
. The following code snippet shows this derived class:
[Serializable]
public class RequestEventArgs : ExternalDataEventArgs
{
private ResourceDescriptor _rd;
private object _resource;
public RequestEventArgs
(Guid InstanceId, ResourceDescriptor rd, object resource) :
base(InstanceId)
{
this._resource = resource;
this._rd = rd;
}
public object Resource
{
get { return _resource; }
set { _resource = value; }
}
public ResourceDescriptor ResourceDescriptor
{
get { return _rd; }
set { _rd = value; }
}
}
In the above specific EventArgs
object, we are passing a resource descriptor and resource body needed for the resource factory implemented in the workflow process.
The response interface contract from a workflow has very simple operation, just a method for passing response parameters. This method is called by the CallExternalMethod
workflow activity.
[ExternalDataExchange]
public interface IWSTransferLocalServiceOut
{
void RaiseResponseEvent(Guid workflowInstanceId,
ResourceDescriptor rd, object resource);
}
Now, we can create a Local Service, let's called it a WSTransferLocalService
. This class represents a communication layer to the workflow from an external "host" layer. Its implementation is straightforward and there is a simple logic for raising an event for request or response. Note, the firing an event to the workflow has an option based on the ManualWorkflowScheduler Service
allowing to run workflow and adapter within the same thread. The following code snippet shows an implementation of the Local Service for a WS-Transfer adapter:
public class WSTransferLocalService :
IWSTransferLocalServiceIn, IWSTransferLocalServiceOut
{
private WorkflowRuntime _workflowRuntime;
private ManualWorkflowSchedulerService _scheduler;
public WorkflowRuntime WorkflowRuntime
{ get { return _workflowRuntime; } }
public ManualWorkflowSchedulerService Scheduler
{ get { return _scheduler; } }
public WSTransferLocalService(WorkflowRuntime workflowRuntime)
{
if (workflowRuntime == null)
throw new ArgumentNullException("workflowRuntime");
_workflowRuntime = workflowRuntime;
_scheduler =
_workflowRuntime.GetService<MANUALWORKFLOWSCHEDULERSERVICE>();
}
#region IWSTransferLocalServiceIn Members - to workflow
public event EventHandler<RequestEventArgs> Request;
public void RaiseRequestEvent
(Guid workflowInstanceId, ResourceDescriptor rd)
{
RaiseRequestEvent(workflowInstanceId, rd, null);
}
public void RaiseRequestEvent(Guid workflowInstanceId,
ResourceDescriptor rd, object resource)
{
RequestEventArgs e =
new RequestEventArgs(workflowInstanceId, rd, resource);
if (this.Request != null)
{
if (this.Scheduler == null)
{
this.Request(null, e);
}
else
{
this.Scheduler.RunWorkflow(workflowInstanceId);
this.Request(null, e);
this.Scheduler.RunWorkflow(workflowInstanceId);
}
}
}
#endregion
#region IWSTransferLocalServiceOut Members - from workflow
public event EventHandler<ResponseEventArgs> Response;
public void RaiseResponseEvent(Guid workflowInstanceId,
ResourceDescriptor rd, object resource)
{
ResponseEventArgs e =
new ResponseEventArgs(workflowInstanceId, rd, resource);
if (this.Response != null)
{
this.Response(null, e);
}
}
#endregion
}
That is all from the workflow communication layer. Lets look at the other side such as the WCF service. As I mentioned in my article WS-Transfer for WCF, the layer for handling a physical resource (factory and operation) is encapsulated into the adapter. Each WS-Transfer message will invoke a specific method in the adapter and return back a response to the service layer for mapping to the WS-Transfer message. The logic implemented in the method is straightforward and lightweight and provides the following:
- Creating a specific workflow based on the config file
- Registering an event sink for workflow response
- Raising a request event to the workflow
- Waiting for a response from the workflow
- Returning result to the service layer
Note, the above steps are necessary for Request/Response message exchange pattern. In the case of the fire and forget event, we need to send only the request event to the workflow sink.
The following code snippet shows an implementation of the Get
method for passing a message into the WorkflowTypeGet
:
public object Get(MessageHeaders resourceIdentifier)
{
base.Properties["TransactionDependentClone"] =
Transaction.Current == null ?
null : Transaction.Current.DependentClone
(DependentCloneOption.BlockCommitUntilComplete);
WorkflowInstance wi = CreateWorkflow(Defaults.Keys.WorkflowTypeGet);
AsyncResponseFromLocalService ar =
new AsyncResponseFromLocalService
(this.wstransferLocalService, wi.InstanceId);
ResourceDescriptor rd = GetResourceIdentifier(resourceIdentifier);
this.wstransferLocalService.RaiseRequestEvent(wi.InstanceId, rd);
ar.WaitForResponse(workflowTimeoutInSec);
object result = ar.Response.Resource;
XmlDocument doc = new XmlDocument();
doc.LoadXml(result as string);
result = doc.DocumentElement;
return result;
}
In the sync Request/Response message exchange, the response is waiting for the message in the blocking fashion pattern. The following code snippet shows an internal class for this purpose. Calling a WaitForResponse
method, the thread will waiting for an event from the workflow invoking a localservice_FireResponse
method. This method will store a response message and then it will set a synchronization object to unblock a thread. If the workflow threw an exception, this class will re-throw it.
internal class AsyncResponseFromLocalService
{
AutoResetEvent _waitForResponse = new AutoResetEvent(false);
WorkflowTerminatedEventArgs _e;
WSTransferLocalService _localservice;
ResponseEventArgs _response;
Guid _istanceId;
public AsyncResponseFromLocalService
(WSTransferLocalService localservice, Guid instanceid)
{
_istanceId = instanceid;
_localservice = localservice;
_localservice.Response +=
new EventHandler<ResponseEventArgs>(localservice_FireResponse);
_localservice.WorkflowRuntime.WorkflowTerminated +=
new EventHandler<WorkflowTerminatedEventArgs>(OnWorkflowTerminated);
}
public void OnWorkflowTerminated
(object sender, WorkflowTerminatedEventArgs e)
{
if (_istanceId == e.WorkflowInstance.InstanceId)
{
this._e = e;
_waitForResponse.Set();
_localservice.WorkflowRuntime.WorkflowTerminated -=
new EventHandler<WorkflowTerminatedEventArgs>
(OnWorkflowTerminated);
}
}
public void WaitForResponse(int secondsTimeOut)
{
bool retval = _waitForResponse.WaitOne(secondsTimeOut * 1000, false);
if (_e != null)
throw this._e.Exception;
if (retval == false)
throw new FaultException("The workflow timeout expired");
}
public ResponseEventArgs Response
{
get { return _response; }
}
public WorkflowTerminatedEventArgs WorkflowTerminatedEventArgs
{
get { return _e; }
}
public void localservice_FireResponse(object sender, ResponseEventArgs e)
{
if (_istanceId == e.InstanceId)
{
_response = e;
_waitForResponse.Set();
_localservice.Response -=
new EventHandler<ResponseEventArgs>(localservice_FireResponse);
}
}
}
The complete solution has been created and decoupled into small projects. The following picture shows its layout:
I made a copy of the WSTransfer folder from my previous article WS-Transfer for WCF and plugged-into the solution making one consistent package. Then, I created a folder WFAdapter for the implementation of the connectivity between the WCF and Workflow. Attaching Workflow to the host process is implemented in the fully reusable assembly via WFServiceHostExtension. The application specific Workflows are located in the WSTransferWorkflowLibrary. Note, this is a test library and it must be created for the specific resource. I created MemoryStorage
Workflows for demonstration purpose only.
Finally, we need a test client and service, therefore I created a separate folder for this project. In addition, I added my Logger, based on the message interceptor with publishing message on the host console programs.
Of course, in order to build this solution, we have to install the WF extension.
After downloading the solution and its compilation, we are ready for testing. The service host program must be started first before the clientApplication console program. The following screenshot will be produced during the testing. The test is very simple, creating a resource in the MemoryStorage
handled by activity in the Workflow and then the client calls an operation for get, put, delete, etc. The Logger will display WS-Transfer conversation via WFAdapter and Workflow.
In this article, I have described how to plumb the WCF and Workflow for WS-Transfer Service. Encapsulating a business logic into the workflow activities enables us to create a logical business model where connectivity is full transparent based on the deployment schema. The WCF Transfer Service with a WFAdapter unifies communications between the workflows based on the WS-Transfer spec. Based on this concept, we can build other WS-* spec driven workflow services and plug them into the "Service Bus" and take advantage of the SOA.