How To: Submit and Poll for Long-Running Tasks

 

Retired Content

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.

patterns & practices Developer Center

Improving .NET Application Performance and Scalability

J.D. Meier, Srinath Vasireddy, Ashish Babbar, and Alex Mackman
Microsoft Corporation

May 2004

Related Links

Home Page for Improving .NET Application Performance and Scalability

Chapter 4, Architecture and Design Review of a .NET Application for Performance and Scalability

Chapter 13, Code Review: .NET Application Performance

Send feedback to Scale@microsoft.com

patterns & practices Library

Summary: This How To shows you how to make a long-running Web service call from an ASP.NET application without blocking the Web page. The application calls the Web service asynchronously and then displays a "Busy…Please Wait" page that polls the Web service for completion. When the results are available, the application redirects the client to a results page.

The techniques described in this How To also apply to other long-running tasks, such as running a complex database query or calling a remote component.

Contents

Applies To
Overview
Before You Begin
Create a Test Web Service
Create a State Class
Create a Temporary Data Store
Implement the Callback Method
Make the Asynchronous Call
Implement a Polling Mechanism
Display the Results
Clean Up the Temporary Data Store
Compile the Code
Run the Sample
Sample Code
Additional Resources

Applies To

  • Microsoft® .NET Framework version 1.0
  • .NET Framework version 1.1

Overview

A common approach to handling long-running calls is to have the client poll for results. After the request is submitted to the server, instead of waiting for the work to complete, the server immediately sends a response to the client indicating that the work is being processed. The client then polls the server for the final result.

You have probably seen this approach in action on various Web sites whether or not you were aware of the implementation. For example, when you search for airline flights on a Web site, it is common to see an animated .gif file while the server is retrieving the results. By returning information to the client immediately rather than waiting for the long-running task to complete, you free the ASP.NET request thread to process other requests. The limited overhead associated with polling the server every few seconds is significantly lower than the overhead of making a blocking call.

This functionality is commonly used when:

  • Calling a database and running complex queries.
  • Making lengthy calls to a Web service.
  • Making lengthy calls to a remote component.

Before You Begin

Create a new virtual directory named Longtask and then create a \bin subdirectory. For example:

c:\inetpub\wwwroot\longtask
c:\inetpub\wwwroot\longtask\bin

Use Internet Services Manager to mark the directory as an Application. As you follow the instructions in this How To, make sure to place all of the files that you create in this directory.

Create a Test Web Service

You need a Web service for testing purposes. You can use an existing Web service, or you can create a new one. To create a new test Web service, create a file named MyWebService.asmx in the Longtask directory and add the following code.

mywebservice.asmx

<%@ WebService Language="c#" Class="MyWebService" %>
using System;
using System.Web.Services;
public class MyWebService
{
  public MyWebService() {}
  [WebMethod]
  public int LongRunningTask(int length)
  {
    DateTime start = DateTime.Now;
    System.Threading.Thread.Sleep(length);
    return (int)((TimeSpan)(DateTime.Now - start)).TotalSeconds;
  }
}

The Web method exposed by this Web service allows you to determine how long the call should block by sleeping for an interval determined by the input parameter to the LongRunningTask method.

Next, create a proxy for the Web service by running the following command from a command prompt.

wsdl.exe /language:cs /namespace:ServiceNameSpace.localhost 
         /out:c:\inetpub\wwwroot\longtask\proxy.cs 
         https://localhost/longtask/mywebservice.asmx?WSDL

Note   Make sure that you place Proxy.cs in the Longtask virtual directory (for example, c:\inetpub\wwwroot\longtask). Before you create the proxy, make sure that a Global.asax file does not exist in the directory. If the file already exists, delete it; otherwise, errors may be generated when you create the proxy.

Create a State Class

To enable the ASP.NET application to poll for the results of the asynchronous call, create a state class that maintains key information about the Web service call. You need this information to resynchronize the results from the callback with the caller's original request context.

