Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Workflow Adapter/Connector Pair

4.81/5 (30 votes)
19 Apr 200730 min read 1   951  
The Workflow Adapter/Connector pair are custom WF Activities for a Business-To-Business Logical Connectivity based on the Interface Contract. The connectivity handles the invoking and consuming of Workflows, Remoting objects, WCF Services in the transparent manner based on the configuration.

Contents

Notes

This article was written using:

  • .NET Framework 3.0 (RTM)
  • VS 2005 Extensions for Windows Workflow Foundation (RTM)

Features

  • Logical Workflow Connectivity represented by Adapter/Connector pair Activities
  • Interface contract driven connectivity
  • Using a transparent proxy for invoking workflow similar to the remoting object
  • Request/Response message exchange pattern
  • Logical Workflow Context
  • Application specific context
  • Open connectivity model for any physical connectivity
  • Built with Remoting and WCF connectivity
  • Sequential and State Machine Workflows usage
  • Generic usage of the WorkflowInvoker class
  • Generic Local Service for Request/Response event driven pattern
  • Send message direct to the Workflow instance queue
  • Create/Get Workflow support
  • NonePersistenceService for stateless operation
  • Generic hosting pattern for any process such as Windows NT Services, WebService, Console, WinForm, WebForm, ...

Technologies and Programming Techniques

  • Custom Remoting
    • Channel and Provider Sink
  • Windows Communication Foundation (WCF)
    • Custom Operation Invoker
    • Passing application context via message (header block)
    • Custom ChannelFactory class
  • Windows Workflow Foundation (WF)
    • Custom Activity, Designer, Validator, TypeConverter, Editor, ...
    • Event driven Workflow base class
    • Request/Response for LocalService Contract

Introduction

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 Workflow represents a logical process of the code units (Activities) running in the same appDomain.

Mapping the Business Model into the Logical Workflows enables the business and system context to flow via the physical workflows based on the deployment schema. This logical model requires a logical connectivity between the physical workflows, event and data sources described by their addresses, binding and contract definitions (ABC). In the Business model, the physical connectivity is the resource known as ABC Knowledge Base persisted in the database or locally in the config file. The logical connectivity between the end points can be virtualized into the Adapter and Connector in the way the following picture shows:

Image 1

The Adapter/Connector design pattern allows to have flowing parameters between the business layers without knowing their physical location in the loosely or tightly coupled manner. This mechanism is successfully used in the BizTalk Server architecture.

In the .NET Connected Systems, the Adapter represents a transparent proxy to map an RPC call into the IMessage abstract image and passing it to the underlying communication channel. On the other side - Connector is the stub to initiate a requested object (in our SingleCall case) and invoke its method based on the IMessage contents. The IMessage stream is transparently delivered to the stub and business layer.

This unique design pattern encapsulated a physical transport from the business layer. The transparent proxy and stub has been introduced by Remoting and forwarded to the Windows Communication Foundation (WCF) paradigm and WF for external data exchange communication.

This article describes an Adapter/Connector plumbing using a Custom Remoting for Workflow connectivity in the fully transparent manner, similar to the Remoting objects. The consumer of the workflow doesn't need to know about the workflow hosting or communication infrastructure, etc. The Custom Remoting for Workflow will handle the plumbing of Adapter to the Workflow Connector based on the deployment schema.

The following pictures show this feature in the same appDomain or across the process boundary. Basically, this solution requires a Custom Remoting Sender and /or Sink, configuration for standard remoting object and custom activities/workflow. We will discuss about the custom activities in detail latter, for now let's assume that we have them.

Note, that our workflows are not derived from the MarshalByRefObject abstract class.

Case 1. - all components are hosted in the same appDomain.

In this scenario, we need to register both workflow types such as Well known SingleCall Remoting object with a unique endpoint URI and also the custom remoting channel for workflow with a schema wf. The endpoint URI (for this channel) has the following format:

wf://localhost/xxxx

where: xxxx represents a unique resource identifier (URI) of the workflow type.

Image 2

How is the above scenario working? Well, from the client point of the view, it is like consuming any remoting object. First of all, the client needs to create a transparent proxy for the specific contract type represented by Interface and endpoint URI, and then it call its method. The following code snippet shows the code example:

C#
IMyContract proxy = (IMyContract)Activator.GetObject(typeof(IMyContract), 
                                endpoint);
response = proxy.MyMethod(...);

As you can see, the above lines are well know remoting "new" operator for interface contract type. The consumer (business layer) doesn't need to know how and where the "remoting" object is located, that's why we are calling the proxy as a transparent proxy. Now, remoting paradigm will invoke the remoting channel based on the schema. In our scenario, case, the schema is wf, therefore the custom remoting channel for workflow will get the control (ClientWorkflowInvoker). The wf channel sender is responsible for creating and initiating a workflow based on the endpoint URI, then passing the remoting message into the workflow instance queue and start processing it. Based on the interface contract, the channel sink can wait for a response or return immediately back to the client.

Using the Remoting for Adapter/Connector connectivity within the appDomain is the best solution. The communication is based on the request/response exchange message pattern driven by interface contract.

Let's look at the scenario where workflows are hosted in the different appDomains. The following picture shows this case:

Case 2. - the components are hosted in different appDomains across the process boundary.

Image 3

In the above case, the Adapter1 is connected to the Connector2 via Remoting, for instance tcp channel:

tcp://targetMachine:1234/xxxx

where: xxxx represents a unique resource identifier (URI) of the workflow type.

The Adapter1 invokes a method on the Connector2 based on the Interface Contract in the standard way such as a remote object (derived by MarshalByRefObject). When the standard remoting channel is used, the IMessage stream is transported via the channel media to the remoting server channel sink. As you can see, the config file (in the above picture) has a custom sink in the serverProviders. This ServerWorkflowInvoker sink will get the IMessage stream in the ProcessMessage call. This sink has the same responsibility as the ClientWorkerInvoker, to create a workflow instance based on the endpoint URI and enqueue the incoming message to the workflow. In the case of the OneWay contract, the sink will return a null response back to the channel stack, otherwise it will wait for a workflow response in the blocking manner.

Note, that the Adapter and Connector pair is fully virtualized for any kind of connectivity, therefore it is not necessary to build one for different channels or transports. The Adapter and Connector pair represents a connectivity layer to the business layer. The default channel connectivity is implemented for Remoting and WCF, but creating another physical connectivity (for instance: WSE) is straightforward.

Well, I hope you have a brief picture of the Custom Remoting for Workflow and position of the Adapter and Connector Activities. I will give you one more example from the event driven architecture - transactional async processor. Changing the endpoint URI property at Adapter1 for example:

   msmq://.\private$\MyOrder/xxxx 

