Workflow and Web Services


Web services are very important in the concept of connected systems. As such, web services play a large role in Windows Workflow Foundation. There are hooks into the workflow platform that allow external web services to be called as well as allow clients to call workflows as web services. The following sections cover these concepts and the relevant pieces of the workflow platform.

Web Service Activities

This section discusses the web-services-related activities with which you should become familiar.

The InvokeWebService Activity

This activity enables a workflow to call external web services. An instance of the InvokeWebService activity must be associated with one particular web service and one particular method on that web service. Parameters that need to be passed to the web service can be bound to properties on the activity. In addition, any values returned by the web service can be bound to specified properties in the workflow.

The WebServiceInput and WebServiceOutput Activities

These two activities are always found together in a workflow. The WebServiceInput and WebServiceOutput activities are used to facilitate exposing workflows as web services. The WebServiceInput activity is generally found at the top of the workflow and defines the entry point of the web service. Conversely, the WebServiceOutput activity defines the exit point of the web service. Any values that are to be returned to the client are configured on this activity.

The WebServiceFault Activity

The WebServiceFault activity is used when the workflow is acting as a web service. If you need to raise an exception to the client in the form of a SOAP fault, the WebServiceFault activity is the solution. This activity does not behave like a throw statement in code, in that the workflow’s execution can continue after a WebServiceFault activity is executed. However, the execution of this type of activity produces the same result on the client as if you called a throw statement in an ASMX web service.

Calling Web Services inside a Workflow

Before learning how to call a web service from a workflow, you need a web service to call. This section describes an example web service and workflow scenario for admitting a patient to a hospital. The simple web service will have a method called AdmitPatient that will be passed basic patient data. This method will also return an instance of a class called PatientRecord. The web service will have a method called GetPatientStatus that would, in a fully implemented application, return a string representing a patient’s status in his or her hospital stay. However, in this example, it will simply return a dummy string.