You use the state object to store the following:

  • The Web service object on which the call is made. You need to store the Web service object on which the call is made to ensure that whenever the callback returns, you call the EndXXX method on the object with the same call context as the one used to call the BeginXXX method.
  • SessionID. You need to store SessionID to ensure that whenever the results are returned from the Web service through the callback mechanism, they are associated with the caller's session ID. This allows you to subsequently retrieve the correct results for the correct client by using the appropriate session ID as a lookup key.

Create a new file named Util.cs and add the following code for the MyState class.

util.cs

public class MyState
{
  public localhost.MyWebService _webServiceState;
  public string_sessionID;
  public MyState(localhost.MyWebService ws, string sessionid)
  {
    _webServiceState = ws;
    _sessionID = sessionid;
  }
}

Create a Temporary Data Store

Create a data store to hold the results obtained from the Web service. The following code uses a Hashtable storage implementation in which the session ID is used as the lookup key. Choose an appropriate data store for your scenario based on your requirements for reliability, ease of retrieval, and amount of data to be stored.

Create a file named Util.cs and add the following code for the TempDataStore class.

util.cs

public class TempDataStore
{
  private static Hashtable _table = new Hashtable();
  static TempDataStore (){}
  public static object GetRecords(string key)
  {
    lock(_table.SyncRoot)
    {
      return _table[key];
    }
  }
  public static void SetRecords(string key, object value)
  {
    lock(_table.SyncRoot)
    {
      _table.Add(key, value);
    }
  }
  public static void Remove(string key)
  {
    lock(_table.SyncRoot)
    {
      _table.Remove(key);
    }
  }
  public static void ClearAll()
  {
    lock(_table.SyncRoot)
    {
      _table.Clear();
    }
  }
}

Implement the Callback Method

When the results are returned from the Web service, they are stored in the data store by using the session ID as the key until the data is polled for and retrieved by the client ASP.NET application.

Retrieve the value for the relevant user by using the session ID as the key to the temporary store. Create a new .aspx page named Longtask.aspx and add the following code.

public void WSCallback(IAsyncResult ar)
{
  MyState myState = (MyState)ar.AsyncState;
  //retrieve the object on which EndXXX needs to be called
  localhost.MyWebService ws = myState._WebServiceState;
  //store the values in the data store
  TempDataStore.SetRecords(myState._SessionID,ws.EndLongRunningTask(ar));
}

Make the Asynchronous Call

To make the asynchronous call, add a CallWebService button to the Web page and implement the button's click event handler.

To make the asynchronous call

  1. Add a button control to the Longtask.aspx page.

  2. Add the following code to the click event handler. This code calls the Web service asynchronously and then redirects to a polling page.

    MyWebService ws = new MyWebService();
    AsyncCallback cb = new AsyncCallback(WSCallback);
    Random rnd = new Random();   
    MyState myState = new MyState(ws,Session.SessionID);
    ws.BeginLongRunningTask(rnd.Next(10000),cb, myState);
    Response.Redirect("poll.aspx", false);
    

Implement a Polling Mechanism

The ASP.NET application needs to poll the server periodically to find out if the long-running task has completed. There are a number of ways to do this. Each approach forces the client browser to refresh itself automatically. The options are:

  • Use a Refresh header.

    Response.AddHeader("Refresh","2;URL=poll.aspx");
    
  • Use a <meta> tag.

    <meta http-equiv="refresh" content="2;url=poll.aspx">
    
  • Call the setTimeout method in the client script.

    setTimeout("window.location.href = 'poll.aspx'",2000);
    

**Note   **Clients can disable the functionality of the <meta> tag and the setTimeout method with the appropriate browser configuration. If this issue concerns you, you should use the Refresh header approach.

The following example uses the <meta> tag. To use this approach, create a new .aspx page named Poll.aspx. Check to see if the long-running task has completed by calling TempDataStore.GetRecords. If this method returns a valid object, the task has completed; otherwise, you need to continue to poll.

poll.aspx

<%@ Page language="c#" %>
<%@ Import Namespace="ServiceNameSpace" %>
<script runat=server>
public void Page_Load()
{
  object obj = TempDataStore.GetRecords(Session.SessionID);
  if(obj!=null)  
  {  //long task is complete, goto the results
    Response.Redirect("results.aspx", false);
  }
}
</script>
<html>
  <head>
  <meta http-equiv="refresh" content="2"/>
  </head>
  <body>
  Busy...Please wait ...
  </body>