where: xxxx represents a unique resource identifier (URI) of the workflow type, we can divide a logical business workflow into the physical workflows such as Sync and Async Workflows.

The first one will create all business activities in the sync manner and its Adapter1 will send a transactional message to the Async Workflow2 via a private queue, in our example - MyOrder. The client will immediately receive a response from the Sync Workflow, which indicates a status of the request (for example, request Id). The async process is started by dispatching a transactional message to the Workflow2.Connector from the ServerWorkflowInvoker Sink located in the MSMQ Server Channel Stack. Based on the workflow sequence, the notification message can be sent back to the client with a correlation information. The MSMQ Channel can be found here.

Workflow Activities for Connectivity

There are 3 major activities for Workflow Connectivity:

  • Connector and Return Activities for incoming and outgoing operation contract
  • Adapter Activity for calling an operation contract

The Adapter and Connector represent a logical connectivity based on the operation contract (Interface type) which can be tightly coupled for remoting communication channel or loosely coupled for WCF, WSE, WS-* technologies. To simplify the plumbing implementation, this article version handles only input arguments in the operation contract.

Let's look closely at these Activities:

  1. ConnectorActivity

    By inserting a ConnectorActivity into the Workflow, we can specify an operation contract for the incoming message. The workflow queue name is created by the Interface type and method. The validation process is in the order of the Type, MethodName and Parameters. The parameters (only inputs) represent a dynamic metadata of the Activity. The Connector will block the execution of the workflow until the message has been received. The Activity post-process can be customized by Received handler.

    Image 4

    Note, the Interface Type can be selected by the BrowseEditor (with an interface filter) that is shown in the following picture:

    Image 5

  2. ReturnActivity

    The Return is a correlation Activity to the Connector for handling a return value of the operation contract. Since the workflow can have more than one Connector, the ReturnActivity must select one, otherwise the error will be indicated. After that, the (ReturnValue) property will show up for its binding. In the case of the void type or (None) selected name, the correlation validity is not applied. The Activity pre-process can be customized by Invoking handler.

    Image 6

  3. AdapterActivity

    The major responsibility of this Activity is to synchronously invoke a selected operation contract on the Connector. The Adapter has a generic connectivity layer based on the Interface contract and target endpoint, therefore it can be used for native connectivity such as remote object, WCF service, WSE, etc. based on the endpoint URI schema format. Note, that connectivity between the Connector and Adapter is based on the physical transport in sync or async manner. For instance, using the MSMQ channel, the Adapter can invoke an operation contract in transactional sync manner, independent from the Connector availability. The Adapter can be pre/post processed by creating accepted handlers such as the Invoking and Invoked handlers.

    Image 7

    The above properties picture shows a value of URI property that has been populated for wf custom channel. The current article version supports the following URI format:

    URI

    Comment

    wf://localhost/myWorkflowNull channel for invoking a Workflow in the same appDomain
    wcf://myClientEndpointNameWindows Communication Foundation (WCF aka Indigo) connectivity described in the config file (ABC endpoint fashion)
    IPC, TCP, HTTP, HTTPS and custom remoting standard and custom remoting URI format, for example:

    tcp://localhost:1234/myWorkflow msmq://.\Private$\MyQueue\myWorkflow, ...

    @myConfigNamethis is an alias name of the connectivity for mapping a real value from the config file - AppSettingSection

Sequential Event driven workflow base class

To simplify an event driven workflow design, the OperationContractWorkflowBase base class can be used for custom sequential workflow. The base class has a built in a capability of the Connector Activity. The following picture shows properties for OneWay Operation Contract:

Image 8

In the case of the return value from the Operation Contract, the ReturnActivity must be used in the workflow design. The following picture shows a pattern of these pair Activities. The ConnectorActivityName must be selected in order to properly correlate a return value of the Operation Contract:

Image 9

That's all about the Workflow side. Now, let's go back to the Workflow Host Layer. As I mentioned earlier, the Workflow Logical Connectivity represents an application (business) connectivity driven by the Interface Contract. These layers can be connected using a tightly coupled technology such as Remoting, which will require to deploy a strong type contract on both sides and intermediate nodes. The Remoting technology is best for its performance in the closed Enterprise solution, where both ends are controllable and not frequently changed.

The Service Oriented Architecture (SOA) is driven by loosely coupled connectivity. The incoming new Microsoft Communication model such as Windows Communication Foundation (WCF - aka Indigo) has the capability to map a business model into the physical model driven by SOA. Note, the first version of the WCF and WF Technologies are not directly incorporated. It would be nice having an attributed workflow class for service contract and using the operation contract for message exchange.

Ok, back to reality. How we can make WCF/WF interop easy and transparently? Unfortunately, both technologies are great for customizing and exposing vertically and/or horizontally. This article solution uses an IOperationInvoker to intercept a selected service operation by WorkflowInvoker for handling a message exchange between the workflow and service. The following picture shows more details:

Loosely Coupled Connectivity - WCF

Based on the above description of the loosely coupled connectivity, using the WorkflowInvokerAttribute at the OperationContract, we can plug the Workflow into the WCF paradigm. This plumbing requires only a service type as shown in the following code snippet:

C#
[ServiceBehavior(IncludeExceptionDetailInFaults=true)]
public class WorkflowService : ITest
{
  //[WorkflowInvoker(CallContextActors = "InterfaceContract, MyContract")]
  [WorkflowInvoker]
  public string SayHello(string msg)
  {
    throw new NotImplementedException();
  }

