Contents
Notes
This article was written using:
- .NET Framework 3.0 (RTM)
- VS 2005 Extensions for Windows Workflow Foundation (RTM)
- 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, ...
- 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
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:
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.
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:
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 appDomain
s. The following picture shows this case:
Case 2. - the components are hosted in different appDomain
s across the process boundary.
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:
-
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.
Note, the Interface Type can be selected by the BrowseEditor
(with an interface filter) that is shown in the following picture:
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.
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.
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/myWorkflow | Null channel for invoking a Workflow in the same appDomain |
wcf://myClientEndpointName | Windows 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, ...
|
@myConfigName | this 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:
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:
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:
[ServiceBehavior(IncludeExceptionDetailInFaults=true)]
public class WorkflowService : ITest
{
[WorkflowInvoker]
public string SayHello(string msg)
{
throw new NotImplementedException();
}
[WorkflowInvoker(WorkflowType = typeof(WorkflowLibrary.Workflow6)]
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:
<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:
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 appDomain
s 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:
and the following code snippet shows an example of the LWC object implemented in this article:
[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
}
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:
LCC.LogicalWorkflowContext.Properties["MyTest"] = mySerializableObject;
myObject = LCC.LogicalWorkflowContext.Properties["MyTest"];
LCC.SetDataContract(OperationContext.Current.IncomingMessageHeaders, actor);
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.
using (LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext)
{
HybridDictionary initData = new HybridDictionary();
initData["abc"] = 12345;
initData["myId"] = Guid.NewGuid();
lwc.WorkflowInitData["InitData"] = initData;
lwc.WorkflowInitData["Counter"] = 123;
string endpoint = "tcp://localhost:1234/myWorkflow5";
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:
[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.
<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.
The representation of the above workflow is shown in the following XOML file:
<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">
<ns0:OperationContractWorkflowBase.Parameters>
<WorkflowParameterBinding ParameterName="msg"/>
</ns0:OperationContractWorkflowBase.Parameters>
<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["msg"].Value"/>
</WorkflowParameterBinding.Value>
</WorkflowParameterBinding>
<WorkflowParameterBinding ParameterName="(ReturnValue)"/>
</ns0:AdapterActivity.Parameters>
</ns0:AdapterActivity>
<ns0:ReturnActivity x:Name="Return"
ConnectorActivityName="Workflow7">
<ns0:ReturnActivity.Parameters>
<WorkflowParameterBinding ParameterName="(ReturnValue)">
<WorkflowParameterBinding.Value>
<ActivityBind Name="Adapter" Path=
"Parameters["(ReturnValue)"].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).
[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:
[CreateWorkflowByXOML(WorkflowDefinitionKey="@workflow7")]
public class Workflow7 : IXomlLoader
{
#region IXomlLoader Members
public Stream GetWorkflowDefinition
(string key, Dictionary<string,object> initData)
{
return File.OpenRead(key);
}
public Stream GetWorkflowRules
(string key, Dictionary<string,object> initData)
{
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.
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:
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.
if (_endpoint.StartsWith(WorkflowDefaults.WorkflowLocalhost))
{
#region Invoke local Workflow
LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext;
Guid workflowInstanceId = lwc.GetAndClearWorkflowId();
bool bGetWorkflowById = !workflowInstanceId.Equals(Guid.Empty);
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);
}
invoker.SendMessage(mcm);
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:
using(ChannelFactory2 factory=new ChannelFactory2
(Type.GetType(mcm.TypeName), endpoint))
{
object tp = factory.CreateChannel();
using (OperationContextScope scope =
new OperationContextScope(tp as IContextChannel))
{
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())
{
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);
}
}
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.
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()
{
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)
{
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:
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:
if (_Next != null)
{
if (requestMsg == null && requestStream != null)
{
requestMsg = (IMessage)bf.Deserialize(requestStream);
requestStream.Close();
}
mcm = requestMsg as IMethodCallMessage;
if (mcm == null)
throw new NullReferenceException
("IMethodCallMessage after deserialization");
LCC.CopyFrom(mcm);
LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext;
Guid workflowInstanceId = lwc.GetAndClearWorkflowId();
bool bGetWorkflowById = !workflowInstanceId.Equals(Guid.Empty);
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");
Type wfType = RemotingServices.GetServerTypeForUri
(endpoint.TrimStart('/'));
invoker = WorkflowInvoker.Create(wfType, lwc.WorkflowInitData);
}
invoker.SendMessage(mcm);
if (!RemotingServices.IsOneWay(mcm.MethodBase))
{
invoker.WaitForResponse(_Provider.TimeOut);
object response = invoker.GetResponse<object>();
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:
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:
object IOperationInvoker.Invoke(object instance,
object[] inputs,
out object[] outputs)
{
outputs = new object[0];
object retVal = null;
Invoker invoker = null;
LCC.SetDataContract(OperationContext.Current.IncomingMessageHeaders,
this._callContextActors);
LogicalWorkflowContext lwc = LCC.LogicalWorkflowContext;
Guid workflowInstanceId = lwc.GetAndClearWorkflowId();
bool bGetWorkflowById = !workflowInstanceId.Equals(Guid.Empty);
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)
{
retVal = this._innerOperationInvoker.Invoke(instance,inputs,out outputs);
return retVal;
}
MethodMessage message =
new MethodMessage(this._description.SyncMethod, inputs, null);
if (bGetWorkflowById)
{
invoker = WorkflowInvoker.Create(workflowInstanceId);
}
else
{
invoker = WorkflowInvoker.Create(this._workflowType,lwc.WorkflowInitData);
}
invoker.SendMessage(message);
if (!this._description.IsOneWay)
{
invoker.WaitForResponse(this.ResponseTime);
retVal = invoker.GetResponse<object>();
}
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).
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);
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.
if (message.MethodName == this.MethodName)
{
LCC.CopyFrom(message);
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:
protected override ActivityExecutionStatus Execute
(ActivityExecutionContext aec)
{
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
{
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
base.RaiseEvent(AdapterActivity.InvokedEvent, this, EventArgs.Empty);
OnMethodInvoked(EventArgs.Empty);
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:
static void Main(string[] args)
{
ServiceHost host = null;
try
{
host = new ServiceHost(typeof(WorkflowService));
host.Open();
string configFile =
AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
RemotingConfiguration.Configure(configFile, false);
WorkflowHosting.Attach("WorkflowRuntimeConfig", "LocalServicesConfig");
Console.Write("Hit <ENTER> to exit server...\n");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
finally
{
WorkflowHosting.Detach();
if (host != null)
host.Close();
}
}
The hosting flexibility is encapsulated into the config file. Here is one example:
<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=
"RKiss.WorkflowRemoting.NonePersistenceService, WorkflowRemoting, ..."/>
</Services>
</WorkflowRuntimeConfig>
<LocalServicesConfig>
<Services>
</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.
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:
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.
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:
string endpoint = "wf://localhost/myWorkflow5";
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
).
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:
For the full picture of the connectivity, the following picture shows a Workflow6
- StateMachine
:
That's all for the testing. You can do additional testing for different endpoints specified in the Client program file.
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.
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:
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
.