To create the web service, open Visual Studio 2005 and select File image from book New image from book Web Site. In the New Web Site dialog box, select the ASP.NET Web Service option from the project templates. After the web site project has been created, change the web service’s name to PatientService.asmx. Finally, add the following code to implement the desired functionality:

  [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class PatientService : WebService {     public PatientService()     {     }     [WebMethod]     public PatientRecord AdmitPatient(string patientId, string name,         string reasonForVisit)     {         Console.WriteLine("Patient {0} was admitted.", name);         return new PatientRecord(patientId, name, reasonForVisit);     }     [WebMethod]     public string GetPatientStatus(string patientId)     {         if (String.IsNullOrEmpty(patientId))             throw new ArgumentException("Patient ID was null or empty.",                 "patientId");         return "Dummy status";     } } 

Next, create the PatientRecord class by using the following code:

  public class PatientRecord {     public string PatientId;     public string Name;     public string ReasonForVisit;     public DateTime TimeAdmitted;     public PatientRecord()         : this(String.Empty, String.Empty, String.Empty)     {     }     public PatientRecord(string patientId, string name, string reasonForVisit)     {         this.PatientId = patientId;         this.Name = name;         this.ReasonForVisit = reasonForVisit;         this.TimeAdmitted = DateTime.Now;     } } 

Now that the patient web service has been created, it’s time to implement the workflow that will use it. First, create a new Sequential Workflow Console Application project, and name it CallWebService. After your project has been created, change the name of the default workflow from Workflow1 to PatientWorkflow. Now you’re ready to modify the workflow to call the previously created web service.

The first web service call in this workflow should be to the AdmitPatient method. To call the web service, the workflow needs an instance of the InvokeWebService activity, so drag this activity from the Toolbox onto the workflow designer. The Add Web Reference dialog box appears, prompting you to enter the necessary information about the web service you want to call (see Figure 14-1).

image from book
Figure 14-1

Because the web service you created uses the filesystem Web Site type, it is easier to select the Web services in this solution link (shown in Figure 14-1) rather than start the web service and then point to the URL. After clicking the link, you are presented with a list of web services found in the current solution. Select PatientService from this list. You are presented with a list of web methods exposed by the service (which should be AdmitPatient and GetPatientStatus), and you can give a new name to the web reference you are about to create. Enter PatientWebReference in the text box, and click the Add Reference button.

When you add a web reference to your workflow project, a web service proxy class is created. You can see this class by first clicking the Show All Files button in the Solution Explorer and then expanding the Web References folder under the workflow project. Next, expand PatientWebReference and then Reference.map. Under Reference.map is a file called Reference.cs. This is the proxy class that makes the call to the patient web service. You don’t need to make any changes to this file - it already contains everything it needs to make the call to your web service.

Next, you need to configure the InvokeWebService activity that you placed in the workflow. The first thing you should do is give the activity instance a more meaningful name. Change the name property to callAdmitPatient. Notice that the activity has the red exclamation icon attached to it, indicating that it is not configured fully or correctly. However, the configuration is already partly there because you ran the web reference wizard, as validated by the ProxyClass and URL properties already being set on this screen. Currently, the only property with an error is the MethodName property, which should point to a method on the configured web service. Because this activity needs to call the AdmitPatient method, try selecting that option from the drop-down list of the property.

As you can see, setting the MethodName property made things worse. Now there are four properties with errors: (ReturnValue), name, patientId, and reasonForVisit. These are the parameters and the return value associated with the AdmitPatient method. To correct the configuration errors, you need to bind each property to a field in the workflow class. One option is to manually create fields and properties in the code-beside class and then select them in the Properties Explorer. Another option, and the one chosen for this example, is to let the binding dialog box create the appropriate members and bind to them automatically. To do this, click the ellipsis button on each property that has an error. This displays a binding dialog box where you add the property members. Figure 14-2 shows what this dialog box looks like after you click the ellipsis in the (ReturnValue) property, click the Bind to a new member tab, enter patientRecord in the text box, click the Create Field radio button, and then click the OK button. As you can see, a new field in the code-beside class is generated, called patientRecord. In addition, the (ReturnValue) property is automatically bound to the new field.

image from book
Figure 14-2

So far, the return value of the web service method has been accounted for. However, the values to be passed to the method have not yet been set. These can be created and bound the same way in which the return value was created and bound. For name, patientId, and reasonForVisit, create new fields that will be bound to. Your code-beside class should look like the following:

  public sealed partial class PatientWorkflow : SequentialWorkflowActivity {     public PatientWorkflow()     {         InitializeComponent();     }     public PatientRecord patientRecord = new PatientRecord();     public String name = default(System.String);     public String patientId = default(System.String);     public String reasonForVisit = default(System.String); } 

Even though the InvokeWebService activity is now correctly configured, the workflow doesn’t do anything meaningful because all the parameters have a default value of an empty string. To remedy this, add a Code activity to the top of the workflow, and add the following code to its ExecuteCode event handler:

  private void setValues_ExecuteCode(object sender, EventArgs e) {     this.name = "Joe Blow";     this.patientId = "123";     this.reasonForVisit = "Broken leg"; } 

Now when the workflow is executed, the values set with the Code activity are passed to the web service. In addition, when the web service method returns, the patientRecord class field is set. Give it a try.

Now, to make the workflow a little more interesting, add another InvokeWebService activity below the first one, and name it callGetPatientStatus. However, this time when the Add Web Reference dialog box appears, simply cancel it before pointing to a web service. You don’t need to add another web reference because you can reuse the proxy generated previously. To do this, select the PatientService class from the ProxyClass property of the activity. Next, you need to configure the activity to point to the GetPatientStatus method and bind the patientId parameter and the return value. Bind the patientId parameter to the PatientId property of the patientRecord class field. Figure 14-3 shows the binding dialog box that is accessed from the patientId property in the properties window. Finally, for the (ReturnValue) property, bind to a new class field called patientStatus.

image from book
Figure 14-3

To round out this tutorial, add a new Code activity to the end of the workflow, and use the following code in the ExecuteCode event handler:

  private void printStatus_ExecuteCode(object sender, EventArgs e) {     Console.WriteLine("The patient's status: " + patientStatus); } 

After running the workflow, you should see the message shown in Figure 14-4.

image from book
Figure 14-4

Exposing Workflows as Web Services

There are two methods you can use to expose a workflow as a web service. Out of the box, Visual Studio provides you with a wizard of sorts to automatically expose a workflow as a web service, as long as the workflow meets the prerequisites discussed in the following section. Alternatively, you can write custom code that allows your workflow to be called through a web service. The following sections discuss both options.

Using the Wizard

Before getting into the particulars of exposing a workflow as a web service, you need to create a workflow. To keep things focused on the topic at hand, the workflow should simply take a person’s name and generate a message.

First, create a Sequential Workflow Library project, and rename the default workflow WebServiceWorkflow.cs. Next, add two fields to the workflow’s code-beside class: a string called name and a string called message. Now add a single Code activity to the workflow and call it createMessage. In that activity’s ExecuteCode event handler, generate a message based on the name field. After you’ve done all this, the code in the code-beside class should look like the following:

  public sealed partial class WebServiceWorkflow: SequentialWorkflowActivity {     public string name = String.Empty;     public string message = String.Empty;     public WebServiceWorkflow()     {         InitializeComponent();     }     private void createMessage_ExecuteCode(object sender, EventArgs e)     {         this.message = "Hello " + this.name;     } } 

Now try running the web service wizard from within Visual Studio. To do this, right-click the project in the Solution Explorer and select the Publish as Web Service option. The error message shown in Figure 14-5 is displayed.

image from book
Figure 14-5

The message tells you that the workflow is not yet configured properly to expose it as a web service. You need to add a few activities to the workflow definition before the wizard is able to run. The first thing you need to do is add a WebServiceInput activity to the top of the workflow.

Right off the bat, you can see that the activity is not configured correctly by default. The first property noted as having an error is the IsActivating property. When creating workflows that will be called as external web services, you need to set this property to true. This indicates that the action of the web service being called will create a new instance of the workflow.

After you set IsActivating to true, the InterfaceType property shows that it has an error. As with other activities (such as the HandleExternalEvent and CallExternalMethod activities), the WebServiceInput activity needs an interface to define its public methods. Develop a new interface in the workflow project called IWebService, and make it look like the following code:

  public interface IWebService {     string GetMessage(string name); } 

This interface says that the web service will have one method, called GetMessage, that will take a string as a parameter and also return a string. Now you can set the InterfaceType property of the WebServiceInput activity. After doing that, you need to select the GetMessage method for the MethodName property. To fully configure this activity, you also need to bind the name property to the name field of the code-beside class.

Now that you have defined the entry point to the web service, you need to define the exit point as well. This involves adding a WebServiceOutput activity after the initial Code activity. You need to do two things to fully configure this new activity. First, you need to point to the WebServiceInput activity using the InputActivityName property. Doing this links the input and output of the web service to form a cohesive web method. Then you need to bind the (ReturnValue) property to the message field of the code-beside class that will hold the message built by the Code activity.

Now the web service wizard is finally ready to be run without any errors. Right-click the workflow project, and select Publish as Web Service. This automatically creates a new web service project in the same solution as the workflow project. The project includes a new ASMX web service, a Web.config file, and an assembly that can be found in the bin folder. You are also presented with a message indicating that the process was successful.

Take a look at the Web.config file created in the new website project. The following is an excerpt of the markup in that file:

  <?xml version="1.0"?> <configuration>    ...    <system.web>       ...       <httpModules>          <add           type="System.Workflow.Runtime.Hosting.WorkflowWebHostingModule,                 System.Workflow.Runtime, Version=3.0.0.0, Culture=neutral,                 PublicKeyToken=31bf3856ad364e35"           name="WorkflowHost"/>       </httpModules>    </system.web> </configuration> 

The preceding portion of the Web.config file adds an HTTP module to the ASP.NET runtime. This HTTP module, called WorkflowWebHostingModule, is a class built into the Windows Workflow Foundation API. This class is responsible for managing the session between the web service client and the web service itself. The module simply checks for a cookie from the client upon request. This cookie’s value should be a workflow instance ID. If the cookie does not exist in the object representing the client’s request, the request is considered a new call to the workflow. After the workflow instance is created, a cookie is added to the response object so that it can be returned to the client. Again, the response cookie contains the created workflow’s instance ID. (Obviously, the calling client needs to be able to handle the cookies passed to and from the web service.)

Although the WorkflowWebHostingModule provides an out-of-the-box method for associating client calls to a specific workflow instance, you can develop your own HTTP module to replace this functionality. For example, if you would rather use the query string to associate calls, your module can implement that behavior instead. Take a look at the implementation of WorkflowWebHostingModule in Lutz’s Reflector to get an idea of what your custom class needs to provide a full and valid implementation.

Although using the wizard to expose a workflow as a web service has some advantages, such as a quick and easy way to allow a workflow to be called by the outside world, there are some things to consider before going down this path. First, you need to understand what the wizard is doing behind the scenes.

When you run the wizard, the ASMX that is created is very simple. All the magic can be found in the assembly that was generated during the process. This assembly contains a few .NET interfaces and classes, including the web service interface that was defined in the workflow project as well as the workflow class itself.

In addition, the assembly contains a class called Settings that is used to access configuration data during the execution of the workflow. You don’t need to worry too much about this class. The interesting code is found in a custom-created class, which follows a <WorkflowClassName>_WebService naming convention. In this example, the class name is WebServiceWorkflow_WebService.

You can inspect the contents of this class by opening the generated assembly in Lutz’s Reflector. The custom class inherits from a class called WorkflowWebService, which is defined in System.Workflow.Activities. This class inherits from System.Web.Services.WebService and contains the base functionality common to all workflows exposed as web services. In addition, the derived class contains the code to call the methods on the web service. In this example, there is a method that looks like the following code, which is executed when the web service is called:

  [WebMethod(Description="GetMessage", EnableSession=false)] public virtual string GetMessage(string name) {     return (string) base.Invoke(typeof(IWebService),         "GetMessage", true, new object[] { name })[0]; } 

Because the wizard for exposing a workflow as web service creates this assembly with a compiled version of the workflow, the first limitation of this method is that you must be using a workflow defined with code - workflows defined with XAML will not work. Depending on how your application’s workflows are being defined, this may be a deal breaker.

Another limitation of using the web service wizard relates to the WorkflowWebHostingModule discussed previously. The out-of-the-box implementation requires cookies to be used on the client to associate individual requests with a specific workflow instance. This can be a disadvantage if you do not have control over the calling client.

Finally, using the web service wizard provides a simple and easy way to expose your workflow to the outside world. However, the flexibility and options provided to you are limited. For example, if you need specific control over things such as the web services metadata or security, the wizard may not meet your exact needs.

Exposing the Workflow Yourself

If the wizard for exposing a workflow as a web service does not provide the exact functionality you are looking for, you can develop a custom ASP.NET web service and host the workflow runtime yourself. The goal of this section is to show you how to integrate Windows Workflow Foundation into a standard ASP.NET web service. The sample workflow shows a scenario that is identical to the workflow scenario used in the previous section covering the web service wizard. A user passes a string representing his or her name, and a personalized message is returned.

As discussed in Chapter 13, there are issues related specifically to ASP.NET that you must consider. Remember that the default workflows scheduler service does not play well with ASP.NET and IIS because it spawns threads freely. Web services are hosted in IIS the same way ASP.NET web forms are, so the same issues apply here. Therefore, you should use the ManualWorkflowSchedulerService in your web service-exposed workflows. The Web.config file shown in the following code adds the manual scheduler to the runtime so that this step does not have to take place in code:

  <?xml version="1.0"?> <configuration>    <configSections>       <section        name="WorkflowRuntime"        type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection,              System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,              PublicKeyToken=31bf3856ad364e35"/>    </configSections>    <WorkflowRuntime>       <Services>          <add           type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService,                 System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,                 PublicKeyToken=31bf3856ad364e35"         useActiveTimers="true"/>       </Services>    </WorkflowRuntime>    <system.web>       ...    </system.web> </configuration> 

The next block of code handles the workflow runtime as well as the task of retrieving messages back from completed workflows:

  public class Global : HttpApplication {     private Dictionary<Guid, string> completedMessages =         new Dictionary<Guid, string>();     public void Application_Start(object sender, EventArgs e)     {         // create "the" instance of the workflow runtime         // for this ASP.NET application         WorkflowRuntime workflowRuntime =             new WorkflowRuntime("WorkflowRuntime");         // wire the workflow completed event so we can access the output message         workflowRuntime.WorkflowCompleted +=             new EventHandler<WorkflowCompletedEventArgs>(                 workflowRuntime_WorkflowCompleted);         workflowRuntime.StartRuntime();         // add the runtime to the application's global state         Application["WorkflowRuntime"] = workflowRuntime;          // save the completed messages dictionary collection         Application["CompletedMessages"] = completedMessages;     }     public void Application_End(object sender, EventArgs e)     {         WorkflowRuntime workflowRuntime =             Application["WorkflowRuntime"] as WorkflowRuntime;         workflowRuntime.StopRuntime();         workflowRuntime.Dispose();     }     ... } 

The Global class in this code is the code-behind for the Global.asax file. This class is extremely important to the ASP.NET infrastructure because it handles events related to the ASP.NET application’s instance, such as its starting and ending. Because an ASP.NET application needs only one instance of the workflow runtime, the Application_Start method is a perfect place to instantiate the runtime and place it in a globally accessible location.

The Application_Start method also takes care of wiring the WorkflowCompleted event so that messages generated in workflow instances can be accessed and also saved to a globally accessible location. (That method is covered in a moment.) Both the workflow runtime instance and the completedMessages Dictionary<Guid, string> instance are stored in the Application object.

The Application_End method is used to access the single runtime instance and stop it. This ensures that the workflow runtime’s resources are cleaned up when the ASP.NET application has ended.

The following code shows the WorkflowCompleted event handler mentioned earlier. This method uses the completedMessages collection to store the message returned using each completed workflow’s output parameters. Notice that the collection uses a Guid representing a workflow instance’s ID as its key. Also remember that the completedMessages object was added to the application’s global variables.

 public class Global : HttpApplication {     ...     private void workflowRuntime_WorkflowCompleted(object sender,         WorkflowCompletedEventArgs e)     {         this.completedMessages.Add(e.WorkflowInstance.InstanceId,             e.OutputParameters["Message"] as string);     }     ... }

Finally, the following code shows the implantation of the ASP.NET web service itself. The Service class inherits from System.Web.Services.WebService and is decorated with the WebService attribute. This is the standard way to declare an ASP.NET web service.

  [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class Service : System.Web.Services.WebService {     private WorkflowRuntime WorkflowRuntime     {         get         {             return Application["WorkflowRuntime"] as WorkflowRuntime;         }     }     private Dictionary<Guid, string> CompletedMessages     {         get         {             return Application["CompletedMessages"] as Dictionary<Guid, string>;         }     }     public Service()     {     }     [WebMethod]     public string GetMessage(string name)     {         WorkflowRuntime runtime = this.WorkflowRuntime;         // obtain a reference to the manual scheduler service         // you will need this to start the workflow's execution         ManualWorkflowSchedulerService scheduler =             (ManualWorkflowSchedulerService)runtime.GetService(                 typeof(ManualWorkflowSchedulerService));         // create the parameters to be passed to the workflow         Dictionary<string, object> parms = new Dictionary<string, object>();         parms.Add("Name", name);         WorkflowInstance instance = runtime.CreateWorkflow(             typeof(GetMessageWorkflow.GetMessageWorkflow), parms);         instance.Start();         // when using the manual workflow scheduler, you must manually         // tell the workflow to run         scheduler.RunWorkflow(instance.InstanceId);         // the workflow is completed; obtain the message through the         // dictionary object which was populated in the "Global" class         string message = this.CompletedMessages[instance.InstanceId];         // now that you have the message it can be discarded         // from the dictionary         this.CompletedMessages.Remove(instance.InstanceId);         return message;     } } 

To make it easier to access the global variables defined in the Global class, a couple of properties have been defined at the top of the Service class. This allows other methods in this class to access the workflow runtime and the messages returned from completed workflow instances.

This web service has only one web method: GetMessage. This method is decorated with the WebMethod attribute, which is the standard way to define a web service method in ASP.NET. It is in this method that a new workflow instance is created, executed, and completed. After completion, the returned message is accessed using the CompletedMessage property. Remember, this collection is populated in the WorkflowCompleted event-handler method of the Global class.

The ManualWorkflowSchedulerService instance has to be explicitly accessed using the runtime’s GetService method so that it can be used to tell the workflow instance to run. This is a necessary step when using this service. Because the manual scheduler causes workflow instances to be run synchronously, you know the instance has completed when the scheduler.RunWorkflow returns. Therefore, the completed instance’s message can be found in the dictionary collection.



Professional Windows Workflow Foundation
Professional Windows Workflow Foundation
ISBN: 0470053860
EAN: 2147483647
Year: 2004
Pages: 118
Authors: Todd Kitta

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net