  [WorkflowInvoker(WorkflowType = typeof(WorkflowLibrary.Workflow6)]
  //[WorkflowInvoker]
  public void OneWay(string msg)
  {
    throw new NotImplementedException();
  }
}

That's great. Thanks for WCF communication model, which enables to decorate a service class to hide all underlying logic and forward a message to the target (Workflow). Note, that this design pattern allows to plug-in a selected operation contract, so we can mix the service contract with WCF and WF business processing. One more note, I didn't find the way to intercept the operation contract by config file. It seems to me, the IOperationBehavior is possible to use by the Attribute only. It would be nice to have a possibility to intercept an IOperationInvoker for a specific endpoint in the config file. Having this feature, we can plug a Workflow to any WCF service via the config file without recompiling the service. Anyway, I will look for this feature in the near future.

Now, the question is, how does the WorkflowInvoker know about the workflow type? Well, there are two ways for WorkflowInvoker to know. The first one is the hard coded way, see the OneWay method, using the WorkflowType argument. The other way (more preferable) is using a config file and remoting service. The following code snippet shows this example:

XML
<system.serviceModel>
 <services>
  <service name="Server.WorkflowService" >
    <endpoint
      address="net.tcp://localhost:1212/myWorkflow6"
      binding="netTcpBinding"
      contract="InterfaceContract.ITest"/>
  </service>
 </services>
</system.serviceModel>

<system.runtime.remoting>
 <application>
  <service>
   <wellknown mode="SingleCall"
                 type="WorkflowLibrary.Workflow6, WorkflowLibrary"
                 objectUri="Server.WorkflowService.SayHello"/>
   <wellknown mode="SingleCall"
                 type="WorkflowLibrary.Workflow6, WorkflowLibrary"
                 objectUri="Server.WorkflowService.OneWay" />
  </service>
   <channels>
    <channel name="wf" timeout="40"
      type="RKiss.WorkflowRemoting.ClientWorkflowInvoker,
                        WorkflowRemoting, ..." />
    <channel ref="tcp" port="1234" >
     <serverProviders>
      <provider type="RKiss.WorkflowRemoting.ServerWorkflowInvoker,
                        WorkflowRemoting,..."/>
     </serverProviders>
   </channel>
  </channels>
 </application>
</system.runtime.remoting>

The specific workflow type can be registered by Remoting such as any WellKnown/SingleCall remoting object. For WCF service, the objectUri value must be corresponded to the service operation contract (in our example is Service.WorkflowService.SayHello or Server.WorkflowService.OneWay). For other connectivity such as Remoting, WSE, etc. we can use the same objectUri or the workflow type can be registered under another name.

Note, the above config snippet also shows the configuration of the local and remote Remoting connectivity. If the consumer of the Workflow6 is from the same appDomain, the wf channel (Null channel) can use it, otherwise the TCP remoting sink can handle the invoking of the workflow.

As you can see, the Workflow6 (in this config example) can be invoked by:

  • WCF service with any binding
  • Locally - direct via the wf custom remoting channel
  • Remotely like any remoting (MarshalByRefObject) object

The following example shows exposing the Workflow by the WCF Service and its activation from the XOML local/remote resource:

Image 10

Logical Workflow Context

The Workflow in the WF model represents the highest layer of the Designer. It is a container of the Activities. Using the built-in Communication Activities (or custom such as AdapterActivity - described in this article), the workflow can invoke another Workflow. In most cases, the workflows are hosted in the different appDomains across machine boundaries. For example: the Front-End Workflows and Back-End Workflows. With a WF Designer, we cannot design a Logical Workflow encapsulated into the physical Workflows based on the deployment model (tiers).

Now, we can say that the Logical Workflow represents one physical Workflow. From the business model point of the view, the Logical Workflow represents one business process with its own unique context. In some cases, the Logical Workflow can be implemented by the StateMachineWorkflow, where a state can handle a physical Workflow. The good example is an async processing, where a Logical Workflow is divided into three physical workflows (states) such as pre-processor, processor and post-processor.

The physical representing of the Logical Workflow is the object known as LogicalWorkflowContext (LWC), flowing together with a business data across all physical Workflows participating in the business process. The LWC represents a stateful information with a lifetime of the business process.

The following picture shows an example of the LWC flowing across the physical tiers:

Image 11

and the following code snippet shows an example of the LWC object implemented in this article:

C#
[Serializable]
[DataContract(Name="LogicalWorkflowContext", 
                Namespace="RKiss.WorkflowRemoting")]
[KnownType(typeof(HybridDictionary))]
public sealed class LogicalWorkflowContext : 
                ILogicalThreadAffinative,IDisposable
{
  #region DataMembers
  [DataMember]
  public DateTime CreatedDT
  {
      get { return _createdDT; }
      set { _createdDT = value; }
  }

  [DataMember]
  public Guid ContextId
  {
    get { return _contextId; }
    set { _contextId = value; }
  }

  [DataMember]
  public Guid WorkflowId
  {
    get { return _workflowId; }
    set { _workflowId = value; }
  }

  [DataMember]
  public Dictionary<STRING, object> WorkflowInitData
  {
      get { return _workflowInitData; }
      set { _workflowInitData = value; }
  }

  [DataMember]
  public string WorkflowInitDataKey
  {
    get { return _workflowInitDataKey; }
    set { _workflowInitDataKey = value; }
  }

  [DataMember]
  public Dictionary<STRING, object> Properties
  {
    get { return _properties; }
    set { _properties = value; }
  }
  #endregion

  // ... constructors, fields, methods
}

The LWC can flow across the built-in Remoting and WCF channels. Note, it is the physical channel's responsibility to pass the LWC object between the client and server thread slot (CallContext).

The following code snippet shows few examples of the LCC static class for handling a LogicalCallContext in different layers:

C#
// any .Net client originator
// application specific object in the context
LCC.LogicalWorkflowContext.Properties["MyTest"] = mySerializableObject;

// example 1: Workflow consumer
myObject = LCC.LogicalWorkflowContext.Properties["MyTest"];

// example 2: any WCF service consumer
// Set the wellknown headers specified by actor(s)
// into the CallContext Thread slot
LCC.SetDataContract(OperationContext.Current.IncomingMessageHeaders, actor);

// example 3: Remoting sink consumer
// Copy LogicalCallContext from remoting message into the
// CallContext Thread slot
LCC.CopyFrom(message);

Note, the Custom Remoting for Workflow will transparently handle passing of the LogicalCallContext across the boundaries. The above examples are used in the application layers out of the Logical Workflow and on the client side.

The LogicalWorkflowContext can also be used to initialize a Workflow. The following code snippet shows an example of how data can be attached to the context. Note, only one property can be used for this purpose.

C#
using (LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext)
{
  // workflow init data (example)
  HybridDictionary initData = new HybridDictionary();
  initData["abc"] = 12345;
  initData["myId"] = Guid.NewGuid();

  // attaching to the context
  lwc.WorkflowInitData["InitData"] = initData;

  // another data
  lwc.WorkflowInitData["Counter"] = 123;

  // endpoint scenarios
  string endpoint = "tcp://localhost:1234/myWorkflow5";

  // action (contract call on the myWorkflow5 workflowtype)
  response = WorkflowInvoker.Contract<ITest>(endpoint).SayHello(text));
}

Application specific workflow context

Any serializable object with an ILogicalThreadAffinative interface can flow across the workflow connectivity. The following code snippet shows an example of the LogicalTicket object declared in the application layer:

C#
[Serializable]
[DataContract(Name = "LogicalTicket", Namespace = "InterfaceContract")]
public class LogicalTicket : ILogicalThreadAffinative
{
  [DataMember]
  public string Msg
  {
      get { return _msg; }
      set { _msg = value; }
  }
  [DataMember]
  public Guid Id
  {
      get { return _id; }
      set { _id = value; }
  }
}

The application context objects are controlled by actor property at each boundary. The initiator boundary must assign an actor for the context travelling. On the other side, the consumer boundary must declare all actors across his boundary, for instance: CallContextActors = "InterfaceContract, myActor" will allow to flow context object from the boundary InterfaceContract and myActor.

