This chapter focuses on theWindows Communication Foundation (WCF) support that is provided by Windows Workflow Foundation (WF). This broad category of WCF support falls under the moniker of workflow services. Included with this support is the ability to declaratively author WCF services using WF as well as to invoke WCF services from within a workflow.
The chapter begins with a brief introduction of workflow services and WCF followed by an in-depth discussion of the WF classes and activities that you will use to declare and invoke WCF services. The centerpiece example of this chapter is an order processing service that simulates an ecommerce transaction. The example begins with a relatively simple request/response service operation. This first basic example is used to demonstrate how to initially test the service, how to publish the service to Internet Information Services (IIS), how to develop a self-hosting application for the service, and how to implement nonworkflow and workflow client applications that invoke the service.
In Chapter 10, you will expand on this basic example by implementing additional workflow services that are consumed by this original workflow.
WF provides the ability to author workflows that use WCF to communicate. Using WF, you can author WCF services that are fully implemented as declarative workflows. These services can be hosted by a centralized Microsoft environment such as IIS or WAS, or they can be self-hosted by an application that you develop yourself. Once hosted, these services can be consumed by any WCF client. Depending on the WCF bindings that you use, the services can also be consumed by web service clients. As is true for all WCF services, the WCF client is unaware of the implementation details of the service. In the case of WF workflow services, the client is not necessarily aware that it is communicating with a workflow instance.
In addition to authoring new WCF services, you can also use WF to consume existing WCF or web services. WF provides standard activities that allow you to declaratively send messages to WCF services as well as handle any response that is part of the service contract. The services that you consume need not be implemented using WF—they can also be implemented in traditional procedural languages.
Before diving deeply into the WF support for WCF services, the next section provides a brief overview of WCF. It should provide you with just enough WCF background information to allow you to effectively consume and author services in WF. If you are an experienced WCF developer, you will already be familiar with the concepts that are presented.
Note The goal of the next section is to provide an overview of how WCF works from the standpoint of traditional non-WF applications. The basic concepts that are discussed also apply to services that are implemented using WF. However, some of the tasks needed to implement a service are not necessary when you are implementing a service with WF.
In this section, I provide a brief overview of the key WCF concepts. Understanding just a few WCF concepts will provide the background that you’ll need to implement and use WF workflow services.
Note WCF is a rich and complex Microsoft foundation that is at least as substantial as WF. In an effort to keep the focus of this book on WF, I’ve kept this overview as concise and high-level as possible. The information that I present here barely scratches the surface of WCF, and entire books have been written on the subject. If you plan on making extensive use of workflow services, you might want to purchase one of the many good WCF books, such as Pro WCF: Practical Microsoft SOA Implementation from Apress, that provide more in-depth coverage.
WCF is the enabling Microsoft technology for building service-oriented applications that was introduced in .NET 3.0. It provides a unified programming model for applications that are providers or consumers of contract-based services. WCF is really a superset of previous communication technologies including web services, Microsoft Message Queuing (MSMQ), and plain old sockets. It doesn’t replace any of these technologies, since they are all still available for use in your applications. But it does replace multiple communication APIs with a single one that can simultaneously target different transport mechanisms.
When using WCF, you implement an application using a single unified model and API. WCF decouples the transport mechanism from the service contract. This allows you to change the transport mechanism without affecting application code.
At its core, WCF is a framework for sending messages between systems. It is not a distributed component-based framework like remoting or Distributed COM (DCOM). When you are working with WCF (including when you are working with workflow services), this is an important distinction to keep in mind. When you use WCF to consume a service, you are exchanging a request message and (if applicable) a response message. The exact type and number of objects that are instantiated by the service provider to handle your request are not under your direct control (and really shouldn’t be your concern). In contrast with this, mechanisms like .NET remoting give you the ability to directly create instances of objects on a remote system. With remoting, you have intimate knowledge of the type of each object that you create as well as complete control over the object lifetime.
All WCF services define a service contract that identifies one or more operations. In a non-WF service implementation, the service contract usually takes the form of a C# interface that is decorated with the ServiceContract
attribute. Each service method defined in the interface is decorated with the OperationContract
attribute to identify it as a service operation.
Note When declaring WCF services using WF, the service contract and operation are inferred from properties of the messaging activities. There is no need to explicitly define the service contract using a C# interface.
Service operations can pass or return simple built-in types (for example, integers or strings) or complex types that you define. You define each complex type as you normally would a C# class. However, the class can be decorated with the DataContract
attribute to identify it as part of the data contract for an operation. The data contract simply describes the data that is transmitted between the client and server and serialized by the DataContractSerializer
(the default serializer used by WCF).
Data contracts are defined on an opt-in basis. When defining a data contract, you must explicitly select the members that you want to include in the contract. You do this by applying the DataMember
attribute to a member of the class. Since you explicitly identify the members that are serialized as part of the data contract, you can choose to include any combination of public and private members.
WCF is all about messages, and it supports several well-established message exchange patterns:
- Request/response: With this message exchange pattern, a client sends a request message to the service and then expects (and waits for) a response message. This is the most common message exchange pattern since it nicely maps to service operations that accept parameters and return a result.
- One-way: Operations that use this pattern do not return a response message. They are considered fire-and-forget operations since the client receives no direct feedback concerning the success or failure of the operation.
- Duplex: With this message exchange pattern, a client initiates contact with a service and provides the service with a direct callback address to the client. The service uses the callback address to send messages (most typically one way) back to the client.
After defining all the necessary contracts, a service class is developed that provides an implementation for all the operations of the service contract. The service itself takes the form of a normal C# class that implements the service interface. Additional attributes such as the ServiceBehavior
attribute can be applied to the service class to control aspects of the runtime service behavior.
Note When you are using WF to implement a service, there is no need for a service class. The implementation of the WCF service is fully provided in a declarative way by a workflow service definition.
A WCF endpoint identifies a location where messages are sent. Each WCF service is exposed as one or more endpoints. For instance, the same service can be simultaneously exposed to web service clients using HTTP and to local clients using TCP. In both cases, the same service implementation is used, but it is exposed as two separate endpoints, each with their own set of capabilities and behaviors.
Each endpoint is composed of an address, a binding, and the contract. The address is a Uniform Resource Identifier (URI) that identifies the location that clients use to invoke operations on the service contract. The address takes different forms depending on the transport being used for the endpoint. For instance, a service that is exposed to web service clients using HTTP might specify the address like this:
http://www.myserverhost.com:8000/MyServices/SampleService1
The same service exposed to TCP clients might use an address such as this:
net.tcp://myserverhost:9876/MyServices/SampleService1
The contract that is associated with the endpoint is the service contract that was discussed in the previous section. It defines all the operations that are available from the service.
The binding describes how the messages are exchanged between systems using this endpoint. It is a named stack containing the transport, encoding, and protocols that control the way WCF processes inbound and outbound messages on an endpoint. At a minimum, the binding identifies the transport protocol (for example, HTTP, TCP, MSMQ) and the encoding (for example, Text, Binary). The encoding determines the form that serialized messages take across the wire.
In addition, a binding can include a number of other capabilities or protocols that are provided with WCF or developed on your own. For example, a binding can also specify the type of security that is used (or not used), security credentials, reliable sessions, transaction support, throttling behavior (to limit the size of messages processed), and so on.
WCF includes a number of system-provided bindings that you can use, configure, or extend (for example, BasicHttpBinding
, WSHttpBinding
, NetTcpBinding
). Each system-provided binding comes with a default set of capabilities and behaviors that are enabled, but the defaults can usually be configured to meet your needs. And you always have the option of building your own stack of capabilities and behaviors to create a custom binding that meets your exact needs.
WCF services can be self-hosted by your own .NET application, by Internet Information Services (IIS), Windows Process Activation Service (WAS), or Windows Server AppFabric.
To configure and host a service, you need to tie together all the elements that were just reviewed. These elements include the service implementation (the service class), the contract (the interface), an endpoint (address and transport), and a binding (capabilities and behaviors). The configuration of the service can be accomplished using application configuration files (App.config
or Web.config
) or in code if you are self-hosting.
Clients can consume the service in a couple of ways. One option is to create a set of client proxy classes based on the service metadata. To accomplish this, you must configure the service so that it exposes the service metadata. This metadata provides a complete description of the service contract (along with any data contracts used by the service) in a standard format. This metadata is then consumed by clients (or utilities) to understand how to use the service. In the Microsoft WCF world, the svcutil.exe
utility (ServiceModel Metadata Utility) generates the necessary client proxy classes from the exposed metadata.
In addition to using a generated proxy, a client application can also make use of the WCF ChannelFactory
class to invoke the service. To do this, the ChannelFactory
references the interface that defines the service contract.
In this section, I provide the background information that you need to know in order to implement and use workflow services. In Chapter 10, I expand on this basic information to discuss more specialized topics. I begin with an overview of the messaging activities that are included with WF.
WF provides a set of standard messaging activities that you use to implement a workflow service or to declaratively consume a WCF service within a workflow. These activities are used to implement a workflow service:
Receive
SendReply
And these activities are used when you need to consume a WCF service from within a client workflow:
Send
ReceiveReply
All the standard WCF message exchange patterns (request/response, one-way, duplex) can be implemented using this standard set of messaging activities. Each of these activities is briefly discussed in the sections that follow.
The Receive
activity defines a single service operation that can be consumed by WCF client applications. If a workflow implements multiple service operations, you would declare a separate Receive
activity for each operation. The request parameters for each operation are defined using the Receive.Content
property.
Here are the most important properties of the Receive
activity:
If you are modeling a request/response operation, a Receive
activity would be paired with a SendReply
activity to send the response message. Any business logic that is necessary to implement the service operation would be declared between the Receive
and SendReply
activities.
If you are modeling a one-way operation, there is no need for additional messaging activities to implement the operation. Once the client application makes the one-way request, there is no expectation of a response.
Valid service contract names include a namespace and a contract name (typically specified as an interface name). For example, http://bukovics.com/IMyService
is a fully qualified service contract name. If you don’t specify a namespace, http://tempuri.org
is assumed. The ServiceContactName
property is defined as an XName
, which provides the means to define the namespace and contract name of the service contract.
The CanCreateInstance
property is especially significant. If it is your intent to create a new workflow instance each time the operation is invoked, you need to set this property to true. Otherwise, the service operation can be invoked only on an existing workflow instance.
The KnownTypes
property allows you to optionally specify additional types that may need to be serialized during the operation. This property is typically used when the content for the operation defines a base type that has multiple derived types. You would specify the derived types using the KnownTypes
property. This informs the serializer that these additional types might be passed as part of the content.
The correlation-related properties are used to manually control correlation for the service operation. Please refer to the sections on correlation in Chapter 10 for more information on how to use these properties.
The SendReply
activity provides the implementation for the second half of a request/response operation. It can never be used by itself. It can be paired only with a Receive
activity to send the response for the service operation.
Here are the most important properties of the SendReply
activity:
The Request
property identifies the Receive
activity that initiated the operation (the request). This property is set for you when a ReceiveAndSendReply
activity template is added to the workflow. The workflow designer and property editor does not provide a way to manually set this property.
A request/response operation requires a coordinated pair of activities (Receive
, SendReply
) to complete the operation. Because this is a common messaging pattern, an activity template is provided that preconfigures this combination of activities.
When you add a ReceiveAndSendReply
activity template to a workflow, three activities are actually added. A Sequence
activity is added as the container for a Receive
and SendReply
activity. The SendReply.Request
property references the Receive
activity to logically link the two activities.
The Send
activity is used to declaratively invoke a service from within a workflow or custom activity. The service that you are invoking may have been implemented using WF, but that is not a requirement.
Here are the most important properties of the Send
activity:
If you are invoking a request/response operation, a Send
activity would be paired with a ReceiveReply
activity to receive the response from the service. If you are invoking a one-way operation, the Send
activity is used by itself without any additional messaging activities.
The three endpoint-related properties provide three different ways to specify the endpoint of the operation that you are invoking. Only one of these properties needs to be specified for any single Send
activity.
The ReceiveReply
activity receives the response from a request/response operation. It cannot be used by itself and must be paired with a Send
activity.
Here are the most important properties of the ReceiveReply
activity:
The SendAndReceiveReply
activity template is used to declare the activities that consume a request/response operation. When you add a SendAndReceiveReply
activity template to a workflow, a set of Sequence
, Send
, and ReceiveReply
activities are added. The template sets the ReceiveReply.Request
property to logically relate the activity to the Send
activity that initiates the request.
To invoke a WCF service operation, a message is sent to an endpoint for the service. If the operation uses the request/response message exchange pattern, another message is returned to the original caller as the response. WCF (and WF workflow services) supports two ways to describe these messages:
- Data contract. A high-level, opt-in approach to defining the data that is exchanged in a service message.
- Message contract. A single class that describes the low-level shape of the message header and body.
These two message types have slightly different characteristics, but in general, you can use either type. A data contract uses classes that are decorated with the DataContract
attribute. Individual members of the class that you want to include in the message are decorated with the DataMember
attribute. One or more of these classes can be included as individual parameters that are passed with the message. The parameters can also be primitive types (such as a string, float, or integer) or a collection of objects or values. A data contract can be likened to a method of a C# class that is passed multiple arguments. Internally, WCF does some wrapping of the individual parameters to create a single message.
In contrast with this, a message contract uses a single argument to represent the entire message. That argument can be a primitive type, or it can be a C# class that defines the individual properties of the message. Since a message contract is limited to a single argument, it avoids the additional layers of wrapping when the message is created. This usually results in a more concise XML representation of the message. A message contract also provides more control over the shape of the resulting message. For example, you can apply attributes to the properties of the message class that determine which properties are sent as message headers (MessageHeader
attribute) versus the message body (MessageBodyMember
attribute).
When you declare a new service operation, you determine the message type using the Content
property of the Receive
activity. The Content Definition dialog that you use to set this property presents you with a choice of Message or Parameters. This can be seen in Figure 9-1 shown later in this chapter. If you select the Message option, you are declaring a WCF message contract and are limited to a single argument that completely defines the message. If that single argument is a class, you can optionally decorate it with the MessageContract
, MessageHeader
, and MessageBodyMember
attributes to fine-tune the exact shape of the message. If you select the Parameters content option, you are declaring a WCF data contract that supports single or multiple arguments. If an argument is a class, you can optionally decorate it with the DataContract
and DataMember
attributes to control which members are included in the message.
If the operation returns a response, the Content
property of the SendReply
activity determines the message type of the response.
Warning A class that is decorated with the MessageContract
attribute and other attributes related to message contracts cannot be used if you select the Parameters content option. However, the inverse is not true. A class that is decorated with the DataContract
and DataMember
attributes can be used for either content type (Message or Parameters).
When you are the consumer of a service operation, the Content
property of the Send
and ReceiveReply
activities must match the signature of the operation being invoked. For example, if the service operation you are invoking uses a data contract, then you must do the same when you set the Content
property of these activities.
Regardless of the message type, complex types such as a C# class can be included in the message. By default, all public fields and properties of the class are serialized (using the DataContractSerializer
) and included in the message.
However, you can determine which members are included and how they are serialized by applying a set of attributes to the class. Applying the DataContract
attribute to the class allows you to control aspects of the contract, such as specifying a namespace used to scope the type. When you apply the DataContract
attribute to a class, you must also apply the DataMember
attribute to each member that you want to include. These attributes change the default behavior that includes all public fields and properties into opt-in serialization instead. Only the members that have a DataMember
attribute applied to them are included in the message.
If you are declaring a message contract, you can control aspects of the contract by applying the MessageContract
attribute to the class. You then apply the MessageBodyMember
or MessageHeader
attribute to each field or property that you want to include in the message. One advantage to using a message contract is that it allows you to selectively control the signing and encryption of individual members of the message class. This is controlled by additional properties on the attributes.
The contract for a conventional WCF service is declared using a C# interface that has been decorated with ServiceContract
and OperationContract
attributes. However, there is current no facility provided with WF to import or otherwise reuse existing contracts that are defined in C# interfaces. Perhaps this is something that Microsoft will address with a post-4 release of .NET. Instead, all contracts for workflow services are inferred based on the properties of the Receive
and SendReply
activities.
These properties of the Receive
activity combine to form the service contract:
ServiceContractName
. The namespace and name of the contract.OperationName
. The name of the operation.Content
. The number, type and sequence of parameters that define a message or data contract.
If you are defining a request/response operation, the Content
property of the SendReply
activity determines the parameters that constitute the response portion of the contract.
Correlation is really an overloaded term in WF. There are two ways that correlation is used with workflow services:
- Associating one messaging activity with another in the same workflow
- A way to route messages to the correct workflow instance
In the first case, correlation is used to logically relate one messaging activity with another. For example, if you are using the duplex message exchange pattern, two workflows can independently exchange messages with each other. Each workflow has a Receive
activity that is used to receive messages from the other workflow and a Send
activity that sends a message to the other workflow. To logically relate the two messages (sent and received), correlation is used within each workflow.
Note Chapter 10 presents a workflow example that uses the duplex message exchange pattern.
On the other hand, if you are implementing a request/reply message exchange pattern, the activities that declare the service (Receive
and SendReply
) are already logically related to each other via the Request
property of the SendReply
. In this case, there is usually no need for additional correlation. However, it may be necessary to implement correlation when a workflow has multiple, parallel messaging activities that are active at the same time.
The second use of correlation is to ensure that messages are routed to the correct workflow instance. For example, you might declare a stateful workflow that implements multiple operations. One operation would be used to create a new workflow instance (a Receive
activity with the CanCreateInstance
property set to true), while the other operations use the existing instance. Correlation is used to route the operations to the correct workflow instance.
Correlation for the purpose of identifying the correct workflow instance can be accomplished using context-based or content-based correlation. Context-based correlation requires a WCF binding that supports the exchange of context information in the SOAP headers or HTTP cookies. For example, the WSHttpContextBinding
, NetTcpContextBinding
, or BasicHttpContextBinding
can be used for this purpose.
Content-based correlation uses data within the message itself to route the message. For example, each message may contain an account or order number that can be used to logically relate each operation to the others. This identifying value could be used to route each message to the correct workflow instance. Content-based correlation has the advantage that it does not require one of the special bindings that supports context exchange.
Note In Chapter 10 you will see both correlation types in action. You will declare a workflow that first uses context-based correction and then modify it to use content-based correlation.
Within a workflow, you can correlate messaging activities using one of these mechanisms:
- Initialize and reference a
CorrelationHandle
.- Place the messaging activities in a
CorrelationScope
activity.
To use a CorrelationHandle
, you first define it as a workflow variable. One messaging activity initializes the handle, while the other messaging activities reference it. The initialization is accomplished using one of the available correlation initializers provided with WF. All of these initializer classes derive from the abstract CorrelationInitializer
class. Each initializer serves a slightly different correlation scenario:
RequestReplyCorrelationInitializer
. Used for correlation in request/response operations.ContextCorrelationInitializer
. Used for context-based correlation.QueryCorrelationInitializer
. Used for content-based correlation.CallbackCorrelationInitializer
. Used with a duplex message exchange pattern to correlate a message with the calling service.
If you simply need to correlate two or more messaging activities with each other, you can choose to use a CorrelationScope
activity. This is a container for one or more activities. The correlation is managed for you by placing the messaging activities that you want to correlate in the CorrelationScope
. This eliminates the need to manually define and manage a CorrelationHandle
.
WF provides two packaging options for workflows that use the WCF messaging activities:
- As a Xaml workflow
- As a Xamlx workflow service
A Xaml workflow is the same type of workflow that you have been using throughout this book. You can use messaging activities such Receive
and SendReply
in the workflow to declare a service operation. An instance of the workflow would typically be created when a client invokes the service operation (assuming that the CanCreateInstance
property of the Receive
activity is set to true). Xaml workflows can also act as a client and invoke a WCF service using the Send
and ReceiveReply
activities.
A Xamlx workflow service is designed to provide additional properties that simplify hosting by one of the Microsoft hosting environments (IIS, WAS, Windows Server AppFabric). If you plan to use one of these environments, it is recommended that you declare your workflows as Xamlx workflow services.
The top-level node of a Xamlx file is an instance of a WorkflowService
class instead of an Activity
. This class provides these properties:
In addition to declaring service operations (using Receive
and SendReply
), a Xamlx workflow service can take on the role of the client and invoke other services using Send
and ReceiveReply
.
When it comes to hosting your Xamlx workflow services, you have these options available to you:
- Self-hosted in your application using the
WorkflowServiceHost
class- Hosted in IIS/WAS
Note Xaml workflows that contain messaging activities can be self-hosted using the WorkflowServiceHost
class, or they can be deployed to IIS and hosted with the addition of an .SVC file. However, it is recommended that you use Xamlx workflow services if you are planning on using a Microsoft-provided hosting environment. Doing so simplifies the deployment of your workflow services since the additional SVC file is not required. Hosting of workflow services is discussed and demonstrated later in this chapter.
A new workflow service instance is created each time an operation is invoked (assuming that the CanCreateInstance
property is set to true). For many operations, the lifetime of the instance is short-lived. The workflow performs the necessary work to complete the operation, returns a response to the caller (if necessary), and is removed from memory.
However, long-running workflow services are also possible. These services may declare multiple operations that are invoked over a span of minutes, hours, or days. While they are waiting for the next service operation to be invoked, these instances are safely persisted in a durable store such as a SQL Server database.
To help with the management of these long-running workflow service instances that are hosted by WorkflowServiceHost
, WF provides a standard WCF management endpoint (WorkflowControlEndpoint
). This endpoint supports operations that manage existing service instances such as Cancel
, Suspend
, and Terminate
. A client proxy class (WorkflowControlClient
) is also provided that simplifies the calls to this control endpoint.
Note You can’t effectively use the control endpoint unless you are working with long-running workflow instances that have been persisted. For this reason, use of the WorkflowControlEndpoint
and the WorkflowControlClient
is discussed in Chapter 12, which covers the larger topic of workflow persistence.
In this section, you will declare the first of several workflow services in this chapter. This first workflow forms the foundation for the subsequent examples in this chapter. In the sections that follow this one, you will declare additional service operations that will be invoked by this workflow service.
This first service operation uses the request/response message exchange pattern. A client invokes the operation by sending it a request message and then waits for a response message. In general, you can follow these steps to implement a service operation using this pattern:
- Use the WCF Workflow Service Application new project template to create a project that is suitable for workflow services.
- Use the WCF Workflow Service add item template to add Xamlx files to the project if needed for additional services.
- Set the
Name
andConfigurationName
properties of the workflow.- Add a
ReceiveAndSendReply
activity template to the workflow.- At a minimum, set the
ServiceContractName
,OperationName
, andContent
properties of theReceive
activity. It is permissible to omit setting theContent
property if the request does not require any input parameters.- Set the
Receive.CanCreateInstance
property to true to create a new instance of the workflow each time this operation is invoked.- At a minimum, set the
Content
property of theSendReply
activity. It is permissible to leave this property unset if the response does not return any data.- Add additional activities between the
Receive
andSendReply
to implement the business logic of the service operation.- Modify the
Web.config
(orApp.config
if self-hosting) to include the necessary WCF entries for the new service (endpoints, bindings, behaviors, and so on).- Deploy the workflow to a Microsoft hosting environment such as IIS or implement a self-hosting application.
Note A new WCF Workflow Service includes a ReceiveAndSendReply
activity template by default. You can choose to modify the properties of the existing Receive
and SendReply
activities or delete them and add a new ReceiveAndSendReply
activity template to the empty workflow.
The business scenario for this example is an order processing workflow service. If you think of the typical ecommerce site with a shopping cart, you’ll quickly get the idea. The request that is sent to this order processing service contains some basic customer information (their name, address, email, and so on), the items that they have ordered, and credit card information that is used for payment processing. This main workflow controls the overall process of assigning an order number, authorizing the credit card purchase, instructing the warehouse to ship the items, and determining a shipping date. The response from the workflow includes the order number, a ship date, and a credit card authorization code.
The initial version of this workflow will complete the service operation internally by populating the response with default values. Subsequent versions will enhance this example by invoking other service operations to perform the individual order processing tasks.
You will complete these tasks to implement this example:
- Create a new WCF Workflow Service Application project, and add a new WCF Workflow Service to the project.
- Implement C# classes that represent the request and response for the service operation.
- Add a
ReceiveAndSendReply
activity template to the workflow service, add the necessary workflow variables, and configure the individual messaging activities.- Add additional activities to populate the response with valid values before it is returned to the caller.
- Review the
Web.config
file, and test the workflow service using the ASP.NET Development Server and WCF Test Client.
To begin this example, create a new project using the WCF Workflow Service Application project template. Name the new project ServiceLibrary
. Delete the Service1.xamlx
file that is created with the new project since it won’t be used. The Web.config
file that is created for the project will be used, so do not delete it.
Add a new WCF Workflow Service to the project, and name it OrderProcessing.xamlx
.
Tip I recommend that you start Visual Studio with elevated administrator privileges (the Run as Administrator option in Vista and Windows 7). Doing so avoids any security issues that you would otherwise encounter opening ports, publishing to IIS, and so on.
The message content type, which supports only a single argument, will be used for this service operation; therefore, C# classes must be implemented for the request and response messages. Add a new C# class to the ServiceLibrary
project, and name it OrderProcessingRequest
. Here is the implementation for this class:
using System;
using System.Collections.Generic;
namespace ServiceLibrary
{
public class OrderProcessingRequest
{
public OrderProcessingRequest()
{
Items = new List<Item>();
}
public String CustomerName { get; set; }
public String CustomerAddress { get; set; }
public String CustomerEmail { get; set; }
public Decimal TotalAmount { get; set; }
public String CreditCard { get; set; }
public String CreditCardExpiration { get; set; }
public List<Item> Items { get; set; }
}
}
This class defines the properties that are passed to the service operation as a request message. The Item
type referenced in the final property defines a single item to be sold along with the quantity to be ordered of that item. Add a new class named Item
to the ServiceLibrary
project to implement this type:
using System;
namespace ServiceLibrary
{
public class Item
{
public Int32 ItemId { get; set; }
public Int32 Quantity { get; set; }
}
}
Finally, add a new class named OrderProcessingResponse
to the ServiceLibrary
to define the response from the service operation:
using System;
namespace ServiceLibrary
{
public class OrderProcessingResponse
{
public Int32 OrderId { get; set; }
public DateTime ShipDate { get; set; }
public String CreditAuthCode { get; set; }
public Boolean IsSuccessful { get; set; }
}
}
As you can see, these are all ordinary C# classes that don’t require any additional attributes. By default, all the public properties are serialized and become part of the message. Optionally, you could explicitly control the serialization by adding instances of the DataContract
and DataMember
attributes to the classes. For example, here is the OrderProcessingRequest
class after the data contract attributes have been added:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace ServiceLibrary
{
[DataContract]
public class OrderProcessingRequest
{
public OrderProcessingRequest()
{
Items = new List<Item>();
}
[DataMember]
public String CustomerName { get; set; }
[DataMember]
public String CustomerAddress { get; set; }
[DataMember]
public String CustomerEmail { get; set; }
[DataMember]
public Decimal TotalAmount { get; set; }
[DataMember]
public String CreditCard { get; set; }
[DataMember]
public String CreditCardExpiration { get; set; }
[DataMember]
public List<Item> Items { get; set; }
}
}
These attributes allow you to explicitly determine which properties are included with the message as well as to override the name or sequence of each property. If you add the DataContract
attribute to the class, only those properties that include the DataMember
attribute will be included.
Alternatively, since the OrderProcessingRequest
class is designed to represent the entire message contract, you can choose to apply the MessageContract
, MessageBodyMember
, and MessageHeader
attributes to the class. Collectively, these attributes provide a fine degree of control over the exact shape of the serialized message. For example, here is the same request class decorated with these message contract attributes:
using System;
using System.Collections.Generic;
using System.ServiceModel;
namespace ServiceLibrary
{
[MessageContract]
public class OrderProcessingRequest
{
public OrderProcessingRequest()
{
Items = new List<Item>();
}
[MessageBodyMember]
public String CustomerName { get; set; }
[MessageBodyMember]
public String CustomerAddress { get; set; }
[MessageBodyMember]
public String CustomerEmail { get; set; }
[MessageBodyMember]
public Decimal TotalAmount { get; set; }
[MessageHeader]
public String CreditCard { get; set; }
[MessageHeader]
public String CreditCardExpiration { get; set; }
[MessageBodyMember]
public List<Item> Items { get; set; }
}
}
The placement of the MessageHeader
or MessageBodyMember
attributes determine whether each property is included in the header or body of the message.
Note The lack of these message contract attributes doesn’t prevent a class from being used as a message contract. A service operation uses a WCF message contract if you select the Message option when you set the Receive.Content
property. The single type that you designate for the Message option can be any of the variations that were just reviewed (no attributes, data contract attributes, or message contract attributes). If you use data contract attributes on the class, it simply means that you are refining elements of the data contract serialization for the class, but it is still considered a WCF message contract since that is the option you chose for the Receive.Content
property.
Now that the request and response classes have been defined, you can turn your attention to the workflow service. Rebuild the solution at this point to ensure that the classes that you have defined build successfully. After opening the OrderProcessing.xamlx
workflow in the designer, you can follow the steps outlined next to declare a new ProcessOrder
service operation.
Note The instructions that follow and the test results that are shown assume that the original version of the request and response classes (without any additional attributes) have been used.
The workflow requires two variables, but you won’t be able to add them immediately. The step-by-step instructions will indicate when the variables can be added. Here is a recap of the variables required by the workflow:
Please follow these steps:
- Select the
Sequence
activity that was included with the new workflow service, and delete it. This also deletes the two messaging activities that are children of theSequence
activity. I’ve included this step since I find that it’s easier to drag a new activity template to the empty workflow to modify the property values of the one that is added by default.- While the empty workflow is selected, set the
Name
andConfigurationName
properties toOrderProcessing
. This sets the name for the service and also identifies the section within theWeb.config
(orApp.config
) file that defines WCF settings for this service.- Add a
ReceiveAndSendReply
activity template (found in the Messaging category of the Toolbox) to the empty workflow. The template adds aSequence
activity with aReceive
andSendReply
activity (namedSendReplyToReceive
) as children.- Add the two workflow variables indicated in the previous table. Both of them are scoped by the
Sequence
activity. Please note that theResponse
variable requires a default value to construct a new instance of the response object.- Open the Content Definition dialog for the
Receive
activity by selecting Content on the activity or by clicking the ellipsis for theContent
property in the Properties window. Once the dialog is opened, select the Message option if it is not already selected. Then set the Message data value toRequest
in order to reference the variable that you defined. When a new message is received for this operation, the value of that message will be used to populate the properties of theRequest
variable. The Message Type property does not need to be set. The type is inferred from the Message data value. Figure 9-1 shows the Content Definition dialog.Note The Message Type is normally inferred from the Message data value that you specify. However, the Message Type property determines the data type that is published as part of the service description. You might need to explicitly set the Message Type if the data value that you set is a derived type but you want to expose a service description with the base type. If you set it, the Message Type must be the same type as the Message data value or its base class.
- Set other properties of the
Receive
activity. Set theServiceContractName
property to{http://tempuri.org/}IOrderProcessing
and theOperationName
property toProcessOrder
. Set theCanCreateInstance
property to true (checked). Figure 9-2 shows the completed properties for theReceive
activity.- Select the
SendReplyToReceive
activity (aSendReply
), and set the Content property in a similar way. Select Message for the content type, and set the Message data to theResponse
variable. Figure 9-3 shows the Content Definition dialog for this activity.
Note The ReceiveAndSendReply
activity template also creates a CorrelationHandle
variable named __handle1
. This handle is initialized with a RequestReplyCorrelationInitializer
by the Receive
activity, but it is not referenced anywhere in the workflow and is not needed for this example. The activity template includes it for you to use when correlating other messaging activities with this Receive
activity. For this example, you can choose to keep this handle or remove it. If you choose to remove it, make sure that you also remove the correlation initializer from the Receive
activity (see the CorrelationInitializers
property).
You have now declared the service operation, but the workflow doesn’t contain any real business logic. For this initial version of the workflow, you will add a set of Assign
activities that populate the properties of the Response
variable with valid values without actually implementing any business logic. In subsequent examples, you will replace these Assign
activities with other activities that invoke additional service operations or use a workflow extension.
Add four new Assign
activities to the workflow. All of these activities should be placed between the Receive
and SendReplyToReceive
activities. Set the properties for each of these Assign
activities using the values shown here. Use one set of Assign.To
and Assign.Value
values for each Assign
activity:
Figure 9-4 shows the completed OrderProcessing
workflow service.
This first example does not require any changes to the default Web.config
file that was created with the ServiceLibrary
project. Here are the contents of the default Web.config
file with only a few cosmetic formatting changes to fit the format of this book:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4" />
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
After building the solution, you should be ready to test this new service operation. Please follow these steps to test the service:
- Open the project properties for the project, and select the Web tab; make sure the Start Action is set to Current Page. This causes the currently selected page to be opened in the WCF Test Client. Make sure the Use Visual Studio Development Server option is set, and choose the Specific Port option. Set the port number to 8080. Setting a specific port isn’t strictly necessary, but it will ensure that your results match those shown here.
- Make sure the
OrderProcessing.xamlx
file is highlighted in the Solution Explorer, and press the Ctrl-F5 combination to start without debugging.- After a brief wait, the ASP.NET Development Server should start, followed by the WCF Test Client.
- If all goes well, the WCF Test Client should be able to retrieve the metadata for the service and allow you to invoke the service operation. Enter test data for the request and then invoke the operation. Figure 9-5 shows the request data that I used along with the results of my test.
You may notice that the BasicHttpBinding
was used for the service. This is the default binding that is used by services that use the HTTP protocol scheme and that are not explicitly defined in the Web.config
file.
Note Since many of the response properties are populated with random numbers, your results will be similar to the results shown here but will not match them exactly.
The ASP.NET Development server is fine for the initial development and testing of the workflow service, but it is not suitable as a permanent hosting environment. In this section, you will walk through the steps needed to publish the OrderProcessing
workflow service to IIS.
When you publish a project to IIS, the Web.config
file for the project is deployed along with any required assembly files. Therefore, it is appropriate to make any adjustments to the Web.config
file before publishing. To make things slightly more interesting, I’ve made a few slight modifications to the Web.config
file for the ServiceLibrary
project. Here is the modified file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4" />
</system.web>
<system.serviceModel>
<services>
<service name="OrderProcessing">
<endpoint contract="IOrderProcessing" binding="wsHttpBinding"
bindingConfiguration="OrderProcessingBinding" />
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="OrderProcessingBinding">
<security mode="Message">
<message clientCredentialType="Windows"
algorithmSuite="TripleDesRsa15"
establishSecurityContext="true"
negotiateServiceCredential="true" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
Instead of relying on the default WCF settings, the file now explicitly defines the OrderProcessing
service. The single endpoint defines the contract (IOrderProcessing
), the binding (wsHttpBinding
), and a binding configuration (OrderProcessingBinding
). The name specified here for the service (OrderProcessing
) must match the value that you used when you set the ConfigurationName
property of the workflow service.
The binding configuration provides values for a few security-related properties. These binding values are not absolutely necessary for this service. I’ve included them only to demonstrate how binding properties can be set within a configuration file.
Tip You can hand-code the WCF configuration settings, or you can use the WCF Service Configuration Editor. This tool should be available as one of the standard options in the Visual Studio Tools menu. You can also start it outside of Visual Studio from the command line. The executable name is svcconfigeditor.exe
, and it is distributed with the Windows SDK.
Assuming that IIS has been installed and configured on your development machine, you can follow these steps to publish the OrderProcessing
service to IIS:
- Right-click the
ServiceLibrary
project in the Solution Explorer, and select the Publish option. Select File System as the Publish Method. Create a new folder namedProWF
under theInetpubwwwroot
folder. Select the newProWF
folder as the Target Location. This should publish all the files needed to host the service to the new folder.- Open the IIS management console plug-in. I start this by right-clicking Computer and selecting the Manage option. You can also start the IIS Manager by running the executable
InetMgr.exe
directly.- Find the
ProWF
folder under the Default Web Site, right-click it, and select the Convert to Application option. You should also verify that the ASP .NET 4 application pool is selected for this application.
The OrderProcessing
service should now be published and ready for use. To test the service under IIS, you can start the WCF Test Client (WcfTestClient.exe
) and add the service located at this URL: http://localhost/ProWF/OrderProcessing.xamlx
. When you invoke the service, you should see similar results as your previous test using the ASP.NET Development Server.
In this section, you will implement a Windows console application that invokes the OrderProcessing
workflow service. The purpose of this short example is to demonstrate how to invoke a WCF service from a nonworkflow application.
Create a new project using the Windows Console Application template (not a workflow project). Name the new project ConsoleClient
, and add it to the solution that you created for this chapter. The first order of business is to add a service reference to the OrderProcessing
workflow service. Adding a service references generates a set of classes that you can use to invoke the service. Please follow these steps to add the service reference:
- Right-click the new
ConsoleClient
project, and select the Add Service Reference option. This opens a dialog that allows you to select, configure, and add a service reference.- Click the Discover button to search for any services in the same solution. You should see the
OrderProcessing.xamlx
service displayed in the list of available services.- Select the
OrderProcessing.xamlx
service, and change Namespace toOrderProcessing
. Before clicking OK, click the Advanced button. Change the Collection type option from the default ofSystem.Array
toSystem.Collections.Generic.List
. This causes the generated client proxy code to use a generic class for any collections instead of a simple array.- After making the changes indicated in the previous steps, click OK to generate the service reference. The ASP.NET Development Server should start to enable the service metadata to be retrieved. Once started, the server should continue running until you stop it, close the solution, or shut down Visual Studio.
Once the service reference has been created, the code to actually invoke the service is fairly simple. Here is the code that should be added to the Program.cs
file of the ConsoleClient
project:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using ConsoleClient.OrderProcessing;
namespace ConsoleClient
{
class Program
{
static void Main(string[] args)
{
In this example code, I’ve chosen to execute the workflow service twice: the first time using a set of generated proxy classes and the second time using a generated interface. Both mechanisms are a valid way to access the service.
CallViaProxy();
CallViaInterface();
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
The service is first invoked using a set of generated proxy classes. This is the simplest way to invoke the service since the proxy classes hide most of the WCF-specific details. To invoke the service, you first create an instance of the request class and populate it with appropriate values. You then create an instance of the client proxy class and invoke the method named for the service operation (ProcessOrder
), passing it the request object. The result is a response object containing the output from the service operation.
By default, the proxy class retrieves the WCF endpoint and binding configuration from the App.config
file. This configuration file was also added to the project when you added the service reference.
Proper WCF etiquette requires that you call the Close
method of the proxy class when you are finished with it. This closes the WCF channel to the server in a graceful and controlled manner.
static void CallViaProxy()
{
OrderProcessingClient client = null;
try
{
Console.WriteLine("About to invoke OrderProcessing service");
client = new OrderProcessingClient();
OrderProcessingRequest request = new OrderProcessingRequest();
request.CreditCard = "4444111111111111";
request.CreditCardExpiration = "0611";
request.CustomerName = "Joe Consumer";
request.CustomerAddress = "100 Main Street";
request.CustomerEmail = "[email protected]";
request.TotalAmount = 75.00M;
request.Items = new List<Item>
{
new Item { ItemId = 1234, Quantity = 1 },
new Item { ItemId = 2345, Quantity = 3 },
};
OrderProcessingResponse response = client.ProcessOrder(request);
Console.WriteLine("Response IsSuccessful: {0}",
response.IsSuccessful);
Console.WriteLine("Response OrderId: {0}",
response.OrderId);
Console.WriteLine("Response ShipDate: {0:D}",
response.ShipDate);
Console.WriteLine("Response CreditAuthCode: {0}",
response.CreditAuthCode);
}
catch (Exception exception)
{
Console.WriteLine("Unhandled exception: {0}", exception.Message);
}
finally
{
client.Close();
}
}
The second way to invoke a service operation is to use a generated interface. To use the interface, the CreateChannel
method of the ChannelFactory<TChannel>
generic class is invoked. The call to this method returns an object that implements the service interface. There are several overloads of the CreateChannel
method. The one used here specifies the binding and endpoint address. The ProcessOrder
method of the returned object can then be invoked in a similar way as was done in the previous code.
static void CallViaInterface()
{
IOrderProcessing client = null;
try
{
Console.WriteLine("About to invoke OrderProcessing service");
WSHttpBinding binding = new WSHttpBinding(
"WSHttpBinding_IOrderProcessing");
EndpointAddress epAddr = new EndpointAddress(
"http://localhost:8080/OrderProcessing.xamlx");
client = ChannelFactory<IOrderProcessing>.CreateChannel(
binding, epAddr);
OrderProcessingRequest request = new OrderProcessingRequest();
request.CreditCard = "4444111111111111";
request.CreditCardExpiration = "0611";
request.CustomerName = "Joe Consumer";
request.CustomerAddress = "100 Main Street";
request.CustomerEmail = "[email protected]";
request.TotalAmount = 75.00M;
request.Items = new List<Item>
{
new Item { ItemId = 1234, Quantity = 1 },
new Item { ItemId = 2345, Quantity = 3 },
};
ProcessOrderResponse poResponse = client.ProcessOrder(
new ProcessOrderRequest(request));
OrderProcessingResponse response =
poResponse.OrderProcessingResponse;
Console.WriteLine("Response IsSuccessful: {0}",
response.IsSuccessful);
Console.WriteLine("Response OrderId: {0}",
response.OrderId);
Console.WriteLine("Response ShipDate: {0:D}",
response.ShipDate);
Console.WriteLine("Response CreditAuthCode: {0}",
response.CreditAuthCode);
}
catch (Exception exception)
{
Console.WriteLine("Unhandled exception: {0}", exception.Message);
}
finally
{
((IChannel)client).Close();
}
}
}
}
When you added a service reference to the ConsoleClient
project, an App.config
file was also added to the project. This file contains the WCF configuration settings that are appropriate for the service that you referenced. You should be able to use the generated settings without any changes. Your App.config
file should look similar to this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IOrderProcessing" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false"
hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text"
textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="8192"
maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSession ordered="true" inactivityTimeout="00:10:00"
enabled="false" />
<security mode="Message">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="Windows"
negotiateServiceCredential="true"
algorithmSuite="TripleDesRsa15" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8080/OrderProcessing.xamlx"
binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_IOrderProcessing"
contract="OrderProcessing.IOrderProcessing"
name="WSHttpBinding_IOrderProcessing">
</endpoint>
</client>
</system.serviceModel>
</configuration>
Note This App.config
file and the code in Program.cs
contain endpoint addresses that reference the OrderProcessing
service that is hosted by the ASP.NET Development Server. It is assumed that you are using the recommended port (8080) for the service. If not, you’ll need to adjust the settings shown here to match your development environment.
After building the ConsoleClient
application, you should be ready to execute it. Before you do, you’ll need to make sure that the ASP.NET Development Server is running and is hosting the ServiceLibrary
project. If it is not already running, you can set the ServiceLibrary
project as the startup project and press Ctrl-F5 to start it without debugging. Once it starts, you can set the ConsoleClient
project as the startup project and execute it. Optionally, you can change the startup project option to start multiple projects (ServiceLibrary
and ConsoleClient
).
Here are my results when I execute the ConsoleClient
project:
About to invoke OrderProcessing service
Response IsSuccessful: True
Response OrderId: 1320239653
Response ShipDate: Friday, December 25, 2009
Response CreditAuthCode: 61478
About to invoke OrderProcessing service
Response IsSuccessful: True
Response OrderId: 696000473
Response ShipDate: Friday, December 25, 2009
Response CreditAuthCode: 32410
Press any key to exit
The previous client application populated the request class with a fixed set of values and then invoked the service operation entirely in code. In this example, you will develop a client workflow that declaratively invokes the service operation. To make the example slightly more interesting, the workflow will prompt the user (via the console) to enter the values that are needed for the request.
To begin this example, create a new project named WorkflowClient
using the Workflow Console Application template, and add it to the existing solution for this chapter. You can delete the Workflow1.xaml
file since it won’t be used.
Tip The workflow for this client application contains a large number of activities, most of which are not directly related to the WCF messaging activities. The nonmessaging activities demonstrate one way to interact with a real human being to obtain input. Because of the size of this workflow, you may want to download the code for this particular example and review it instead of constructing the application from scratch.
This example uses a custom activity that provides a way to prompt the user with a message and then wait for input from the console. To implement this activity, add a new Code Activity to the WorkflowClient
project, and name it ReadLineWithPrompt
. Here is the code for this activity:
using System;
using System.Activities;
using System.Reflection;
namespace WorkflowClient
{
public class ReadLineWithPrompt<TResult> : AsyncCodeActivity<TResult>
{
This generic activity defines a single input argument named Prompt
that allows you to specify the message that is displayed to the user on the console. The generic type identifies the return type of the activity. In the example workflow that uses this activity, you will use return types of String
and Int32
.
public InArgument<string> Prompt { get; set; }
protected override IAsyncResult BeginExecute(
AsyncCodeActivityContext context,
AsyncCallback callback, object state)
{
After displaying the prompt message, the code to read input from the console (WaitForConsoleInput
) is asynchronously executed.
Console.WriteLine(Prompt.Get(context));
Func<TResult> getInput = () => { return WaitForConsoleInput(); };
context.UserState = getInput;
return getInput.BeginInvoke(callback, state);
}
private TResult WaitForConsoleInput()
{
TResult value = default(TResult);
String stringInput = Console.ReadLine();
If the generic type is String
, the work of this activity is done and no conversion of the input data is required. If it is some other type, reflection is used to execute the static Parse
method. The assumption of this activity is that only types that support the Parse
method will be used as the generic type.
if (typeof(TResult) == typeof(String))
{
value = (TResult)(Object)(stringInput);
}
else
{
MethodInfo parse = typeof(TResult).GetMethod(
"Parse", BindingFlags.Static | BindingFlags.Public,
null, new Type[] { typeof(String) }, null);
if (parse != null)
{
try
{
value = (TResult)parse.Invoke(
null, new Object[] { stringInput });
}
catch
{
//ignore any parsing errors
}
}
else
{
throw new InvalidOperationException(
"Parse method not supported");
}
}
return value;
}
protected override TResult EndExecute(
AsyncCodeActivityContext context, IAsyncResult ar)
{
return ((Func<TResult>)context.UserState).EndInvoke(ar);
}
}
}
Before moving on to the next step, you should build the solution to ensure that this custom activity builds correctly. This also adds the activity to the Toolbox to make it available for your use. If you receive an error that the Workflow1
type is missing, you can delete the offending code in the Program.cs
file.
Just as was the case with the previous client application, the WorkflowClient
project requires a service reference to the OrderProcessing
workflow service. However, adding a service reference to a workflow project generates a custom activity for each service operation instead of the proxy classes that you used in the previous example. These generated activities enable you to easily invoke a WCF service operation declaratively within a workflow.
You can follow the same set of steps to add a service reference that you completed for the previous client application. After the service reference has been added to the project, build the solution to ensure that everything builds correctly. This also adds the generated messaging activity to the Toolbox, making it available for your use.
Add a new workflow to the WorkflowClient
project using the Activity add item template. Name the new workflow InitiateOrderProcessing
. The goal of this workflow is to invoke the ProcessOrder
service operation. It begins by prompting the user to enter property values for the service request. Once the request has been populated with the user’s input, the ProcessOrder
operation is invoked. When the operation has completed, the properties of the service response are displayed on the console.
Tip This workflow references a number of types that were generated by adding the service reference. To make it easier to quickly work with these types, you may want to add the WorkflowClient.OrderProcessing
namespace to the Imports list of the workflow. To do this, click the Imports button on the design canvas, and enter the namespace at the top of the list. Adding a namespace to this list is similar in concept to adding a using
statement to the top of a C# class. It allows you to access the types defined within the namespace without the need to fully quality them with the namespace.
Here is a recap of the workflow variables that you will add to the workflow. The instructions that follow will tell you when to add the variables.
Please follow these steps to declare the workflow:
- Add a
Flowchart
activity to the empty workflow.- Add the
Request
andResponse
variables that are scoped by this topmost Flowchart activity.- Add another
Flowchart
as the child of the topmostFlowchart
activity. Change theDisplayName
of this newFlowchart
toEnterCustInfoFC
to more easily distinguish from otherFlowchart
activities. Drag a connection from the starting point of theFlowchart
to the top of theEnterCustInfoFC
activity that you just added.- After opening the
EnterCustInfoFC
activity, add a set of fiveReadLineWithPrompt
activities. Set the generic type for all of them toString
. Connect the activities so that they are all executed in sequence. The purpose of these activities is to prompt the user to enter values for the customer-related properties of the request. Please refer to the following table for a list of the properties that should be set for each of these activities. The completedEnterCustInfoFC
activity should look like Figure 9-6.- Return to the topmost
Flowchart
, and add anotherFlowchart
activity as a child. Change theDisplayName
of this new activity toEnterItemsFC
. Drag a connection from the bottom of theEnterCustInfoFC
to this new activity.- Open the
EnterItemsFC
activity, and add the Item and Items variables that are scoped by theEnterItemsFC
activity.- Add an
Assign
activity as the first child of theEnterItemsFC
activity. Connect the starting point of the flowchart to the top of theAssign
activity. Set theAssign.To
property toItem
and theAssign.Value
property toNew Item()
. This initializes theItem
variable and prepares it for the steps that follow this one.- Add a
ReadLineWithPrompt
activity below theAssign
. Set the generic type toInt32
. Drag a connection between the bottom of theAssign
and the top of this new activity. Set thePrompt
property to"Enter an item ID (enter 0 if done entering items)"
and theResult
property toItem.ItemId
.- Add a
FlowDecision
control below theReadLineWithPrompt
. Add a connection between the two activities. Set theCondition
property toItem.ItemId <> 0
.- Add a
ReadLineWithPrompt
activity below and to the left of theFlowDecision
. Set the generic type toInt32
. Set thePrompt
property to"Enter the quantity of the item"
and theResult
property toItem.Quantity
. Drag a connection from the true side of theFlowDecision
(the left side) to the top of theReadLineWithPrompt
.- Add an
AddToCollection<T>
activity below the lastReadLineWithPrompt
activity. Set the generic type of the activity toItem
. Set theCollection
property toItems
and theItem
property toItem
. This adds the newly constructedItem
object to the collection.- Drag a connection from the side of the
AddToCollection<T>
activity to the side of theAssign
activity at the top of the flowchart. This causes the flowchart to loop back to the top to allow entry of another item.- Turning your attention to the false side of the
FlowDecision
, add anAssign
activity to the right and below theFlowDecision
. Set theAssign.To
property toRequest.Items
and theAssign.Value
property toItems
. This populates theItems
property of theRequest
variable with the collection of items that has been constructed by this flowchart. Drag a connection from the false side of theFlowDecision
to the top of thisAssign
activity.- Add another
Assign
activity below the one that you just added. Set theAssign.To
property toRequest.TotalAmount
and theAssign.Value
property toItems.Count * 1.99D
. Add a connection between the twoAssign
activities. This completes theEnterItemsFC
flowchart activity, which is shown in Figure 9-7.- Return to the topmost
Flowchart
activity, and add aWriteLine
activity below theEnterItemsFC
. Set theText
property to"Invoking ProcessOrder service method"
. Add a connection from theEnterItemsFC
to this activity.- Add an instance of the
ProcessOrder
custom activity below theWriteLine
. This is the activity that was generated for you when you added a service reference to the project. Set theOrderProcessingRequest
property toRequest
and theOrderProcessingResponse
property toResponse
. Add a connection between this activity and theWriteLine
that is directly above it.- Add another
WriteLine
below theProcessOrder
activity. Set theText
property to"Back from ProcessOrder"
. Connect it to theProcessOrder
activity directly above it.- Add another
Flowchart
activity below theWriteLine
. Change itsDisplayName
toDisplayResponseFC
and connect it to theWriteLine
that is directly above it.- Open the
DisplayResponseFC
activity, and add a set of fiveWriteLine
activities. Connect all of the activities so that they are executed in order. Set theText
property of eachWriteLine
activity as indicated in the following table. Figure 9-8 shows the completedDisplayResponseFC
activity.
Figure 9-9 shows the completed InitiateOrderProcessing
workflow.
The final task for this example is to add the code that executes the InitiateOrderProcessing
workflow. Here is the code for the Program.cs
file of the WorkflowClient
project:
using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;
namespace WorkflowClient
{
class Program
{
static void Main(string[] args)
{
ExecuteClientWorkflow();
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
private static void ExecuteClientWorkflow()
{
WorkflowApplication wfApp = new WorkflowApplication(
new InitiateOrderProcessing());
AutoResetEvent waitEvent = new AutoResetEvent(false);
wfApp.Completed = (a) =>
{
waitEvent.Set();
};
wfApp.OnUnhandledException = (e) =>
{
Console.WriteLine("OnUnhandledException: {0}",
e.UnhandledException.Message);
return UnhandledExceptionAction.Cancel;
};
wfApp.Run();
waitEvent.WaitOne(90000);
}
}
}
Adding the service reference to the project should have also added an App.config
file containing the necessary WCF configuration settings. The contents of this App.config
file should look exactly like the previous client application.
To test the application, you need to also start the ServiceLibrary
project using the ASP.NET Development Server. The easiest way to do this is to change the startup project option to start multiple projects (ServiceLibrary
and WorkflowClient
). After pressing Ctrl-F5 (run without debugging), you should be prompted to enter the customer and order information. When you have finished entering one or more items with their quantity, you can just press Enter without entering a value or enter a value of 0. This is the signal to the workflow that entry of the request properties is complete and that it should invoke the ProcessOrder
service operation.
Here is a representative example of my results:
Enter the customer name
Bruce Bukovics
Enter the customer email
[email protected]
Enter the customer address
100 Main Street
Enter credit card number
4444111122223333
Enter credit card expiration date (mmyy)
0611
Enter an item ID (enter 0 if done entering items)
200
Enter the quantity of the item
1
Enter an item ID (enter 0 if done entering items)
300
Enter the quantity of the item
2
Enter an item ID (enter 0 if done entering items)
Invoking ProcessOrder service method
Back from ProcessOrder
ProcessOrder response:
IsSuccessful: True
ShipDate: Sunday, December 27, 2009
OrderId: 351417896
CreditAuthCode: 38789
Press any key to exit
Up to this point, you have used the ASP.NET Development Server and IIS to host the workflow service. You can also choose to self-host the service in an application that you develop. The advantage to self-hosting is that it is typically easier to deploy your application since you don’t require a dependency on IIS. Self-hosting also presents you with additional opportunities to fine-tune the hosting environment in code.
The WorkflowServiceHost
class that is included with WF provides the ability to self-host a workflow service in your own application. You construct this class by passing an instance of an Activity
(a Xaml workflow) or WorkflowService
(a Xamlx workflow service) in the constructor. You then provide binding and endpoint information for the service (either in code or using entries in an App.config
file). Finally, you call the Open
method to enable the service to begin processing messages. During the shutdown processing of your application, you call the Close
method to gracefully transition the service host to a state that no longer processes incoming messages.
Warning Be aware that .NET 4 actually includes two types with the same WorkflowServiceHost
name. The one that is capable of hosting .NET 4 workflow services is in namespace System.ServiceModel.Activities
. The one that is located in the System.ServiceModel
namespace is used for hosting WF 3.x workflows.
The WorkflowServiceHost
class derives from the ServiceHostBase
class, which in turn, derives from the base CommunicationObject
class. Because of this class hierarchy, the WorkflowServiceHos
t
inherits a large number of members that are defined by these base classes. Here are the most important members of the WorkflowServiceHost
class:
Note Chapter 11 discusses the use of persistence with workflow services along with other persistence topics.
In general, you can follow these steps to self-host a workflow service:
- Create an instance of the
Activity
orWorkflowService
that you want to host.- Use the
Activity
orWorkflowService
instance to construct aWorkflowServiceHost
instance.- Add any workflow extensions (if required).
- Configure persistence options (if required).
- Provide a binding and endpoint for the service (either in code or via
App.config
entries).- Open the workflow service to enable it to receive requests.
- Close the workflow service to gracefully transition the service to a closed state before exiting the application.
To see the WorkflowServiceHost
in action, you will implement a simple console application that hosts the OrderProcessing
workflow service. Create a new project using the Workflow Console Application template. Name the new project ServiceHost
, and add it to the existing solution for this chapter. Delete the Workflow1.xaml
file since it won’t be used.
By default, the Workflow Console Application project template targets the .NET Framework 4 Client Profile. This profile does not include the server-related types needed to host a workflow service. For this reason, you need to open the project properties and change the Target framework to .NET Framework 4 (the full framework).
You also need to add a project reference to the ServiceLibrary
project. You will be loading the workflow service definition directly from the Xamlx file instead of referencing the compiled types in the ServiceLibrary
project. However, you do need to reference the ServiceLibrary
assembly in order to deserialize the types that are used by the workflow service such as the request and response classes. Adding the project reference causes the ServiceLibrary
assembly to be copied into the output folder for the ServiceHost
project. This is strictly a runtime dependency—not a design-time dependency.
Here is the complete Program.cs
file that contains the code needed to host the service:
using System;
using System.Collections.Generic;
using System.IO;
using System.ServiceModel.Activities;
using System.Xaml;
namespace ServiceHost
{
class Program
{
private static List<WorkflowServiceHost> _hosts =
new List<WorkflowServiceHost>();
static void Main(string[] args)
{
try
{
A private CreateServiceHost
method is called to create a WorkflowServiceHost
instance for the requested workflow service. The code was organized in this way to make it easier to host the additional services that you will develop in Chapter 10. Each service host that has been created is then opened. For this example, that’s a single host. Code in the finally block closes any service hosts that have been opened.
This code assumes that the necessary WCF configuration entries for this service will be provided by an App.config
file. Alternatively, you could call the AddServiceEndpoint
method to add an endpoint directly in the code.
CreateServiceHost("OrderProcessing.xamlx");
foreach (WorkflowServiceHost host in _hosts)
{
host.Open();
foreach (var ep in host.Description.Endpoints)
{
Console.WriteLine("Contract: {0}",
ep.Contract.Name);
Console.WriteLine(" at {0}",
ep.Address);
}
}
Console.WriteLine("Press any key to stop hosting and exit");
Console.ReadKey();
}
catch (Exception exception)
{
Console.WriteLine("Service Exception: {0}", exception.Message);
}
finally
{
Console.WriteLine("Closing services...");
foreach (WorkflowServiceHost host in _hosts)
{
host.Close();
}
Console.WriteLine("Services closed");
_hosts.Clear();
}
}
The CreateServiceHost
method first loads the service definition directly from the named Xamlx file. The root node of a Xamlx file is actually a WorkflowService
instead of an Activity
. It is for this reason that the code defines the output from the LoadService
method as a WorkflowService
and uses it to construct the WorkflowServiceHost
instance.
The WorkflowService
object includes the additional Name
and ConfigurationName
properties that associate the workflow service with configuration entries in the App.config
file. Alternatively, you could reference the WorkflowService.Body
property if you only want to use the Activity
definition instead of the entire WorkflowService
definition. You would do this if you wanted to provide your own Name
and ConfigurationName
values for the service.
private static WorkflowServiceHost CreateServiceHost(String xamlxName)
{
WorkflowService wfService = LoadService(xamlxName);
WorkflowServiceHost host = new WorkflowServiceHost(wfService);
_hosts.Add(host);
return host;
}
The private LoadService
method deserializes a WorkflowService
instance from the named Xamlx file. For this example, the Xamlx file is assumed to be found in a relative path under the ServiceLibrary
project. This relative path was used as a convenience since that is the source location of the Xamlx file. This code assumes that these projects are located in the same solution. You will need to adjust this path if the Xamlx files are in a different location. To deserialize the Xamlx file, the static XamlServices.Load
method is called.
private static WorkflowService LoadService(String xamlxName)
{
String fullFilePath = Path.Combine(
@"......ServiceLibrary", xamlxName);
WorkflowService service =
XamlServices.Load(fullFilePath) as WorkflowService;
if (service != null)
{
return service;
}
else
{
throw new NullReferenceException(String.Format(
"Unable to load service definition from {0}", fullFilePath));
}
}
}
}
Tip Please remember that the XamlServices.Load
method requires runtime access to the types in the ServiceLibrary
assembly. In particular, any types referenced by the Xamlx file such as the request and response classes must be available in order to deserialize the workflow service. If you receive an exception that the Xamlx file cannot be deserialized, it is most likely caused by a missing ServiceLibrary.dll
assembly. If you make sure that this assembly is in the same indebug
folder as ServiceHost.exe
, the deserialization should work correctly.
Before you can run the ServiceHost
project, you need to provide WCF configuration entries in the App.config
file. Here is a complete App.config
file that provides the necessary settings:
<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<services>
<service name="OrderProcessing">
<host>
<baseAddresses>
<add baseAddress="http://localhost:9000/"/>
</baseAddresses>
</host>
<endpoint contract="IOrderProcessing"
address="http://localhost:9000/OrderProcessing"
binding="wsHttpBinding"
bindingConfiguration="OrderProcessingBinding" />
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="OrderProcessingBinding">
<security mode="Message">
<message clientCredentialType="Windows"
algorithmSuite="TripleDesRsa15"
establishSecurityContext="true"
negotiateServiceCredential="true" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceDebug includeExceptionDetailInFaults="True"
httpHelpPageEnabled="True"/>
<serviceMetadata httpGetEnabled="True"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<startup>
<supportedRuntime version="v4" sku=".NETFramework,Version=v4"/>
</startup>
</configuration>
This configuration file defines the OrderProcessing
service with an endpoint address of http://localhost:9000/OrderProcessing. The port number of 9000 was chosen somewhat at random because it’s just a nice round number that is easy to remember. The name specified for the service (OrderProcessing
) matches the ConfigurationName
property that is set within the Xamlx file. A binding configuration is also included which has the similar set of properties that you have used in previous examples.
After building the solution, you should be ready to test the ServiceHost
project. Select this project as the startup project, and press Ctrl-F5 to run the project without debugging. After a short wait, you should see these results:
Contract: IOrderProcessing
at http://localhost:9000/OrderProcessing
Press any key to stop hosting and exit
The workflow service is now hosted and is awaiting client requests. You can use the WCF Test Client to perform an initial test of the service. You should be able to point to the base service address of http://localhost:9000/ to retrieve the metadata for the service.
Tip These instructions assume that you have started Visual Studio with elevated administrator privileges. If you run the ServiceHost.exe
application outside of Visual Studio, you will also need to run it with these increased privileges. If you don’t, you will receive an error indicating that you do not have the authority to open the designated port. Alternatively, you can use the netsh
scripting utility to add the current machine user to the access list for the desired port. Please refer to the "Configuring HTTP and HTTPS" topic in the WCF online documentation for more information.
At this point you should also change the configuration for the WorkflowClient
application to reference this self-hosted service. Previously, the WorkflowClient
application referenced the service that was hosted by the ASP.Net Development Server (port 8080). To reference the self-hosted service, change this port number to 9000. The .xamlx
extension has also been removed from the endpoint address. Here is an abbreviated version of the App.config
file showing the section that requires a change:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
…
<client>
<endpoint address="http://localhost:9000/OrderProcessing"
binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_IOrderProcessing"
contract="OrderProcessing.IOrderProcessing"
name="WSHttpBinding_IOrderProcessing">
</endpoint>
</client>
</system.serviceModel>
</configuration>
After making this configuration change, you should be able to execute the WorkflowClient
application and reference the self-hosted service. Make sure that the ServiceHost
project is also running. Your results should be consistent with the previous test for this application.
The focus of this chapter was the WCF and workflow services support provided with WF. The chapter presented a brief introduction of workflow services and WCF followed by an in-depth review of the WF messaging-related activities and classes.
The chapter presented a workflow service that declared a relatively simple request/response operation. This example service was used to demonstrate how to publish a workflow service to IIS as well as how to implement an application that self-hosted the service. Two client applications were developed. One was a nonworkflow application and the other invoked the workflow service declaratively from another workflow.
In the next chapter, you will build upon the example service that you began in this chapter to learn about correlation and other message exchange patterns.
3.129.42.243