January 2009

Volume 24 Number 01

ASP.NET Workflow - Web Apps that Support Long-Running Operations

By Michael Kennedy | January 2009

.img3 { float: left; margin: 5px 10px 10px 0px; width:245px; font-weight: normal; font-size: 14px; color: #003399; font-family: "Segoe UI", Arial; }

Code download available

This article discusses:

  • Process-independent workflows
  • Synchronous and asynchronous activities
  • Workflows, activities, and persistence
  • Integrating with ASP.NET
This article uses the following technologies:
Windows Workflow Foundation, ASP.NET

Contents

Harnessing Workflows
Synchronous and Asynchronous Activities
What Exactly Do You Mean by Idle?
Making Synchronous Tasks Asynchronous
Workflows and Activities
Persistence
Making It Real
Integrating with ASP.NET
A Few Things to Ponder
Bringing It All Together

Software developers are often askedto build Web applications that support long-running operations. One example is the checkout process of an online store, which might take several minutes to complete. While that is a long-running operation by some standards, in this article I will explore long-running operations of an entirely different scale: operations that can take days, weeks, or even months to complete. One example of such an operation is the application process for employment, which can involve interactions between several people and the exchange of many real documents.

First, let's consider a more benign problem from an ASP.NET standpoint: you need to architect a solution for a checkout operation at an online store. There are special considerations to make for this solution because of its duration. For example, you may choose to store the shopping cart data in an ASP.NET session. You may even choose to move that session state into an out-of-process state server or database to allow for updates to the site and load balancing. Even so, you'll find all the tools you need to solve this problem easily are provided for by ASP.NET itself.

However, when the duration of the operation grows longer than the typical ASP.NET session duration (20 minutes) or requires multiple actors (as in my hiring example), ASP.NET does not offer sufficient support. You may recall that the ASP.NET worker processes automatically shut down on idle and periodically recycle themselves. This will cause big problems for long-running operations, as state held within those processes will be lost.

Imagine for a moment that you were to host these very long running operations inside of a single process. Clearly the ASP.NET worker process is not suitable for them for the reasons just outlined. So maybe it would be possible to create a windows service whose sole responsibility will to be to execute these operations. If you never restart this service, you'll be closer to a solution than if using ASP.NET directly, since simply having a service process that does not automatically restart theoretically ensures that I will not lose the state of my long running operation.

But will this really solve the problem? Probably not. What if the server requires load balancing? That becomes very difficult when you're tied to a single process. Worse, what if you have to reboot the server or the process crashes? Then all of the operations that have been running will be lost.

In fact, when operations take days or weeks to complete, you need a solution that is independent of the lifecycle of the process that is executing it. This is true in general, but it is especially important for ASP.NET Web applications.

Harnessing Workflows

Windows Workflow Foundation (WF) may not be the technology that comes to mind for building Web applications. However, there are several key features provided by WF that make a workflow solution worth considering. WF gives you the ability to achieve process independence for long-running operations by unloading idle workflows entirely from the process space and automatically reloading them into the active process when they're no longer idle (see Figure 1). Using WF, you can rise above the nondeterministic lifecycle of the ASP.NET worker process and provide for long-running operations inside of Web application.

Figure 1 Workflows Preserve Operations across Process Instances

Two key features of WF combine to provide this capability. First, asynchronous activities signal to the workflow runtime that the workflow is idle while waiting on an external event. Second, a persistence service will unload idle workflows from the process, save it to a durable storage location such as a database, and reload the workflows when they are ready to run again.

This process independence has other benefits. It provides a simple means of load balancing as well as durability—fault tolerance in the face of process or server failures.

Synchronous and Asynchronous Activities

Activities are the atomic elements of WF. All workflows are built from activities in a pattern resembling the composite design pattern. In fact, workflows themselves are simply specialized activities. These activities can be classified as either synchronous or asynchronous. A synchronous activity executes all of its instructions from start to end.

An example of a synchronous activity might be one that computes tax given an order in an online store. Let's think about how such an activity might be implemented. Like most WF activities, the majority of work happens in the overridden Execute method. The steps of that method might go something like this:

  1. Obtain the order data from a previous activity. This is usually done through data binding, and you will see an example of this later.
  2. Look up the customer associated with the order from a database.
  3. Look up the tax rate in a database based on the customer's location.
  4. Do some simple math using the tax rate and the order items associated with order.
  5. Store the total tax in a property that subsequent activities can bind to in order to complete the checkout process.
  6. Signal to the workflow runtime that this activity is completed by returning the Completed status flag from the Execute methods.

It is important to recognize that nowhere are you waiting. You are always working. The Execute method simply runs through the steps and finishes in short order. This is the essence of a synchronous activity: all the work is done in the Execute method.

Asynchronous activities are different. Unlike their synchronous counterparts, asynchronous activities execute for some period of time and then wait on an external stimulus. While waiting, the activities become idle. When the event occurs, the activity resumes operation and finishes executing.

An example of an asynchronous activity is a step in the hiring process when an application for a position needs to be reviewed by a manager. Consider what might happen if this manager is on vacation and will not get a chance to review the application until next week. It is completely unreasonable to block in the middle of the Execute method while waiting for this response. When software waits on a person, it might have to wait for a long time. You need to consider this in your design.

What Exactly Do You Mean by Idle?

At this point English semantics and architectural semantics diverge. Let's take a step back from WF and think more generally about what it means to be idle.

Consider the following class that uses a Web services to change a password:

public class PasswordOperation : Operation { Status ChangePassword(Guid userId, string pw) { // Create a web service proxy: UserService svc = new UserService(); // This can take up to 20 sec for // the web server to respond: bool result = svc.ChangePassword( userId, pw ); Logger.AccountAction( "User {0} changed pw ({1}).", userId, result); return Status.Completed; } }

Is the ChangePassword method ever idle? If so, where?

This method's thread is blocked waiting for an HTTP response from the UserService. So conceptually the answer is yes, the thread is idle waiting on the service response. But can the thread actually do other work while you're waiting on the service? No, not as it's currently utilized. Thus, from a WF perspective, this "workflow" is never idle.

Why is it never idle? Imagine that you had some larger scheduler class that was to run operations like ChangePassword efficiently. By efficiently, I mean running several operations in parallel, using the minimum number of threads required for full parallelism, and so on. It turns out that the key to this efficiency is to know when the operation is executing and when it is idle. Because when an operation becomes idle, the scheduler can use the thread that was running the operation to do other work until the operation is ready to run again.

Unfortunately, the ChangePassword method is completely opaque to the scheduler. Because although there is a period when it is effectively idle, viewed from the outside by the scheduler that method is a single unit of blocking work. The scheduler has no capability of splitting that unit of work apart and reusing the thread during the idle period.

Making Synchronous Tasks Asynchronous

You can add this needed scheduling transparency to the operation by splitting the operation into two pieces: one that executes up to point where the operation is potentially idle and one that executes the code after the idle state.

For the hypothetical example shown earlier, you could use the asynchronous capabilities provided by the Web service proxy itself. Keep in mind that this is a simplification and WF actually works a little bit differently, as you will see in a minute.

In Figure 2, I created an improved version of the password-changing method called ChangePasswordImproved. I create the Web service proxy as before. Then the method registers a callback method to be notified when the server has responded. Next, I asynchronously execute the service call and report back to the scheduler that the operation is idle, but not finished, by returning Status.Executing. This step is important—it is this step that allows other work to be accomplished by the scheduler while my code is idle. Finally, when the completed event occurs, I call the scheduler to signal that the operation is finished and that it can move on.

Figure 2 Simple Password-Changing Service Call

public class PasswordOperation : Operation { Status ChangePasswordImproved(Guid userId, string pw) { // Create a web service proxy: UserService svc = new UserService(); svc.ChangePasswordComplete += svc_ChangeComplete; svc.ChangePasswordAsync( userId, pw ); return Status.Executing; } void svc_ChangeComplete(object sender, PasswordArgs e) { Logger.AccountAction( "User {0} changed pw ({1}).", e.UserID, e.Result ); Scheduler.SignalCompleted( this ); } }

Workflows and Activities

Now I am going to apply the concept of an operation being idle to building activities in WF. This is very similar to what you saw earlier, but now I must work within the WF model.

WF comes with many built-in activities. However, when you first get started building real systems with WF, you will quickly want to start building your own custom reusable activities. This is simple to do. You just define a class that derives from the ubiquitous Activity class. Here is a basic example:

class MyActivity : Activity { override ActivityExecutionStatus Execute(ActivityExecutionContext ctx) { // Do work here. return ActivityExecutionStatus.Closed; } }

For your activity to do anything useful, you must override the Execute method. If you're building a short-lived synchronous activity, then you simply implement your activity's operation inside this method and return the status Closed.

There are a few bigger issues that real-world activities are likely to consider. How will your activity communicate with other activities within the workflow and the larger application hosting the workflow? How will it access services such as database systems, UI interaction, and so on? For building synchronous activities, these issues are relatively straightforward.

Building asynchronous activities, on the other hand, can be a more complex undertaking. Fortunately, the pattern employed is repeated across most asynchronous activities. In fact, you can easily capture this pattern in a base class, as I will demonstrate in a moment.

These are the basic steps required for building most asynchronous activities:

  1. Create a class that derives from Activity.
  2. Override the Execute method.
  3. Create a workflow queue that can be used to receive notification that the asynchronous event you have been waiting for has completed.
  4. Subscribe to the queue's QueueItemAvailable event.
  5. Initiate the start of the long running operation (for example, send an e-mail asking a manager to review an application for a job position).
  6. Wait for an external event to occur. This effectively signals that the activity has become idle. You indicate this to the workflow runtime by returning ExecutionActivityStatus.Executing.
  7. When the event occurs, the method handling the Queue­ItemAvailable event removes the item from the queue, converts it to the expected data type, and processes the results.
  8. Typically this concludes the activity's operation. The workflow runtime is then signaled by returning ActivityExecutionContext.CloseActivity.

Persistence

At the beginning of this article, I said the two fundamental things that needed to achieve process independence via workflow were asynchronous activities and a persistence service. You have just seen the asynchronous activities portion. Now let's dig into the technology behind persistence: workflow services.

Workflow services are a key extensibility point for WF. The WF runtime is a class that you instantiate in your application to host all the running workflows. This class has two opposing design goals that are simultaneously achieved through the concept of workflow services. The first goal is for this workflow runtime to be a lightweight object that can be used in many places. The second goal is for this runtime to provide powerful capabilities to the workflows while they're running. For example, it can provide the ability to automatically persist idle workflows, track workflow progress, and support other custom capabilities.

The workflow runtime remains lightweight by default because only a couple of these capabilities are built in. More heavyweight services such as persistence and tracking are optionally installed through the service model. In fact, the definition of a service is any global capability that you want to provide to your workflows. You install these services in the runtime by simply calling the AddService method on the WorkflowRuntime class:

void AddService(object service)

Because AddService takes a System.Object reference, you can add anything your workflow might need.

I am going to work with two services. First, I will use WorkflowQueuingService for accessing the workflow queues that are fundamental for building asynchronous activities. This service is installed by default and cannot be customized. The other service is SqlWorkflowPersistenceService. This service will, of course, provide the persistence capabilities and it is not installed by default. Luckily, it is included with WF. You just have to add it to the runtime.

With a name like SqlWorkflowPersistenceService, you can bet that a database will be required somewhere. You can create an empty database for this purpose or you can add some tables to an existing database. I personally prefer to use a dedicated database rather than mingle workflow persistence data with my other data. So I create an empty database in SQL Server called WF_Persist. I create the necessary database schema and stored procedures by running a couple of scripts. These are installed as part of the Microsoft .NET Framework and are by default located in this folder:

C:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN\

You will want to run the SqlPersistenceService_Schema.sql script first and then run the SqlPersistenceService_Logic.sql script. Now I can use this database for persistence by passing the connection string to the persistence service:

SqlWorkflowPersistenceService sqlSvc = new SqlWorkflowPersistenceService( @"server=.;database=WF_Persist;trusted_connection=true", true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); wfRuntime.AddService(sqlSvc);

This simple AddService method call is all that is required to begin unloading idle workflows and storing them in the database, and then restoring them when they're needed again. The WF runtime takes care of everything else.

Making It Real

Now that you have enough of the technical underpinnings in place, you can weave them together to build an ASP.NET Web site that supports long-running operations. The three major elements that you will see in this example are the construction of asynchronous activities, integrating the workflow runtime into the Web application, and communicating with the workflow from the Web pages.

I am going to be working with a hypothetical .NET consulting company called Trey Research. They would like to automate the recruiting and hiring process for their consultants. So I will be building an ASP.NET Web site to support this hiring process. I am going to keep things very simple, but there are several steps to the process:

  1. A job candidate will visit the Trey Research Web site and express interest in a job.
  2. An e-mail will be sent to the manager noting that there is a new applicant.
  3. This manager will review the application and approve the applicant for a particular position.
  4. An e-mail will be sent the applicant with information about the proposed position.
  5. The applicant will visit the Web site and either accept or reject the position.

While this process is straightforward, there are several steps where the application is waiting for a person to return and fill out some more information. These idle points can take a long time. So they are perfect for demonstrating a long-running process.

This Web application is included in the source code for the article. However, to see the full effect you will need to create the persistence database and configure the sample to use it. I've created a switch to manage whether the persistence service is on or off and have set it to be off by default. To turn it on, set usePersistDB to true in the AppSettings section of web.config.

fig03.gif

Figure 3 The Hiring Process as a Workflow

I will begin by designing the workflow completely independent from ASP.NET. To build the workflow, I will create four custom activities. The first is a send e-mail activity and will be a simple synchronous activity. The other three will represent steps 1, 3, and 5 shown earlier and will be asynchronous activities. These activities are the key to the long running operation's success. I will call these GatherEmployeeInfoActivity, AssignJobActivity, and ConfirmJobActivity, respectively. I will then combine these activities into the admittedly bare bones workflow shown in Figure 3.

The send e-mail activity is straightforward so I won't go into the details of that activity in this article. It is a synchronous activity like the MyActivity class shown earlier. Take a look at the code download for details.

That leaves me with creating the three asynchronous activities. I will save myself a lot of work if I can encapsulate that eight-step process for building an asynchronous activity into a common base class. Toward that end, I will define a class called AsyncActivity (see Figure 4). Note that this listing does not include several internal helper methods or error handling that is present in the real code. Those details the have been left out for the sake of brevity.

Figure 4 AsyncActivity

public abstract class AsyncActivity : Activity { private string queueName; protected AsyncActivity(string queueName) { this.queueName = queueName; } protected WorkflowQueue GetQueue( ActivityExecutionContext ctx) { var svc = ctx.GetService<WorkflowQueuingService>(); if (!svc.Exists(queueName)) return svc.CreateWorkflowQueue(queueName, false); return svc.GetWorkflowQueue(queueName); } protected void SubscribeToItemAvailable( ActivityExecutionContext ctx) { GetQueue(ctx).QueueItemAvailable += queueItemAvailable; } private void queueItemAvailable( object sender, QueueEventArgs e) { ActivityExecutionContext ctx = (ActivityExecutionContext)sender; try { OnQueueItemAvailable(ctx); } finally { ctx.CloseActivity(); } } protected abstract void OnQueueItemAvailable( ActivityExecutionContext ctx); }

In this base class, you'll see that I wrapped up several of the tedious and repetitive parts of building an asynchronous activity. Let's run through this class from top to bottom. Starting with the constructor, I pass in a string for the queue name. Workflow queues are the input points for the host application (the Web pages) to pass data into activities while remaining loosely coupled. These queues are referred to by name and workflow instance, so every asynchronous activity needs its own distinct queue name.

Next, I define the GetQueue method. As you can see, accessing and creating workflow queues is easy, but somewhat tedious. I created this method as a helper method for use inside this class and derived classes.

I then define a method called SubscribeToItemAvailable. This method encapsulates the details of subscribing to the event fired when an item arrives in the workflow queue. This almost always represents the completion of a long waiting period where the workflow has been idle. So the use case goes something like this:

  1. Begin long running operation and call SubscribeToItemAvailable.
  2. Tell the workflow runtime that the activity is idle.
  3. The workflow instance is serialized to the database by the persistence service.
  4. When the operation completes, an item is sent to the workflow queue.
  5. This triggers the workflow instance to be restored from the database.
  6. The abstract template method OnQueueItemAvailable is executed by the base AsyncActivity.
  7. The activity finishes its operation.

To see this AsyncActivity class in action, let's implement the AssignJobActivity class. The other two asynchronous activities are similar and are included in the code download.

In Figure 5, you can see how AssignJobActivity uses the template provided by the AsyncActivity base class. I override Execute to do any preliminary work to begin the long running activity, though in this case there really isn't any. Then I subscribe to the event for when more data is available.

Figure 5 AssignJobActivity

public partial class AssignJobActivity : AsyncActivity { public const string QUEUE NAME = "AssignJobQueue"; public AssignJobActivity() : base(QUEUE_NAME) { InitializeComponent(); } protected override ActivityExecutionStatus Execute( ActivityExecutionContext ctx) { // Runs before idle period: SubscribeToItemAvailable(ctx); return ActivityExecutionStatus.Executing; } protected override void OnQueueItemAvailable( ActivityExecutionContext ctx) { // Runs after idle period: Job job = (Job)GetQueue(ctx).Dequeue(); // Assign job to employee, save in DB. Employee employee = Database.FindEmployee(this.WorkflowInstanceId); employee.Job = job.JobTitle; employee.Salary = job.Salary; } }

There is an implicit contract here that the host application, the Web page, will send in a new Job object into the activity's queue when it has gathered that information from the manager. This will signal the activity that it is OK to continue. It will update the employee in the database. The next activity in the workflow will send an e-mail to the prospective employee informing him that this job is proposed for his position.

Integrating with ASP.NET

That's how it works inside the workflow. But how do you start the workflow? How does the Web page actually gather the job offer from the manager? How does it pass the Job to the activity?

First things first: let's look at how to start the workflow. On the landing page for the Web site there is an Apply Now link. When the applicant clicks this link, it starts both the workflow and the navigation through the user interface in parallel:

protected void LinkButtonJoin_Click( object sender, EventArgs e) { WorkflowInstance wfInst = Global.WorkflowRuntime.CreateWorkflow(typeof(MainWorkflow)); wfInst.Start(); Response.Redirect( "GatherEmployeeData.aspx?id=" + wfInst.InstanceId); }

I simply call CreateWorkflow on the workflow runtime and start the workflow instance. After that I keep track of the workflow instance by passing the instance ID to all subsequent Web pages as a query parameter.

How do I send data from the Web page back into the workflow? Let's look at the assigned job page, in Figure 6, where a manager chooses a job for an applicant.

Figure 6 Assigning a Job

public class AssignJobPage : System.Web.UI.Page { /* Some details omitted */ void ButtonSubmit_Click(object sender, EventArgs e) { Guid id = QueryStringData.GetWorkflowId(); WorkflowInstance wfInst = Global.WorkflowRuntime.GetWorkflow(id); Job job = new Job(); job.JobTitle = DropDownListJob.SelectedValue; job.Salary = Convert.ToDouble(TextBoxSalary.Text); wfInst.EnqueueItem(AssignJobActivity.QUEUE_NAME, job, null, null); buttonSubmit.Enabled = false; LabelMessage.Text = "Email sent to new recruit."; } }

The assign job Web page is largely just a simple input form. It has a dropdown list of available jobs and a textbox for the proposed salary. It also displays the current applicant, although that code is omitted from the listing. When the manager assigns a position and salary to the applicant, he will click the submit button and run the code in Figure 6.

This page uses the workflow instance ID as a query string parameter to look up the associated workflow instance. Then a Job object is created and initialized with the values from the form. Finally, I send this information back into the activity by enqueuing the job into that activity's queue. This is the key step that reloads the idle workflow and allows it to continue executing. AssignJobActivity will associate this job with the previously collected employee and save them to a database.

These last two code listings emphasize how fundamental workflow queues are to the success of asynchronous activities and workflow communication with the outside host. It is also important to note that the use of workflow here has no impact on page flow. Though I could also use WF to control the page flow, it's simply not the focus of this article.

In Figure 6, you saw that I accessed the workflow runtime via the global application class, as follows:

WorkflowInstance wfInst = Global.WorkflowRuntime.GetWorkflow(id);

This brings me to the final point of integrating Windows Workflow into our Web application: all workflows execute inside the workflow runtime. Although you can have as many workflow runtimes as you like in your AppDomain, it most often makes sense to have a single workflow runtime. Because of this, and because the WF runtime object is thread-safe, I made it a public static property of the global application class. Additionally, I start the workflow runtime in the application start event and stop it in the application stop event. Figure 7is an abbreviated version of the global application class.

Figure 7 Starting the Workflow Runtime

public class Global : HttpApplication { public static WorkflowRuntime WorkflowRuntime { get; set; } protected void Application_Start(object sender, EventArgs e) { WorkflowRuntime = new WorkflowRuntime(); InstallPersistenceService(); WorkflowRuntime.StartRuntime(); // ... } protected void Application_End(object sender, EventArgs e) { WorkflowRuntime.StopRuntime(); WorkflowRuntime.Dispose(); } void InstallPersistenceService() { // Code from listing 4. } }

In the application start event, I create the runtime, install the persistence service, and start the runtime. And in the application end event, I stop the runtime. This is an important step. It will block if there are running workflows until those workflows have been unloaded. After stopping the runtime, I call Dispose. While the call to StopRuntime and then to Dispose may look redundant, it is not. You need to call both methods in that order.

A Few Things to Ponder

Let me give you a few things to think about that I didn't directly touch on, presented in question and answer format. Why didn't I use ManualWorkflowSchedulerService? Often when people talk about integrating WF with ASP.NET they emphasize that you should replace the default scheduler for workflows (which uses the thread pool) with a service called ManualWorkflowSchedulerService. The reason is because it's not required or really appropriate for our long running goals. The manual scheduler is good when you expect to run a single workflow to completion within a given request. It makes less sense when your workflow will execute across process lifetimes, not to mention across requests.

Is there a way to track the current progress of a given workflow instance? Yes, there is an entire tracking service built into WF and it is used in a similar manner as the SQL persistence service. See the March 2007 Foundations column, " Tracking Services in Windows Workflow Foundation" by Matt Milner.

Bringing It All Together

I can summarize the techniques discussed in this article with a couple of steps. I began by outlining why the ASP.NET worker processes and the process model in general are not appropriate for very long running operations. To escape this limitation I took advantage of two features of WF that are combined to achieve process independence: asynchronous activities and workflow persistence.

Because building asynchronous activities can be somewhat tricky, I encapsulated the details into the AsyncActivity base class introduced in this article. Then I expressed the long-running operation as a sequential workflow built with asynchronous activities that I can incorporate that into a Web application and get process-independence for free.

Finally, I demonstrated that integrating the workflow into ASP.NET has two basic parts: communicating with the activities via a workflow queue and hosting the runtime in the global application class.

Now that you have seen WF integrated with ASP.NET to support long running operations, you have one more powerful tool for building solutions on top of the .NET Framework.

Michael Kennedy is an instructor for DevelopMentor where he specializes in core .NET technologies as well as agile and TDD development methodologies.