Note, the list of actors must be created by consumer boundary (server side) for the object deserialization, therefore the actor represents an assembly of the context object type. In the above example, the LogicalTicket type is in the InterfaceContract assembly, so the deserializer can construct a qualified name for the type, such as "InterfaceContract.LogicalTicket,InterfaceContract".

Workflow activation (Create/Get or XOML)

The Logical Connectivity represented by ABC Endpoint needs to be, at some underlying level, mapped to the physical connectivity. The logical key of the mapping (known as a descriptor of the Logical Connectivity) is used to select a physical channel and transport media for delivering a message (for instances: IMessage and SoapMessage). At the server side, the logical endpoint represents an object which can be a remoting object, WCF Service, WSE, etc, and also the Workflow instance.

As I mentioned above, the Logical Connectivity uses a remoting paradigm for mapping a business layer to/from physical layer. The following code snippet shows a remoting section service for mapping a logical key (objectUri) to the type object. We can obtained a type of the object by using a RemotingServices in the host process. That is the concept between the remote object and its consumer.

XML
<system.runtime.remoting>
  <application>
    <service>
      <wellknown mode="SingleCall"
        type="WorkflowLibrary.Workflow5,WorkflowLibrary" 
                    objectUri="myWorkflow5"/>
      <wellknown mode="SingleCall"
        type="WorkflowLibrary.Workflow6, WorkflowLibrary" 
                    objectUri="myWorkflow6"/>
      <wellknown mode="SingleCall"
        type="WorkflowLibrary.Workflow7, WorkflowLibrary" 
                    objectUri="myWorkflow7"/>
    </service>
    <channels>
      <channel ref="tcp"/>
      <channel name="wf" timeout="40" callcontextActor="InterfaceContract"
        type="RKiss.WorkflowRemoting.ClientWorkflowInvoker, 
                    WorkflowRemoting,..."/>
    </channels>
  </application>
</system.runtime.remoting>

Let's look at the case, when the remote object is the Workflow. The WorkflowRuntime Core supports creating the Workflow instance from the following sources (represents workflow definition):

  • type object (generated programmatically or by Workflow Designer)
  • XOML formatted text

In the case of the Workflow Type, the activation of the Workflow (creating its instance , loading into the memory and start executing) is a default way for mapping the endpoint. The above configuration shows this case. Each unique endpoint has assigned one type object in the appDomain. Notice, the type can be duplicated between the endpoints, it can represent a Workflow, or any kind of object based on the built-in WorkflowInvoker features such as a XomlLoader - see more details in the following descriptions:

Let assume, that we have a product with many different types of business workflows. Instead of coding the workflow for each known product variant, we can create a Product Knowledge Base - metadata of the Business Workflows from End-To-End (we can call them as Logical Workflows) . This metadata will describe a presentation layer, connectivity and workflows. The Windows Workflow Foundation markup language is XOML.

The following picture shows very simple event driven workflow (OperationContractWorkflowBase), where the incoming message is forwarded to another Workflow by the Adapter activity in sync manner. The result of the Workflow outer is returned back to the client via a Return activity.

Image 12

The representation of the above workflow is shown in the following XOML file:

XML
<!-- workflow7.xoml -->
<ns0:OperationContractWorkflowBase x:Name="Workflow7"
  MethodName="SayHello"
  Type="{x:Type p2:ITest}"
  xmlns:p2="clr-namespace:InterfaceContract;Assembly=InterfaceContract"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:ns0="clr-namespace:RKiss.ActivityLibrary;Assembly=ActivityLibrary">

  <!-- workflow parameters-->
  <ns0:OperationContractWorkflowBase.Parameters>
    <WorkflowParameterBinding ParameterName="msg"/>
  </ns0:OperationContractWorkflowBase.Parameters>

  <!-- Adapter -->
  <ns0:AdapterActivity x:Name="Adapter"
  Uri="wf://localhost/MyWorkflow5"
  MethodName="SayHello"
  Type="{x:Type p2:ITest}">
  <ns0:AdapterActivity.Parameters>
    <WorkflowParameterBinding ParameterName="msg">
      <WorkflowParameterBinding.Value>
      <ActivityBind Name="Workflow7" Path="Parameters[&quot;msg&quot;].Value"/>
      </WorkflowParameterBinding.Value>
    </WorkflowParameterBinding>
      <WorkflowParameterBinding ParameterName="(ReturnValue)"/>
    </ns0:AdapterActivity.Parameters>
  </ns0:AdapterActivity>

  <!-- Return -->
  <ns0:ReturnActivity x:Name="Return"
    ConnectorActivityName="Workflow7">
    <ns0:ReturnActivity.Parameters>
      <WorkflowParameterBinding ParameterName="(ReturnValue)">
        <WorkflowParameterBinding.Value>
          <ActivityBind Name="Adapter" Path=
                "Parameters[&quot;(ReturnValue)&quot;].Value"/>
        </WorkflowParameterBinding.Value>
      </WorkflowParameterBinding>
    </ns0:ReturnActivity.Parameters>
  </ns0:ReturnActivity>
</ns0:OperationContractWorkflowBase>

The XOML didn't allow to declare the properties and field. This must be embedded in the component such as Activity. We have a generic, event driven workflow base class in our ActivityLibrary assembly. This class combines the sequential workflow with Connector Activity features in one class, which can be used as a workflow base class.

Using the OperationContractWorkflowBase class as a root XOML Activity, we can simplify a workflow definition resource in terms of the binding data. We can change the workflow behavior based on the application needs by modifying the XOML source.

Now, here is the question. How can the WorkflowInvoker handle a definition source such as type or XOML? Well, thanks to custom attribute fashion. In this case, we have a built-in the CreateWorkflowByXOMLAttribute class. When this attribute is found in the type object, the WorkflowInvoker will force an XOML loader.

The following code snippet shows a default option; where any class can be decorated with this attribute and passing the keys for sources such as source of the workflow definition (.xoml) and rules (.rules).

C#
[CreateWorkflowByXOML(WorkflowDefinitionKey="workflow7")]
public class Workflow7
{
}

The other advanced option is to have its own loader, for instance, database loader. In this case, the custom loader must inherit the IXomlLoader interface and implement its methods. Of course, the loader must be decorated with CreateWorkflowByXOML attribute as well. Based on the attribute's arguments and application specific data, the loader can deliver a Stream of the XOML definition and rules. The following code snippet shows a very simply loader, where the XOML resource is stored in the file system:

C#
//[CreateWorkflowByXOML]
[CreateWorkflowByXOML(WorkflowDefinitionKey="@workflow7")]
public class Workflow7 : IXomlLoader
{
  #region IXomlLoader Members
  public Stream GetWorkflowDefinition
            (string key, Dictionary<string,object> initData)
  {
    // file system resource
    return File.OpenRead(key);
  }
  public Stream GetWorkflowRules
            (string key, Dictionary<string,object> initData)
  {
    // no rules
    return null;
  }
  #endregion
}

Having the capability to create a workflow based on the metadata (XOML), we can create the business workflows based on the static knowledge base or dynamically from message payload. That's a very powerful mechanism for mapping the business model to the physical implementation - see the above example.

One last thing - activating a persisted workflow based on its instance ID. The WorkflowInvoker has a built-in feature as well. The WorkflowInstanceId can be passed from the business layer via a LogicalWorkflowContext. If this value is not empty, the WorkflowInvoker will call the WorkflowRuntime core for loading its instance into the memory. This scenario is suitable for long running state machines, where idle state can be unloaded by built-in SqlWorkflowPersistenceService service.

Concept and Design Implementation

The concept of the Logical Workflow Connectivity is based on the encapsulation of the physical connectivity from the application (business) layers. The Logical Ends represent an application abstraction for mapping a business data to/from a communication layer. This chapter will describe a concept and major implementation of each end such as client and server, where the client/server can be represented by Remoting, Workflow, WCF Service, etc. Technologies. Let me start with a client side.

1. Client Side

Invoking a Workflow is based on creating a remoting transparent proxy from the Interface Contract. The IMessage call can be dispatched to the endpoint based on the built-in schema provider by plumbing the Custom Channel in the Remoting paradigm. The following picture shows dispatching a remoting call for two destinations such as localhost and wcf connectivity:

Image 13

In the localhost case, where the client and workflow are in the same appDomain, the WorkflowInvoker helper class will create a workflow instance based on the endpointUri key and enquiring an IMessage object directly to the registered workflow queue. Note, the workflow queue is registered by <a href="#ConnectorActivity">ConnectorActivity</a> based on the Interface Contract. Each Operation Contract will have its own workflow queue, therefore the StateMachine can have more that one Connector in the State. The following code snippet shows a partial implementation of this local dispatcher:

Invoking local Workflow

Dispatching an IMessage call within the appDomain (Null Channel) is very straightforward. Based on the WorkflowInstanceId value (passed via a LogicalWorkflowContext), the WorkflowInvoker will create or get a workflow instance from the WorkflowRuntime core. Once we have the Invoker, the message can be sent to the workflow instance. The Response from the workflow is handled by LocalService (event driven mechanism) in this current implementation. I am planning to update the IMessage return object as well.

C#
if (_endpoint.StartsWith(WorkflowDefaults.WorkflowLocalhost))
{
  #region Invoke local Workflow
  LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext;
  Guid workflowInstanceId = lwc.GetAndClearWorkflowId();
  bool bGetWorkflowById = !workflowInstanceId.Equals(Guid.Empty);

  // create workflow
  Invoker invoker = null;
  if (bGetWorkflowById)
  {
     invoker = WorkflowInvoker.Create(workflowInstanceId);
  }
  else
  {
    string endpoint = this.GetObjectUri(_endpoint);
    Type workflowType = RemotingServices.GetServerTypeForUri(endpoint);
    invoker = WorkflowInvoker.Create(workflowType, lwc.WorkflowInitData);
  }

  // send remoting message
  invoker.SendMessage(mcm);

  // handle response
  if (RemotingServices.IsOneWay(mcm.MethodBase) == false)
  {
    invoker.WaitForResponse(_Sender.TimeOut);
    returnValue = invoker.GetResponse<object>();
  }
  #endregion
}

Invoking remote Workflow

The WCF Connectivity is the second built-in schema dispatcher in the Remoting Custom Channel. We can transparently provide a call to the WCF Service with any knowledge of the physical connectivity by having this schema. The implementation of this feature (such as creating a ChannelFactory2 and passing the LogicalWorkflowContext across the channel boundaries) challenged me for some time. The following code snippet shows this magic implementation:

C#
using(ChannelFactory2 factory=new ChannelFactory2
            (Type.GetType(mcm.TypeName), endpoint))
{
  // transparent proxy
  object tp = factory.CreateChannel();

  using (OperationContextScope scope = 
            new OperationContextScope(tp as IContextChannel))
  {
    // CallContext over the loosely coupled contract
    // (LogicalWorkflowContext and Unknown)
    object lwc = LCC.GetLogicalWorkflowContext;
    if (lwc != null)
    {
      MessageHeader header = MessageHeader.CreateHeader
                    ("LogicalWorkflowContext",
        "RKiss.WorkflowRemoting", lwc, false, "WorkflowRemoting");
      OperationContext.Current.OutgoingMessageHeaders.Add(header);
    }
    if (!string.IsNullOrEmpty(Sender.CallContextActor))
    {
      IDictionaryEnumerator enumerator = LCC.GetDataContract().GetEnumerator();
      while (enumerator.MoveNext())
      {
        // skip it
        if (enumerator.Value is LogicalWorkflowContext)
            continue;

        DataContractAttribute dca = enumerator.Key as DataContractAttribute;
        MessageHeader header = MessageHeader.CreateHeader(dca.Name,
          dca.Namespace, enumerator.Value, false, Sender.CallContextActor);
        OperationContext.Current.OutgoingMessageHeaders.Add(header);
      }
    }

    // action
    returnValue = factory.InvokeMethod(mcm.MethodName, mcm.Args);
  }
  factory.Close();
}

and of course, the following is the ChannelFactory2 class that creates a transparent proxy based on the type. Note, the WCF uses a generic ChannelFactory<> class to create a proxy, where the type needs to be known during the compilation process.

C#
public class ChannelFactory2 : ChannelFactory
{
  Type _channelType;
  IChannelFactory _factory;
  object _tp;

  public ChannelFactory2(Type channelType, string endpointConfigurationName)
  {
    if (!channelType.IsInterface)
        throw new InvalidOperationException
            ("The channelType must be Interface");
    _channelType = channelType;
    base.InitializeEndpoint(endpointConfigurationName, null);
  }

  public object CreateChannel()
  {
    // create transparent proxy
    base.EnsureOpened();
    object[] objArray = new object[] 
            { _channelType, base.Endpoint.Address, null };
    _tp=_factory.GetType().InvokeMember
            ("CreateChannel",BindingFlags.InvokeMethod | ...);
    return _tp;
  }

  protected override void OnOpen(TimeSpan timeout)
  {
    _factory = base.CreateFactory();
    this.GetType().BaseType.InvokeMember
            ("innerFactory", BindingFlags.SetField | ...);
    base.OnOpen(timeout);
  }