</html>

This is a very simplified example. With a real implementation, you could implement more functionality within the polling page. For example, you could:

  • Use an animated .gif file for display purposes.
  • Provide status updates or an elapsed second count.
  • Put the polling page in a hidden IFrame to make the refresh operations transparent to the user.

Display the Results

Next, you need to display the results of the long-running task to the user. Create a new .aspx page named Results.aspx as follows.

results.aspx

<%@ Page language="c#" %>
<%@ Import Namespace="ServiceNameSpace" %>
<script runat=server>
public void Page_Load()
{
  object obj = TempDataStore.GetRecords(Session.SessionID);
  //double-check to make sure the results are still there
  if(obj!=null)
  {
    Response.Write(string.Format("Results: <b>{0}</b> Session: 
<b>{1}</b>",(int)obj,Session.SessionID));    
    //remove the results    
    TempDataStore.Remove(Session.SessionID);
  }
}
</script>

Clean Up the Temporary Data Store

Create a Global.asax file and add cleanup code to clean up the temporary store if the session terminates or the application's OnEnd event is called. Add the following code to Global.asax.

protected void Session_End(Object sender, EventArgs e)
{
  TempDataStore.Remove(Session.SessionID);
}

protected void Application_End(Object sender, EventArgs e)
{
  TempDataStore.ClearAll();
}

In this application, you do not actually store any information in ASP.NET session state. As a result, ASP.NET does not initialize session-state processing, and a new session ID is generated for each request. Obtaining the results from the Web service is dependent on the session ID, so you need to ensure that sessions are enabled. To do so, add the following code to your application's Global.asax:

protected void Session_Start(Object sender, EventArgs e)
{
  Session["valid'] = true;
}

Compile the Code

To compile the code, run the following command from within the virtual directory that you created earlier (for example, c:\inetpub\wwwroot\longtask).

csc.exe /t:library /out:bin\helper.dll *.cs

**Note   **The c:\inetpub\wwwroot\longtask\bin directory must already exist, as described in "Before You Begin" earlier in this How To.

Run the Sample

To run the sample, use Microsoft Internet Explorer to browse to https://localhost/longtask/Longtask.aspx and click the CallWebService button. The browser should be redirected to Poll.aspx, which continues to refresh until the Web service is complete. At this point, the browser is redirected to Results.aspx, where the results from the Web service should be displayed.

Sample Code

The complete code for the ASP.NET application and the Web service, together with a batch file that you can use to compile the code, is shown below.

Compile.bat

@echo off

set WORKING_DIR=c:\inetpub\wwwroot\longtask\
set WEB_SERVICE_URL=https://localhost/longtask/mywebservice.asmx?WSDL

echo.
echo WORKING_DIR=%WORKING_DIR%
echo WEB_SERVICE_URL=%WEB_SERVICE_URL%
echo.
if exist "global.asax" goto :RENAME

:GENERATEPROXY
echo.
echo Generating proxy.cs
echo.
wsdl.exe /language:cs /nologo /namespace:ServiceNameSpace.localhost 
/out:%WORKING_DIR%proxy.cs %WEB_SERVICE_URL%
if exist "~global.asax" goto :RESTORE

:COMPILEDLL
echo.
echo Compiling %WORKING_DIR%bin\helper.dll
echo.
csc.exe /t:library /nologo /out:%WORKING_DIR%bin\helper.dll %WORKING_DIR%*.cs
goto :EXIT

:RENAME
echo.
echo Renaming %WORKING_DIR%global.asax to %WORKING_DIR%~global.asax
echo.
ren %WORKING_DIR%global.asax ~global.asax
goto :GENERATEPROXY

:RESTORE
echo.
echo Renaming %WORKING_DIR%~global.asax to %WORKING_DIR%global.asax
echo.
ren %WORKING_DIR%~global.asax global.asax
goto :COMPILEDLL

:EXIT
echo.
echo Done
echo.

Mywebservice.asmx

<%@ WebService Language="c#" Class="MyWebService" %>
using System;
using System.Web.Services;
public class MyWebService
{
  public MyWebService() {}
  [WebMethod]
  public int LongRunningTask(int length)
  {
    DateTime start = DateTime.Now;
    System.Threading.Thread.Sleep(length);
    return (int)((TimeSpan)(DateTime.Now - start)).TotalSeconds;
  }
}

Util.cs

using System;
using System.Collections;
using ServiceNameSpace.localhost;

namespace ServiceNameSpace
{
  public class MyState
  {
    
    public MyWebService _webServiceState;
    public string _sessionID;
    public MyState(MyWebService ws,string sessionID)
    {
      _webServiceState = ws;
      _sessionID = sessionID;
    }
  }
  public class TempDataStore
  {
    private static Hashtable _table = new Hashtable();
    static TempDataStore (){}
    public static object GetRecords(string key)
    {
      lock(_table.SyncRoot)
      {
        return _table[key];
      }
    }
    public static void SetRecords(string key, object value)
    {
      lock(_table.SyncRoot)
      {
        _table.Add(key, value);
      }
    }
    public static void Remove(string key)
    {
      lock(_table.SyncRoot)
      {
        _table.Remove(key);
      }
    }
    public static void ClearAll()
    {
      lock(_table.SyncRoot)
      {
        _table.Clear();
      }
    }
  }
}

Longtask.aspx

<%@ Page language="c#" %>
<%@ Import Namespace="ServiceNameSpace" %>
<%@ Import Namespace="ServiceNameSpace.localhost" %>
<script runat=server>
public void WSCallback(IAsyncResult ar)
{
  MyState myState = (MyState)ar.AsyncState;
  //retrieve the object on which EndXXX needs to be called
  MyWebService ws = myState._webServiceState;
  //store the values in the data store
  TempDataStore.SetRecords(myState._sessionID,ws.EndLongRunningTask(ar));
}
private void Button1_Click(object sender, System.EventArgs e)
{
  MyWebService ws = new MyWebService();
  AsyncCallback cb = new AsyncCallback(WSCallback);
  Random rnd = new Random();              
  MyState myState = new MyState(ws,Session.SessionID);
  ws.BeginLongRunningTask(rnd.Next(10000), cb, myState);
  Response.Redirect("poll.aspx", false);
}
</script>
<html>
<body>
  <form id="Form1" method="post" >
<asp:Button id="Button1"  onclick="Button1_Click" 
Text="Button"></asp:Button>
  </form>
</body>
</html>

Poll.aspx

<%@ Page language="c#" %>
<%@ Import Namespace="ServiceNameSpace" %>
<script runat=server>
public void Page_Load()
{
  object obj = TempDataStore.GetRecords(Session.SessionID);
  if(obj!=null)
  {  //long task is complete, goto the results
    Response.Redirect("results.aspx", false);
  }
}
</script>
<html>
  <head>
  <meta http-equiv="refresh" content="2"/>
  </head>
  <body>
  Busy...Please wait ...
  </body>
</html>

Results.aspx

<%@ Page language="c#" %>
<%@ Import Namespace="ServiceNameSpace" %>
<script runat=server>
public void Page_Load()
{
  object obj = TempDataStore.GetRecords(Session.SessionID);
  //double-check to make sure the results are still there
  if(obj!=null)
  {
    Response.Write(string.Format("Results: <b>{0}</b> Session: 
<b>{1}</b>",(int)obj,Session.SessionID));  
    //remove the results      
    TempDataStore.Remove(Session.SessionID);
  }
}
</script>

Global.asax

<%@ Application  Language="c#" %>
<%@ Import Namespace="ServiceNameSpace" %>
<script runat=server>
protected void Session_Start(object sender, EventArgs e)
{
  //this is needed so we don't generate a new session with each request
  Session["valid"] = true;
}
protected void Session_End(Object sender, EventArgs e)
{
  TempDataStore.Remove(Session.SessionID);
}
protected void Application_End(Object sender, EventArgs e)
{
  TempDataStore.ClearAll();
}
</script>

Additional Resources

For more information, see the following resources:

patterns & practices Developer Center

Retired Content

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.

© Microsoft Corporation. All rights reserved.