ASP.NET WF4 / WCF and Async Calls

How should you use WF4 and WCF with ASP.NET? 

For this post I’ve created a really simple workflow and WCF service that delay for a specific amount of time and then return a value.  Then I created an ASP.NET page that I can use to invoke the workflow and WCF service to test their behavior

The Workflow Definition

First off – let’s get one thing straight.  When you create a workflow, you are creating a workflow definition.  The workflow definition still has to be validated, expressions compiled etc. and this work only needs to be done once. When you hand this workflow definition to WorkflowInvoker or WorkflowApplication it will create a new instance of the workflow (which you will never see).

To make this clear, in this sample I have a workflow file SayHello.xaml but I named the variable SayHelloDefinition.

    1: private static readonly Activity SayHelloDefinition = new SayHello();

The Easy Way

If you want to do something really simple, you can just invoke workflows and services synchronously using WorkflowInvoker

    1: private void InvokeWorkflow(int delay)
    2: {
    3:     var input = new Dictionary<string, object> { { "Name", this.TextBoxName.Text }, { "Delay", delay } };
    4:  
    5:     this.Trace.Write(string.Format("Starting workflow on thread {0}", Thread.CurrentThread.ManagedThreadId));
    6:  
    7:     var output = WorkflowInvoker.Invoke(SayHelloDefinition, input);
    8:  
    9:     this.Trace.Write(string.Format("Completed workflow on thread {0}", Thread.CurrentThread.ManagedThreadId));
   10:  
   11:     this.LabelGreeting.Text = output["Greeting"].ToString();
   12: }

Likewise you can invoke the WCF service synchronously

    1: private void InvokeService(int delay)
    2: {
    3:     this.Trace.Write(string.Format("Calling service on thread {0}", Thread.CurrentThread.ManagedThreadId));
    4:  
    5:     var proxy = new TestServiceClient();
    6:  
    7:     try
    8:     {
    9:         var result = proxy.DoWork(delay);
   10:         proxy.Close();
   11:         this.Trace.Write(
   12:             string.Format(
   13:                 "Completed calling service on thread {0} delay {1}", Thread.CurrentThread.ManagedThreadId, result));
   14:  
   15:         this.LabelDelay.Text = result.ToString();
   16:     }
   17:     catch (Exception)
   18:     {
   19:         proxy.Abort();
   20:         throw;
   21:     }
   22: }

With the default delay of 1000ms this page will take about 2.5 seconds to load.  1 second for the workflow, 1 second for the WCF service and .5 seconds for everything else.

The Fast Way

If you are going to write code that runs on a server you should get used to writing async code.  Yes I know it is generally a pain and more complex but what can I say… the result is far better.

Step 1 – Tell ASP.NET you want to do Async

ASP.NET won’t allow async work unless you set the Async directive.

    1: <%@ Page Title="" Language="C#" ... Async="true" Trace="true" %>

Step 2 – Use the ASP.NET Synchronization Context

In this simple workflow I’m not using persistence or bookmarks (which would make the code slightly different if I did).  One of the more confusing elements of doing Async work is that as you search the web you will see the history of how async was done in the past and you might get confused.  In fact I found that it is quite difficult to pin down what the correct method is today.

What you want to do is to tell the Workflow Runtime that you are working with a SynchronizationContext from ASP.NET.  Once you have done this, you can inform ASP.NET (via the context) when your operation begins and ends.  This code gets a little tricky but follow along.

    1: private void InvokeWorkflowAsync(int delay)
    2: {
    3:     // Create the inputs
    4:     var input = new Dictionary<string, object> { { "Name", this.TextBoxName.Text }, { "Delay", delay } };
    5:  
    6:     var workflowApplication = new WorkflowApplication(SayHelloDefinition, input)
    7:         {
    8:             // Tell the runtime we are using the ASP.NET synchronization context
    9:             SynchronizationContext = SynchronizationContext.Current,
   10:  
   11:             Completed = (args) =>
   12:                 {
   13:                     this.Trace.Write(
   14:                         string.Format("Completed workflow on thread {0}", Thread.CurrentThread.ManagedThreadId));
   15:  
   16:                     // Update page controls here
   17:                     this.LabelGreeting.Text = args.Outputs["Greeting"].ToString();
   18:  
   19:                     // Tell ASP.NET we are finished
   20:                     SynchronizationContext.Current.OperationCompleted();
   21:                 }
   22:         };
   23:  
   24:     this.Trace.Write(string.Format("Starting workflow on thread {0}", Thread.CurrentThread.ManagedThreadId));
   25:  
   26:     // Tell ASP.NET we are starting an async operation
   27:     SynchronizationContext.Current.OperationStarted();
   28:  
   29:     workflowApplication.Run();
   30:  
   31:     // Don't try anything here - it will run before your workflow has completed
   32: }

Step 3 – Configure your Service Reference for Async Operations

When you add a Service Reference the default is to not generate async operations. 

SNAGHTML1f9cda4

image

Step 4 – Call your Service Async

Now you have a super-charged proxy that can do async.  There are two methods for async the old APM (Begin/End) and the newer Event based async model (EPM).  In this code I’m using the EPM model.  Note that I don’t have to call OperationStarted/Completed because WCF will do it for me.

    1: private void InvokeServiceAsync(int delay)
    2: {
    3:     this.Trace.Write(string.Format("Calling service on thread {0}", Thread.CurrentThread.ManagedThreadId));
    4:  
    5:     var proxy = new TestServiceClient();
    6:  
    7:     proxy.DoWorkCompleted += (o, args) =>
    8:         {
    9:             if (!args.Cancelled)
   10:             {
   11:                 this.Trace.Write(
   12:                     string.Format(
   13:                         "Completed calling service on thread {0} delay {1}",
   14:                         Thread.CurrentThread.ManagedThreadId,
   15:                         args.Result));
   16:                 this.LabelDelay.Text = args.Result.ToString();
   17:             }
   18:         };
   19:  
   20:     proxy.DoWorkAsync(delay);
   21: }

Step 5 – Test Your Service

I’ve enabled the ASP.NET Page Trace on my service so we can see what is happening. 

image

As you can see we started the workflow and the service call on thread 37.  Then ASP.NET was able to do some additional work before our workflow and service call which completed later.  ASP.NET will wait on both of these to complete before it finishes rendering the page and returning to the caller.

Summary

With Workflows you

  • Set the WorkflowApplication.SynchronizationContext to the ASP.NET synchronization context
  • Call OperationStarted/Completed to let ASP.NET know when the workflow is done

With WCF you

  • Configure your service reference for async operations
  • Use async operations in your code
  • Don’t have to worry about SynchronizationContext

Happy Coding!

Ron Jacobs

https://blogs.msdn.com/rjacobs

Twitter: @ronljacobs https://twitter.com/ronljacobs