  protected override System.ServiceModel.Description.ServiceEndpoint 
                            CreateDescription()
  {
    ContractDescription desc = ContractDescription.GetContract(_channelType);
    return new ServiceEndpoint(desc);
  }

  public object InvokeMethod(string methodName, object[] args)
  {
    // invoke method on the transparent proxy
    if (_tp == null)
        throw new NullReferenceException("The channel is not created");
    object retVal=_tp.GetType().InvokeMember(methodName, 
                    BindingFlags.InvokeMethod |...);
    return retVal;
  }
}

2. Server Side

As I mentioned above, accessing the Workflow in the same appDomain is encapsulated into the WorkflowInvoker static class. This class is a wrapper layer to the Workflow namespace for handling a workflow activation, Request/Response event driven LocalService, SendMessage and etc. The WorkflowInvoker can be used programmatically in the web service, WCF service, remoting object, etc. to invoke a Workflow via built-in LocalService. This approach always requires hard-coding. However, there is another way, fully transparent invoking a Workflow in the loosely coupled manner. The following is a solution for remoting and WCF Service. Other services such as web or WSE can also be implemented using this design pattern. Let's look at an easy case first - a remoting server, where an IMessage stream is forwarded directly to the activated workflow instance.

Remoting endpoint

The remoting paradigm allows plugging a custom sink into the channel pipeline. In our case, the remoting message must be designated in the workflow instance and generates a response message back based on the Interface Contract. The following picture shows a concept of the Custom Sink:

Image 14

The implementation of the custom sink is straightforward using the WorkflowInvoker. Note, our Custom Sink must be the first sink in the channel pipeline (no formatter is used) in order to avoid checking the remote type object for MarshalByRefObject base class. This article supports only a binary formatted IMessage stream.

The following code snippet shows a major part of the custom sink implementation:

C#
if (_Next != null)
{
  if (requestMsg == null && requestStream != null)
  {
    // we are supporting only binary formatter
    requestMsg = (IMessage)bf.Deserialize(requestStream);
    requestStream.Close();
  }

  mcm = requestMsg as IMethodCallMessage;
  if (mcm == null)
    throw new NullReferenceException
            ("IMethodCallMessage after deserialization");

  // LogicalCallContext
  LCC.CopyFrom(mcm);
  LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext;
  Guid workflowInstanceId = lwc.GetAndClearWorkflowId();
  bool bGetWorkflowById = !workflowInstanceId.Equals(Guid.Empty);

   // create workflow
  Invoker invoker = null;
  if (bGetWorkflowById)
  {
     invoker = WorkflowInvoker.Create(workflowInstanceId);
  }
  else
  {
    string endpoint = mcm.Uri;
    if (string.IsNullOrEmpty(endpoint))
    {
      endpoint = requestHeaders["__RequestUri"] as string;
    }
    if (string.IsNullOrEmpty(endpoint))
      throw new NullReferenceException("Internal error - missing endpoint");

    // create workflow instance
    Type wfType = RemotingServices.GetServerTypeForUri
                        (endpoint.TrimStart('/'));
    invoker = WorkflowInvoker.Create(wfType, lwc.WorkflowInitData);
  }

  // send remoting message to the workflow
  invoker.SendMessage(mcm);

  // handle response
  if (!RemotingServices.IsOneWay(mcm.MethodBase))
  {
    // wait for response
    invoker.WaitForResponse(_Provider.TimeOut);
    object response = invoker.GetResponse<object>();

    // return message back to the caller
    // (Note that this version is not allow to change a CallContext!!)
    responseMsg = (IMessage)new ReturnMessage(response, null, 0, null, mcm);
  }
}

WCF Service endpoint

Windows Communication Foundation (WCF) Service is incoming a new programming model for tightly and loosely connectivity. The Service can be intercepted at any vertical layer such as Service, Endpoint, Operation and Parameter. For our WorkflowInvoker, the best place to intercept an Operation is shown in the following picture:

Image 15

Decorating an OperationContract by WorkflowInvokerAttribute, the operation behavior is changed based on our custom WorkflowOperationInvoker class. The incoming message from the WCF Transport will be forwarded into the IOperationInvoker.Invoke method. First of all, our invoker checks an option of the workflow activation. There are two choices: activation based on the WorkflowInstanceId obtained from the LogicalWorkflowContext (header block) and based on the type.

The workflow type can be declared directly in the operation attribute or indirectly using the remoting config section. Note that the operation contract name must be used for objectUri declaration (example: ServiceNamespace.MethodName).

Once we have a Workflow instance, the message can be sent into its workflow queue. Of course, we have to create the "remoting IMessage" object like it is generated by transparent proxy. For this purpose, the lightweight MethodMessage has been designed - see the source file.

The following code snippet shows a core of the WCF Invoker for Workflow:

C#
object IOperationInvoker.Invoke(object instance,
                                object[] inputs,
                                out object[] outputs)
{
  outputs = new object[0];
  object retVal = null;
  Invoker invoker = null;

  // set incoming objects in the CallContext
  LCC.SetDataContract(OperationContext.Current.IncomingMessageHeaders,
    this._callContextActors);
  LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext;

  // check the workflowId
  Guid workflowInstanceId = lwc.GetAndClearWorkflowId();
  bool bGetWorkflowById = !workflowInstanceId.Equals(Guid.Empty);

  // options for workflow type
  if (this._workflowType == null)
  {
    string endpointUri = string.Concat(instance.GetType().FullName, ".",
      this._description.SyncMethod.Name);
    this._workflowType = RemotingServices.GetServerTypeForUri(endpointUri);
  }
  if (!bGetWorkflowById && this._workflowType == null)
  {
    // throw exception or perform some pre-processing
    retVal = this._innerOperationInvoker.Invoke(instance,inputs,out outputs);

    // done (no workflow invoked)
    return retVal;
  }

  // create message operation
  MethodMessage message =
    new MethodMessage(this._description.SyncMethod, inputs, null);

  // workflow invoker options (stateful or stateless)
  if (bGetWorkflowById)
  {
    invoker = WorkflowInvoker.Create(workflowInstanceId);
  }
  else
  {
    invoker = WorkflowInvoker.Create(this._workflowType,lwc.WorkflowInitData);
  }

  // send message to the workflow queue
  invoker.SendMessage(message);

  // response
  if (!this._description.IsOneWay)
  {
    invoker.WaitForResponse(this.ResponseTime);
    retVal = invoker.GetResponse<object>();
  }

  // done
  return retVal;
}

That is all from the Invoker sides. Now, let me demonstrate how the IMessage is processed in the Workflow.

Workflow Connector

The Workflow Connector is a custom event driven Activity for receiving an IMessage object via a workflow queue created based on the Interface Contract. The message process is shown in the following code snippets. The message can be received by Execute or onEvent methods, depending on the workflow executor processing (sync vs. async scenario). Once the message is received, the ConnectorActivity is going to close, otherwise it will remain in the Executing process (waiting for message).

