Publishing WF Workflows as REST Services
It was possible to build
REST consumer programs into WF 3.0 workflows via Code Activities or
custom activities, but you had to deal with the primitives in the System.Net namespace to form HTTP requests and parse the responses.
Publishing REST services in WF 3.0 was challenging because ASMX only supported SOAP services. It was possible to write HttpHandlers to accept REST requests, parse URLs, deserialize HTTP bodies and then invoke workflows based on the use of ExternalDataExchange; however, that approach required a fair amount of low level development.
WCF in .NET 3.5 added the “Web Programming Model” to bring the SOA programming model to REST endpoints. The webHttpBinding abstracted REST endpoints in the same way basicHttpBinding allowed access to SOAP endpoints. Attributes like WebGet and WebInvoke
could annotate service contracts to shape URLs for REST requests to
send request parameters embedded in the URL instead of having to send
SOAP bodies.
You were able to combine
.NET 3.5 Workflow Services with the Web Programming Model for publishing
or consuming REST services with WF. WCF represents REST contracts very
similar to SOAP contracts (as an interface with annotations):
Example 16.
[ServiceContract(SessionMode = SessionMode.NotAllowed)] public interface IRESTCustomer { [OperationContract] [WebInvoke(Method = "POST", UriTemplate = "customers/{cid}")] Customer GetCustomer(string cid); }
|
In addition to the already familiar *Contract attributes, the contract in the preceding example includes the REST-specific attribute WebInvoke,
which binds an interface to a URL and an HTTP method. The base URL is
supplied by the binding and parameters are either passed as part of the
URL or as URL parameters. This example defines the GetCustomer operation
as a POST request to <baseurl>/customers/<cid> (for example http://services.example.org/CustomerService/customers/1234), based on the UriTemplate in the attribute.
The WCF Send and Receive
messaging activities can consume the REST service contract just like a
SOAP contract. You select an operation from the ServiceContract using the ServiceOperationInfo property dialog for the activities (Figure 15).
Services hosted in IIS require a .svc file to load the workflow through the WorkflowServiceHostFactory since workflows don’t explicitly implement the service contract by deriving from the contract interface. The WorkflowServiceHost infers the contract from the Receive activities.
The .svc file for the previous example could look like this:
Example 17.
<%@ ServiceHost Factory="System.ServiceModel. Activation.WorkflowServiceHostFactory" Service="WF35Services.RESTCustomerService" %>
|
Note the Factory attribute pointing to the WorkflowServiceHostFactory and the Service attribute identifying the workflow type implementing the service.
Self-hosted services are loaded into a WorkflowServiceHost through the API and don’t require the .svc file. Configuring an endpoint for REST communication requires the webHttpBinding and the webHttp endpoint behavior:
Example 18.
<system.serviceModel> <services> <service name="WF35Services.RESTCustomerService"> <endpoint address="" binding="webHttpBinding" contract="StandardMold.QuoteProcess.IRESTTest2" behaviorConfiguration="REST"> </endpoint> </service> </services> <behaviors> ... <endpointBehaviors> <behavior name="REST"> <webHttp/> </behavior> </endpointBehaviors> </behaviors> </system.serviceModel>
|
JSON Encoding
WCF expects XML messages, even with the REST binding. You can configure the contract for JSON encoding by adding the enableWebScript behavior.
Example 19.
<endpointBehaviors> <behavior name="REST"> <webHttp/> <enableWebScript/> </behavior> </endpointBehaviors>
|
Unfortunately, this behavior conflicts with the UriTemplate property. As a work-around, you can set the ResponseFormat property to WebMessageFormat.JSON.
Example 20.
[OperationContract] [WebInvoke(Method = "GET", UriTemplate = "customers/{cid}", ResponseFormat=WebMessageFormat.Json)] Customer GetCustomer(string tid, string msg);
|
Send and Receive Activity Configuration
Configuring the Send
activity to retrieve data from REST services follows the same steps as
configuring the Receive activity. The only difference is the
configuration of the endpoint. The Send activity exposes a ChannelToken property that can reference the client endpoint configuration in the .config file.
ChannelToken requires
a unique name and the name of an endpoint configuration. If you’re
sharing a channel, for example, a proxy to the server for context or
performance reasons, then you can also set the OwnerActivityName property.
The configuration in Figure 16 references a RESTCustomerService endpoint that can be configured in detail in the .config file:
Example 21.
<system.serviceModel> <client> <endpoint address="http://localhost:8000/Customers" binding="webHttpBinding"contract= "StandardMold.QuoteProcess.ICustomerCollection" behaviorConfiguration="rest"name="RESTCustomerService"/> </client> <behaviors> <endpointBehaviors> <behavior name="rest"> <webHttp /> </behavior> </endpointBehaviors> </behaviors> </system.serviceModel>
|
Note
It’s
technically possible to build long-running REST services with WF and
WCF. However, because REST explicitly prohibits operations that rely on
multiple HTTP requests, the limitations around message correlation with
WF and WCF don’t impact the implementation of a RESTful orchestrated
task service. An incoming request is always to be serviced by a new
instance of a workflow.
Orchestrated Task Services with REST and WF 4.0
WF 4.0 infers ServiceContracts
instead of linking operation contracts directly to Send and Receive
activities. This approach works for SOAP messaging and RPC style
services, but it takes away the extension points for the REST WebInvoke and WebGet attributes. With WF 4.0, REST services are consumed by building custom activities invoking WCF directly.
The following example shows a simple activity calling a REST service.
Example 22.
public sealed class GetRESTCustomer : CodeActivity { public InArgument<string> Cid { get; set; } public OutArgument<Customer> CustomerResponse { get; set; } protected override void Execute(CodeActivityContext context) { using (WebChannelFactory<IRESTCustomer> cf = new WebChannelFactory<IRESTCustomer> ("RESTCustomerService")) { IRESTCustomer channel = cf.CreateChannel(); context.SetValue<Customer> (CustomerResponse, channel.GetCustomer (context.GetValue<string>(Tid))); } } }
|
The definition for IRESTCustomer can contain WebGet or WebInvoke attributes that define the request shape and encoding. The WebChannelFactory
references an endpoint configuration stored in the App.Config file to
externalize behavior details. This simple class includes everything
required to invoke a REST service from a WF 4.0 workflow (Figure 17).
REST relies on state
messaging and prohibits operations that require receiving multiple
messages to execute. Thus, a REST service operation can receive a
service request, create a new workflow instance, and pass the received
parameters to the new instance. WF 4.0 doesn’t have a Receive activity,
but you can easily implement the workflow instantiation logic in code
and implement the necessary service logic in a workflow.
This next example
illustrates the bootstrapping of workflow instances in code. Production
quality implementations would likely include parameter validation, error
handling, security checks, and other functionality. In cases where the
WorkflowInvoker’s functionality is too limited, the WorkflowApplication
provides a more capable alternative.
Example 23.
public string GetCustomerData(string cid) { WF4UtilityActivities.RESTservice svc = new WF4UtilityActivities.RESTservice(); svc.Cid = cid; IDictionary<string, object> result = WorkflowInvoker.Invoke(svc); return (string)result["Result"]; }
|
WF
4.0 exposes input arguments as properties on the workflow class. Custom
activities could also return a strongly typed value. XAML-based
workflows return a dictionary of named return parameters.
Note also that the
implementation of the REST operation includes no REST-specific code.
This implementation can therefore be exposed as a REST service or a SOAP
endpoint for different service consumers.