ASP.NET, WCF, and Asynchronous Programming

Jeffrey Richter told me once that a CPU was a terrible thing to waste. I don't know if he was the one to coin the term - in fact, I think he told me someone else coined that term. Regardless of where it came from, the principle behind that phrase is solid. All too often, developers do I/O synchronously, thereby wasting CPU time, and making their applications less scalable and responsive. Terrible.

Often the excuse is that one expects the I/O to happen quickly. One seldom (if ever) can predict how fast any I/O is going to take, so this is a non-starter. Never do synchronous I/O. Never ever. Read Jeff Richter's CLR Via C# for an eloquent explanation of why.

This premise surfaces in WCF programming from within an ASP.NET application. Consider this scenario:

A Page_Load event handler needs to send a message to a web service and do something with the result.

Simply put, the ASP.NET application needs to do I/O. If the web service call is synchronous, the thread that is executing the Page_Load event (and the web service call) blocks until the web service call returns. That thread cannot be used to process other requests while waiting, and the web server will soon return 503 errors. Since the web service call takes a long time (try not to quibble that it is usually fast), you are wasting threads and CPU time. Terrible.

If, however, you use ASP.NET Async pages and an asynchronous call to the web service, you are not wasting the CPU. Very good. For more info on the async capabilities of ASP.NET 2.0, see Jeff Prosise's most excellent article: https://msdn.microsoft.com/msdnmag/issues/05/10/WickedCode/. With WCF, you would do something like:

  1 public partial class _Default : System.Web.UI.Page 
 2 {
 3     IService1 proxy;
 4     ChannelFactory<IService1> factory;
 5 
 6     protected void Page_Load(object sender, EventArgs e)
 7     {
 8         WSHttpBinding binding = new WSHttpBinding();
 9         EndpointAddress address = new EndpointAddress("https://localhost:8080/Service1");
10 
11         // you could also reuse a proxy...
12         factory = new ChannelFactory<IService1>(binding, address);
13 
14         this.AddOnPreRenderCompleteAsync(
15             new BeginEventHandler(BeginAsyncOperation),
16             new EndEventHandler(EndAsyncOperation)
17         );
18     }
19 
20 
21     IAsyncResult BeginAsyncOperation(Object sender, EventArgs e,
22         AsyncCallback cb, Object state)
23     {
24         proxy = factory.CreateChannel();
25         return proxy.BeginGetData(TextBox1.Text, cb, state);
26     }
27 
28     void EndAsyncOperation(IAsyncResult ar)
29     {
30         String output = proxy.EndGetData(ar);
31         Label2.Text = output;
32     }
33 }

Line 24 is where is I/O starts. BeginGetData returns an IAsyncResult and the thread goes into back into the thread pool. When the result comes back, the EndAsyncOperation method is called, and the proxy's end method can be called.

The model isn't hard, and it greatly improves the scalability of your ASP.NET application. On top of that, it doesn't waste the CPU...

The full code sample is here

AsyncPagesWithWCFSite.zip