C#
protected sealed override
  ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
  if (executionContext == null)
      throw new ArgumentNullException("executionContext");

  WorkflowQueuingService queueService =
    executionContext.GetService<WorkflowQueuingService>();
  WorkflowQueue queue = queueService.GetWorkflowQueue(_queueName);
  queue.RegisterForQueueItemAvailable(this, this.QualifiedName);

  if (queue.Count > 0)
  {
      object item = queue.Dequeue();
      ProcessQueueItem(executionContext, item);
      queue.UnregisterForQueueItemAvailable(this);
      return ActivityExecutionStatus.Closed;
  }
  return ActivityExecutionStatus.Executing;
}

void IActivityEventListener<QueueEventArgs>.OnEvent
                (object sender,QueueEventArgs e)
{
  ActivityExecutionContext executionContext = 
                sender as ActivityExecutionContext;
  WorkflowQueuingService queueService =
    executionContext.GetService<WorkflowQueuingService>();
  WorkflowQueue queue = queueService.GetWorkflowQueue(e.QueueName);

  if (queue.Count > 0)
  {
    object item = queue.Dequeue();
    ProcessQueueItem(executionContext, item);
    queue.UnregisterForQueueItemAvailable(this);

    // done
    executionContext.CloseActivity();
  }
}

The incoming message is dequeued in the internal ProcessQueueItem method. The following code snippet shows its implementation, how the input arguments are collected by WorkflowParameterBindingCollection object declared as DependencyProperty with a Parameters name. Note, the return value must be assigned by ReturnActivity, otherwise the class validator will throw an exception.

C#
if (message.MethodName == this.MethodName)
{
  // LogicalCallConetext
  LCC.CopyFrom(message);

  // roles
  Helpers.ValidateRoles(this, message);

  WorkflowParameterBindingCollection collection = this.Parameters;
  if (collection != null)
  {
    bool flag = false;
    int ii = 0;
    MethodInfo mi = this.Type.GetMethod(this.MethodName);
    if (mi != null)
    {
      foreach(ParameterInfo pi in mi.GetParameters())
      {
        if (!pi.ParameterType.IsByRef && (!pi.IsIn || !pi.IsOut))
        {
          if (collection.Contains(pi.Name))
          {
            WorkflowParameterBinding binding = collection[pi.Name];
            binding.Value = message.InArgs[ii++];
          }
        }
        else
        {
            flag = true;
        }
      }
    }
  }
  OnReceived(EventArgs.Empty);
  base.RaiseEvent(ConnectorActivity.ReceivedEvent,this,EventArgs.Empty);
  return;
}

As I mentioned above, this article version assumes that the Interface Contract (the IMessage) has the input and return arguments declared, that's the current limitation of the Interface Contract. Notice, the ConnectorActivity is a full transparent "Endpoint" for physical connectivity such as Remoting, WCF Service and etc.

Workflow Adapter

The Adapter is a custom Activity to invoke an Endpoint object based on the Interface Contract in the transparent manner. Its functionality is decoupled into the override Execute method. The concept of the AdapterActivity class is based on the Remoting client, where the transparent proxy is created based on the Interface Contract and Endpoint logical address (URI).

The Adapter does not need to know how and where the remote object is located (it can be a Workflow Connector, as well). Based on the configuration file, the physical connectivity is declared by a custom or standard remoting channel. We can consume the Workflow, WCF Service or remoting object by using the custom channel such as wf and wcf.

The following code snippet shows the core of the Adapter where the call is processed by accessing a standard remoting object:

C#
protected override ActivityExecutionStatus Execute
                (ActivityExecutionContext aec)
{
  // intercept before the call
  base.RaiseEvent(AdapterActivity.InvokingEvent, this, EventArgs.Empty);
  OnMethodInvoking(EventArgs.Empty);

  #region Invoke remoting object
  MethodInfo mi = Type.GetMethod(this.MethodName, BindingFlags.Public | ...);
  object[] objArray = Helpers.GetParameters(mi, this.Parameters);
  WorkflowParameterBinding returnValueBinding = null;
  if (this.Parameters.Contains("(ReturnValue)"))
  {
    returnValueBinding = this.Parameters["(ReturnValue)"];
  }
  try
  {
    // endpoint
    string objectUri = this.Uri.StartsWith("@") ?
      ConfigurationManager.AppSettings[this.Uri.Substring(1)] : this.Uri;
    if (string.IsNullOrEmpty(objectUri))
      throw new Exception(string.Format("Missing endpoint in {0}",
                            this.QualifiedName));

    object proxy = Activator.GetObject(this.Type, objectUri);
    object retVal=Type.InvokeMember(this.MethodName, 
                    BindingFlags.InvokeMethod |...);
    if (returnValueBinding != null)
    {
      returnValueBinding.Value = retVal;
    }
  }
  catch (TargetInvocationException ex)
  {
    if (ex.InnerException != null)
    {
      throw ex.InnerException;
    }
    Trace.WriteLine(ex);
    throw;
  }
  catch(Exception ex)
  {
    Trace.WriteLine(ex);
    throw;
  }
  #endregion

  // intercept after the call
  base.RaiseEvent(AdapterActivity.InvokedEvent, this, EventArgs.Empty);
  OnMethodInvoked(EventArgs.Empty);

  // activity is done
  return ActivityExecutionStatus.Closed;
}

Hosting WorkflowRuntime

The Windows Workflow Foundation requires plugging its WorkflowRuntime core and services into the host process prior to its usage. This process is simplified by custom WorkflowHosting static class using two methods such as Attach and Detach. We can use them for any kind of host processes in the same way, it is shown in the following code snippet:

C#
static void Main(string[] args)
{
  ServiceHost host = null;
  try
  {
      // WCF services
      host = new ServiceHost(typeof(WorkflowService));
      host.Open();

      // Remoting
      string configFile = 
        AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
      RemotingConfiguration.Configure(configFile, false);

      // Workflow
      WorkflowHosting.Attach("WorkflowRuntimeConfig", "LocalServicesConfig");

      // processing
      Console.Write("Hit <ENTER> to exit server...\n");
      Console.ReadLine();
  }
  catch (Exception ex)
  {
      Console.WriteLine(ex.Message);
      Console.ReadLine();
  }
  finally
  {
      // clean-up
      WorkflowHosting.Detach();
      if (host != null)
          host.Close();
  }
}

The hosting flexibility is encapsulated into the config file. Here is one example:

XML
<configuration>
 <configSections>
  <section name="WorkflowRuntimeConfig"
    type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, ... "/>
  <section name ="LocalServicesConfig"
    type =
    "System.Workflow.Activities.ExternalDataExchangeServiceSection, ..."/>
  </configSections>

