WCF: Creating Long-Running Services
Not all business operations finish in seconds. Using Windows Communication Foundation you can still create -- as a single project -- an application that supports business services that take hours (or days or weeks or months) to complete.
As I mentioned in my June 2011 Practical .NET column, "WCF and Service-Oriented Architectures", not all business processes are over in seconds -- some run on for hours, days or weeks. A complaint-management system, for instance, begins with a complaint being submitted, but it might be several hours before anyone even starts to resolve that complaint. Once someone does look at it, the complaint's status might move straight to a "Resolved" status, which would terminate the complaint-management process. Typically, though, a complaint will be in process for a long time before it's resolved.
Think of this process as a series of activities, some of them automated, but also including several "contact points" where manual processes update the complaint. For instance, there's an initial contact point when the complaint is submitted: the user submits a description of their complaint and gets back a ComplaintId. After the process performs any automated activities (for instance, notifying the appropriate person that a complaint has been received), the process will wait for input about the status of complaint -- that forms the next contact point. That start/stop process will continue until the complaint's status finally reaches "Resolved."
There's a serious amount of utility/overhead code that's required to support this kind of stop/start application. But if you have Visual Studio 2010, IIS and the Microsoft .NET Framework, you can reduce that overhead substantially. There's only one significant change you need to make: You should create the backbone of the system using the Visual Studio Windows Workflow Service Application template.
This sets up the process as a service with each contact point as a method on that service. You can then create whatever clients you need to call those contact points in the application's backbone. Creating a Workflow Service isn't all that different from creating other kinds of applications. Normally, you'd use a visual designer to create forms and call code from the forms' events. With Workflow you use a visual designer to create your workflow and call code from its activities.
Picking the Visual Studio WCF Workflow Service Application project template creates an ASP.NET application that acts as the host for your workflow (your workflow appears as a Web service to the outside world). Instead of an .asmx or .svc file, the template includes a .xamlx file that holds the output from the visual designer you use to define your workflow (and which, in turn, generates the necessary Web service methods). Your first step is to rename the default .xamlx file to something meaningful (I used "ComplaintManagement") and delete its default content.
The Initial Contact Point
When creating a Web Form or a Windows Form, you drag controls onto your designer. With Workflows, you drag activities. To create the initial contact point, for instance, you drag a ReceiveAndSendReply activity onto the designer from the Messaging section of the Toolbox. A ReceiveAndSendReply activity actually adds two activities to your workflow: a Receive (which accepts a client's inbound message) and a Reply activity (which sends a message back to the client). Together these two activities define a method on your service.
After adding the ReceiveAndSendReply activity, you should set the Receive activity's OperationName property to the name you want to use for the method (I used "SubmitComplaint"). Because this is the start point for your workflow, you must set the Receive activity's CanCreateInstance property to True. You should also replace the default ServiceContractName property with a unique identifier (I used "http://www.phvis.com/complaintmanagement") and set the activities' DisplayName properties to something meaningful ("Receive Initial Complaint" and "Respond to Initial Complaint," in my case).
I used the same format for both the incoming and the outgoing messages: A ComplaintId (not used in the incoming message but set in the outgoing message), a ComplaintText (a string) and a Status (an enumerated value set to InProgress in the outgoing message). The easiest way to define a message is to create a class with a property for each part of the message:
Public Class Complaint
Public Property ComplaintId As String
Public Property ComplaintText As String
Public Property Status As ComplaintStatus
After rebuilding your project you must tie your message to the Receive activity. In the Workflow designer, click on the Define line in the Receive activity to display its Content Definition dialog. The incoming message will be passed to the Receive activity as a parameter, so you'll need to select the dialog's Parameters option to tie your message class to the activity. In the blank line that's displayed, first give your parameter a name (I used "Complaint"). To set the parameter's data type to your message class, click in the line's Type column and, from its drop-down list, select Browse for Types to drill down to your message class.
You'll need to refer to the complaint you receive later in the process, so you need to assign the incoming object to a "workflow-level" variable. To do that, set the dialog's AssignTo column to the name for some variable (I used "CurrentComplaint"). Windows Workflow will take care of setting up the service, adding this method to it, accepting the message in the service, converting the message to your class and assigning the class to the variable.
However, after closing the Content Definition dialog, you do need to define to the workflow any variables you used in the Content Definition dialog. To do that, click on the Variables link at the bottom of the designer to display the list of variables you're using in your workflow. Add your new variable to the list, set the Type field to your class name, and set the variable's scope to Sequential Service (that makes the variable accessible from everywhere in your service).
Adding Processing and Sending a Reply
You can insert as many additional activities as you want between the Receive and the SendReply for any processing that's required in the initial contact. In the ComplaintManagement system, for instance, I need to assign a ComplaintId so I can return it as part of the response message.
If all you're doing is making an assignment, you can drag an Assign activity from the Primitives section of the Toolbox onto your designer and drop it between the Receive and SendReply activities. To assign a unique GUID to the ComplaintId property of the CurrentComplaint, I enter CurrentComplaint.ComplaintId in the left-hand box of the Assign activity and GUID.NewGuid.ToString in the right-hand box.
The next step in setting up the contact point is to define the outgoing message for the SendReply activity. Begin that by clicking on the SendReply's View Message link to display its Content Definition dialog. If all you wanted to return was some text, you could set the Message Data to a literal string (for example, "Your complaint has been received") and set the Type field to string. You can incorporate some Visual Basic code if you want by, for instance, concatenating in the ComplaintId that your service has generated (for example, "Your complaint has been received" & CurrentComplaint.ComplaintId).
I'd rather return the Complaint object I've been modifying. To do that, still in the SendReply's Content Definition dialog, I set the MessageData to the CurrentComplaint object and the Type field to Complaint.
Effectively, at this point, you've had the designer generate this code for you:
Dim CurrentComplaint As Complaint
Public Function Receive(Complaint As Complaint) As Complaint
CurrentComplaint = Complaint
CurrentComplaint.ComplaintId = Guid.NewGuid.ToString
If you're thinking it would've been easier to type that code in -- you're probably right. But, by letting the designer generate this code for you, Workflow is able to save state information for your workflow when the process is waiting for input (or when IIS shuts down). You don't need to save the CurrentComplaint object, for instance, because Workflow will take care of keeping it around for the life of the complaint.
Integrating Custom Code
This has all been very code-free, but any real-world application will need custom code. In a Windows Form or Web Form you add custom code to form events. Much of that code will typically call methods on other objects. In a Workflow, you drag an InvokeMethod activity from the Primitives section of the Toolbox. This activity lets you call methods from classes you define in your workflow or from classes in libraries to which you've added references.
The InvokeMethod activity has three boxes. You use the top box if you're calling a static/shared method on a class -- just enter the class name. If you need to instantiate the class, use the middle box and enter the code to instantiate the class (for example, "New ComplaintFactory"). You always use the bottom box to enter the name of the method to call.
For instance, I might have a class with a method like this to send an e-mail to the person responsible for managing the customer:
Public Class MailManager
Public Shared Sub SendEMail(CustomerId As String, Subject As String,
Text As String)
'code to send email
Because this is a static method, I set the InvokeMethod activity's top box to the class name (MailManager) and the bottom box to SendEMail (the name of the method).
Most methods, like this one, require parameters to be passed to them. You specify those parameters from the InvokeMethod's Properties Window. Clicking on the builder button (the one with three dots) in the Parameters line displays the Parameters dialog, where you specify the values to pass to the method's parameters.
To call my SendEMail method, I add three lines to the dialog, all with their direction set to In and their type to String. While those settings are identical for all three parameters, their values are different: The first parameter is passed CurrentComplaint.CustomerId, while the other two parameters are passed string literals for the subject and the body of the e-mail (for instance, "Complaint Received" & CurrentComplaint.CustomerId & "has submitted complaint" & CurrentComplaint.ComplaintId). If I need to capture the result of calling the method, I'd define another variable in the Variables pane at the bottom of the designer and put its name in the InvokeMethod's Result property.
If you have some code that you'll use frequently (either in this workflow or in others) you can create your own custom activities to simplify calling that code.
Calling Your Service
With your initial contact point created, you can start testing your workflow. To test the ComplaintManagement backbone, I add a Test project to my Solution. I then add a reference to my ComplaintManagement project using Add | Add Service Reference, just as I would for any other service. Adding a service reference generates all the necessary classes I need to call the method defined by my initial ReceiveAndSendReply activity. These include a client class for handling communication with my service, a copy of my message class and a wrapper class that holds the method's message class.
To call the method that starts the complaint process, I first create an instance of my message class and set its properties:
Dim com As New ComplaintManagement.Complaint
com.ComplaintMember = "My test complaint"
com.CustomerId = "PHVIS"
Each method has a wrapper class that holds the message class, so my next step is to instantiate that wrapper class and tuck my message object into the property with the same name as my message class:
Dim rqReceive As New ComplaintManagement.SubmitComplaint
rqReceive.Complaint = com
Finally, I create an instance of the service's client class, call my method (SubmitComplaint) and pass the wrapper class, catching any response:
Dim csc As New ComplaintManagement.ServiceClient
Dim resComplaint As Complaint
resComplaint = csc.SubmitComplaint(rqReceive)
Once I know that my contact point works, I can go back to extending my service.
Accessing Other Services
The ComplaintManagement system is going to need information about the customer and -- provided that the CustomerManagement system has been set up with the appropriate services -- getting that information can be easy (even easier than using InvokeMethod to call a method).
As with any other application, the first step in accessing a service from a workflow is to add a reference to the service using Add | Service Reference. After adding the service reference, rebuild the project and -- voilá! -- all of the service's methods appear at the top of the designer's Toolbox as new activities. When you drag one of these new activities onto the designer, you'll find it has a property for each input parameter; these are considerably easier to set than using the Parameters dialog in the InvokeMethod activity. There's also a property for you to enter the name of a variable that will catch any result the method returns.
My CustomerManagement service has a method called GetCustomerByID that accepts a CustomerId and returns a Customer object. So, after adding a reference to the service, I'll have a new activity in the Toolbox called GetCustomerById.
I just drag that new custom activity onto the designer and set its CustomerId property (the input parameter) to CurrentComplaint.CustomerId and the GetCustomerByIdResult property to the name of the variable I want to hold the returned Customer object (ComplainingCustomer). As with previous variables I've used in the workflow, I'll need to add the ComplainingCustomer variable to the Variables list at the bottom of the designer and set its type and scope. When the workflow process gets to this point, it will call the CustomerManagement service's GetCustomerById method, wait for the response and put the result in the ComplainingCustomer variable.
If, for some reason, you can't set a reference to a Web service, you can still call it: Just use the SendAndReceiveReply activity in the Messaging section of the Toolbox.
Of course, calling another service raises the issue of how you handle another long-running service -- you don't want your workflow to hang for the hours, days or weeks that some other service might need to respond. In that case, you'd need to set up another contact point -- a method defined using ReceiveAndSendReply -- for that other service to call to pass your workflow the result you're waiting for.
Waiting for Input
And that brings us back to the stop/start nature of a long-running process. The reason you have a long-running process is because for long periods of time, your process is waiting for input from a client or some other process.
One way to wait for input is to drag a While activity onto your designer. The While activity has a Condition property that accepts a test of some kind -- until that test is passed, the workflow will wait at that point in the process. For the complaint system, I set my While activity's condition to "CurrentComplaint.Status <>
ComplaintStatus.Resolved" to cause the process to wait until the complaint is resolved. If you click the builder button in the While activity's Condition property, you'll get an Expression Builder dialog that provides some IntelliSense support for creating the condition.
The bulk of the While activity is a block that you can add more activities to -- presumably, the activities that will, at some point, cause the condition to return True and allow the process to continue. Often, this is another contact point that a client (or another process) would use to provide input to the workflow. In the ComplaintManagement system, for instance, I add another ReceiveAndSendReply activity to create an UpdateComplaintStatus method on my service. This method is called by anyone updating the complaint: It accepts a ComplaintId and a ComplaintStatus and updates the CurrentComplaint's status with the status passed into it.
To set that up, in the Content Definition dialog for the Receive activity, I define the two parameters. In the dialog, I also specify that the ComplaintStatus parameter is to be assigned to the CurrentComplaint's Status property. In the Content Definition dialog for the SendReply activity, I use its Message option to return some text (something like: "Complaint" & CurrentComplaint.ComplaintId & "'s status has been updated to" & CurrentComplaint.Status).
This is the simplest way to cause the workflow to wait until the next input shows up. You could also use a Pick activity inside the While activity. A Pick activity includes the ability to have the activity time-out if the wait period goes on for too long.
Keeping Complaints Separate
Each new call to your initial contact point launches a new instance of the workflow (this is why you set the CanCreateInstance property on the initial contact point's Receive activity to True). So, when a client calls the UpdateComplaintStatus method, Windows Workflow has to figure out which instance of your workflow to retrieve so that the correct CurrentComplaint will be updated -- a problem known as correlation.
There are a couple of ways of handling correlation, but the most obvious way is to identify some value (unique to each workflow instance) that's provided as part of the incoming message at every contact point. Windows Workflow can use this value to retrieve the correct instance of your workflow. For the ComplaintManagement system, the obvious choice is the ComplaintId.
Workflow recognizes that the ComplaintId may have different names in different messages (ComplaintId, ComplId, ComplaintIdentifier and so on). To handle this, you initialize a correlation handle at the first contact point and then tie values in the incoming messages used at later contact points to that handle.
Correlation handles can only be initialized in some activities -- primarily Receive and SendReply activities. In the first contact point (my SubmitComplaint method), the ComplaintId isn't available in the Receive activity -- but it is available in the SendReply, so that's where I initialize my correlation handle. In the SendReply activity, I click on its CorrelationInitializers property to display the Add Correlation Initializers dialog.
This dialog box has its own bizarre UI conventions, so it does take some getting used to. First, click on the Add Initializers line to enable the two drop-down lists in the dialog box. Leave the top drop-down list set at "Query correlation initializer" and, from the bottom drop-down list, select the identifier for the handle (in my case, ComplaintId). You should also replace the default name of "key1" with something more meaningful (I used "ComplaintHandle").
The next step is to tie the subsequent contact points to the correlation handle you've initialized in the first contact point. In the complaint system, the next contact point is the UpdateComplaintStatus. In the contact point's Receive activity, set its CorrelatesWith property to the handle you created (ComplaintHandle, in my case). Then you must specify which field in the incoming message holds the matching value. For that, click on the CorrelatesOn property's builder button to display the CorrelatesOn Definition dialog. From the dialog's drop-down list, select the field in the incoming message with the value that matches your correlation handle (you should also replace the default name for this item).
There's no point in keeping every instance of your workflow in memory -- most of the time your workflows will be waiting for something to happen. Also, because your workflow may take hours, days or weeks to terminate, your IIS host may be shut down during that time. Workflow persistence lets you save your workflow in a database when it's not doing anything and, as an extra benefit, survive IIS being shut down. Because you've been defining all your key variables through the designer, Workflow can do an effective job of saving your workflow's state (the corollary of this is that you should store anything you'll need later in the process in variables you've defined in the designer).
Before taking advantage of workflow persistence, you'll need to add the necessary tables to some installation of SQL Server or SQL Server Express. You'll need to use SQL Server Management Studio to add the SqlWorkflowInstance schema that ships with the .NET Framework to SQL Server.Your next step is to add sqlWorkflowInstance elements to your web.config file to configure your workflow to use persistence. A typical set looks like this:
"...connection string for server with persistence tables..."
You can force the state of a Workflow to be persisted by dragging Persist activities onto your designer. However, it's probably easier to add a workflowIdle element to your web.config file to cause the workflow's state to be saved when the workflow is waiting. This example causes the workflow's state to be saved and unloaded after it has been idle for 15 minutes:
And, with that, you've created the backbone of your application. If you want, you can simply define the contact points for your workflow and then extract the resulting service's WSDL. That WSDL can be used to generate a dummy application that can be used to build and test clients. This strategy allows other teams to start building clients and integrating your workflow into their applications, while you can go on to finish building the rest of your backbone. With any luck, those other teams will be stuck with creating all of the clients while you're busy fleshing out the workflow.