<WorkflowRuntimeConfig >
 <Services>
  <add type="System.Workflow.Activities.ExternalDataExchangeService, 
    System.Workflow.Activities,..."
    ConfigurationSection="LocalServicesConfig" />
  <!--<add type=
    "System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, ..."/>-->
  <!--<add 
    type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, ..."
    UnloadOnIdle="true" 
    ConnectionString="Initial Catalog=Storage;Data Source=localhost;"/>-->
  <!--<add type=
    "System.Workflow.Runtime.Tracking.SqlTrackingService, ..."
    UnloadOnIdle="true" ConnectionString=
    "Initial Catalog=Storage;Data Source=localhost;" />-->
  <add type=
    "RKiss.WorkflowRemoting.NonePersistenceService, WorkflowRemoting, ..."/>
 </Services>
</WorkflowRuntimeConfig>

<LocalServicesConfig>
 <Services>
  <!--<add type=
    "RKiss.WorkflowRemoting.InvokerLocalService, WorkflowRemoting, ..." />-->
 </Services>
</LocalServicesConfig>

</configuration><SPAN style="FONT-SIZE: 8pt">

WorkflowInvoker

The static custom WorkflowInvoker class represents an access layer to the Workflow. Its major feature is creating a workflow instance and its Invoker with a LocalService layer for Request/Response event driven message exchange. In addition, there is a method for sending an IMessage to the workflow queue. The WorkflowInvoker can be used in any application layer to programmatically handle communication with a Workflow. I will skip the description of this class as I hope the functionality can be understood from the commented source code.

Test

The implementation of the Workflow Adapter/Connector pair is divided into two major projects such as the WorkflowRemoting and ActivityLibrary. These two assemblies must be included in the application. The following picture shows these projects in the WorkflowRemoting solution:

Image 16

The WorkflowRemoting project contains all of the magic and reusable classes for handling hosting and communication with a Workflow via Remoting and WCF (aka Indigo). The other assembly, ActivityLibrary is a collection of custom communication Activities for incoming and outgoing message exchange based on the Interface Contract. These Activities are "neutral" for any kind of connectivity. There is one event driven workflow base class for any generic usage. It is very useful for the root Activity of XOML workflow.

You can include the WorkflowRemoting and Activity assemblies to your solution and modify your config files based on the Adapter/Connector connectivity, but I would recommend to step into the following test process. For this purpose, the solution includes a Test folder (see the below picture), that contains the client and server console programs, InterfaceContract and WorkflowLibrary with different workflows.

Image 17

Note, the access to your SQL database must be modified in both config files. In addition, there is a sample test that uses MSMQ connectivity, therefore the transactional queue is required. The name of the queue is Workflow.

The sample test can be selected in the Client's Program file based on the following endpoint:

C#
// endpoint scenarios for some kind of tests
string endpoint = "wf://localhost/myWorkflow5";  // #0: null remoting
//endpoint = "tcp://localhost:1234/myWorkflow5"; // #1: tcp remoting
//endpoint = "tcp://localhost:1234/myWorkflow6"; 
                    // #2: tcp remoting - state machine
//endpoint = "wf://localhost/myWorkflow6";       
                // #3: null remoting - state machine
//endpoint = "tcp://localhost:1234/myWorkflow4"; 
                // #4: tcp remoting with Adapter(W4->W5)
//endpoint = "wf://localhost/myWorkflow4";       // #5: null remoting
//endpoint = "wf://localhost/myWorkflow7";       
                // #6: null remoting - xoml workflow
//endpoint = "wcf://myWorkflow4";                // #7: wcf/workflow invoker

The remoting or WCF tests will require you to start the Server host process in prior. Let's start with a simple case, where a local Workflow5 (W5) is invoked by the client. This is a default test, therefore we don't need to change anything (no SQL or MSMQ are required)

Test #0:

This simple test will invoke an operation SayHello on the ITest contract at the specified endpoint (Workflow5).

Image 18

The Worklow5 functionality is very simple, the incoming message is returned back to the invoker. As you can see in the above Console screen, there are the workflow tracking process and Id of the LogicalWorkflowContext displayed. Note, the connectivity between the Workflow and the client is NULL Remoting channel.

We never have any problems with the typical HelloWorld demo, it always works. How about some more complicated connectivity where we can validate this concept, for instance: Remoting, WCF, MSMQ, Sequential and StateMachine, sync and async processing. That is the following test:

Test #7:

This test requires a private transactional MSMQ queue - Workflow. Please make sure you have it. In the client program file we need to uncomment an endpoint for test #7, recompile and start it after the Server console program.

This test invokes a Workflow4 behind the WCF Service. The Workflow4 has a Connector and two Adapters. Each Adapter will invoke another Workflow via its own connectivity specified in the Server config file.

The Workflow4.Adapter2 activity will invoke a StateMachine (W6) in the async transactional manner using a WCF connectivity with netMsmqBinding.

If you have access to the SQL Server, the test can persist into the database. For this purpose, please change the Client and Server configuration files.

Ok, now, launch the Server host console program and then the Client one. You should see on their screens the following processes:

Image 19

Image 20

For the full picture of the connectivity, the following picture shows a Workflow6 - StateMachine:

Image 21

That's all for the testing. You can do additional testing for different endpoints specified in the Client program file.

Conclusion

This article described a Logical Connectivity between the Workflows, Remoting and Services. Using the Adapter/Connector Activities, the Workflows can be connected to the Service Oriented Architecture in the transparent manner. On the other hand, the Null Remoting gives a great performance for invoking the Workflows within the same appDomain. Based on the deployment schema, the physical connectivity can be configured using a Remoting and WCF channels. Of course, there are no restrictions to build your own connectivity channel, for example WSE.

To finish, I would like to say many thanks to WF Gallery of Samples (especially the WFWebPage.sln) and to the Reflector for help with the implementation of this article.

Appendix A - Firing Workflow Event

This feature has been added in the version 1.1 to enable sending an event message from the local/remote client directly to the HandleExternalEventActivity in the loosely coupled fire & forget manner.

The concept is based on mapping two interface contracts such as communication and workflow in one direction using the WorkflowInvokerAttribute. In the case of the non-correlated events, the connectivity to the workflow doesn't require to implement an ExternalDataExchangeService. Note, the WorkflowInvoker can create the workflow instance based on the type or instance id.

The following picture shows a test case of firing an event between the remoting and state machine:

Image 22

The local/remote client in the above scenario is firing the "virtual remote object (workflow)" defined by the ITest interface contract on the method Done. This method is decorated by WorkflowInvokerAttribute with specific mapping properties for ExternalDataExchange interface contract. The mandatory property is the EventType.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here