Asynchronous Asp.net Page

Jeff Prosise showed us the power of Asynchronous Asp.net Page in developing truly scalable web application in his great article. But I found a lot people finding hard time implementing it in real life based upon those examples. For example, in the data bind example he uses the BeginExcecuteReader/EndExecuteReader method of the DataReader but in real life we do not use it, instead we return some custom entity collection or the DataReader/DataSet directly from the bussiness logic layer. Omar showed me why this approach will not work and even it will add extra overhead comparing the regular model.In this post, I will show you some real life usage of the asynchronous pages and how easily you can convert your regular pages with it. Let us start with a simple example, you want show all the customers of USA of the Northwind database in a GridView. You will come up something similar like the following:

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        LoadCustomers();
    }
}

private void LoadCustomers()
{
    string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvCustomers.DataSource = rdr;
                gvCustomers.DataBind();
            }
        }
    }
}

Now let us convert this regular page with the asynchronous model. First add Async="true" in the page directive. Next, create a delegate with the same signature of LoadCustomers method, we will use this delegate for the async operation. The following shows the complete code:

private delegate void LoadData();
private LoadData _loadCustomers;

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginLoadCustomers),
            new EndEventHandler(EndLoadCustomers));
    }
}

private IAsyncResult BeginLoadCustomers(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadCustomers = new LoadData(LoadCustomers);
    return _loadCustomers.BeginInvoke(cb, state);
}

private void EndLoadCustomers(IAsyncResult result)
{
    _loadCustomers.EndInvoke(result);
}

private void LoadCustomers()
{
    string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvCustomers.DataSource = rdr;
                gvCustomers.DataBind();
            }
        }
    }
}

What we are doing is very simple, we are wrapping up the actual method with a Begin/End Pair and we are passing the pair to AddOnPreRenderCompleteAsync method. In the Begin we are creating an instance of the delegate and calling the invoke which actually fires a thread from the ThreadPool. We can also use the RegisterAsyncTask method instead of AddOnPreRenderCompleteAsync and there some extra advantage of using it instead of AddOnPreRenderCompleteAsync, among the advantages I think the thread impersonate(It is not possible to access the HttpContext.Current from the other threads) and the parallel execution of multiple async method are the most important. Let us consider that you also want to show the employees in another GridView of the Northwind database along with the customers which country is set to USA, since there is no dependency between these operations we can use two separate thread to for each operation. The following shows the complete code:

private delegate void LoadData();
private LoadData _loadCustomers;
private LoadData _loadEmployees;

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadCustomers),
            new EndEventHandler(EndLoadCustomers), null, null, true)
            );

        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadEmployees),
            new EndEventHandler(EndLoadEmployees), null, null, true)
            );
    }
}

private IAsyncResult BeginLoadCustomers(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadCustomers = new LoadData(LoadCustomers);
    return _loadCustomers.BeginInvoke(cb, state);
}

private void EndLoadCustomers(IAsyncResult result)
{
    _loadCustomers.EndInvoke(result);
}

private IAsyncResult BeginLoadEmployees(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadEmployees = new LoadData(LoadEmployees);
    return _loadEmployees.BeginInvoke(cb, state);
}

private void EndLoadEmployees(IAsyncResult result)
{
    _loadEmployees.EndInvoke(result);
}

private void LoadCustomers()
{
    string SQL = string.Format("SELECT * FROM [Customers] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvCustomers.DataSource = rdr;
                gvCustomers.DataBind();
            }
        }
    }
}

private void LoadEmployees()
{
    string SQL = string.Format("SELECT * FROM [Employees] WHERE [Country] = '{0}'", "USA");

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                //Bind it with the GridView
                gvEmployees.DataSource = rdr;
                gvEmployees.DataBind();
            }
        }
    }
}

The code is almost same as the previous one except we have added another set of methods for the employees and we are using the RegisterAsyncTask method instead of AddOnPreRenderCompleteAsync in page load event. Here the most important thing in the RegisterAsyncTask method is the last parameter where we are passing true, this ensures that both the method will be executed simultaneously. If we used AddOnPreRenderCompleteAsync method or do not pass true in the last parameter of the RegisterAsyncTask method then it will first execute the customer part, once complete it start the employee part.

In the last and final section, we will see an Master/Detail example of Async Page. We will have a DropDownList which will be populated with all the categories of Northwind database and based upon the selected category we will load the products in a GridView. Since the product loading depends upon the selected category& thus it is not possible to do the parallel execution of both method, instead the product loading needs to wait until the category loads first. The following shows the compete code:

private delegate void LoadData();
private LoadData _loadCategories;
private LoadData _loadProducts;

protected void Page_Load(object sender,
    EventArgs e)
{
    if (!IsPostBack)
    {
        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadCategories),
            new EndEventHandler(EndLoadCategories), null, null)
            );

        RegisterAsyncTask(
            new PageAsyncTask(
            new BeginEventHandler(BeginLoadProducts),
            new EndEventHandler(EndLoadProducts), null, null)
            );
    }
}

protected void Category_Changed(object sender,
    EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(
        new BeginEventHandler(BeginLoadProducts),
        new EndEventHandler(EndLoadProducts), null, null)
        );
}

private IAsyncResult BeginLoadCategories(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadCategories = new LoadData(LoadCategories);
    return _loadCategories.BeginInvoke(cb, state);
}

private void EndLoadCategories(IAsyncResult result)
{
    _loadCategories.EndInvoke(result);
}

private IAsyncResult BeginLoadProducts(object sender,
    EventArgs e,
    AsyncCallback cb,
    object state)
{
    _loadProducts = new LoadData(LoadProducts);
    return _loadProducts.BeginInvoke(cb, state);
}

private void EndLoadProducts(IAsyncResult result)
{
    _loadProducts.EndInvoke(result);
}

private void LoadCategories()
{
    const string SQL = "SELECT [CategoryID], [CategoryName] FROM [Categories]";

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                ddlCategory.DataSource = rdr;
                ddlCategory.DataBind();
            }
        }
    }
}

private void LoadProducts()
{
    string SQL = string.Format("SELECT * FROM [Products] WHERE [CategoryID] = {0}",
        ddlCategory.SelectedItem.Value);

    using (IDbConnection cnn = new SqlConnection(_connectionString))
    {
        cnn.Open();

        using (IDbCommand cmd = cnn.CreateCommand())
        {
            cmd.CommandText = SQL;
            cmd.CommandType = CommandType.Text;

            using (IDataReader rdr = cmd.ExecuteReader())
            {
                gvProducts.DataSource = rdr;
                gvProducts.DataBind();
            }
        }
    }
}

As you can see that we are not passing true in the last argument of RegisterAsyncTask method, it ensures that the category will load first and then the products. We are also registering another async task in the selected index change event of the DropDownList.

I hope this will clear your confusion with async pages and you will be able to implement easily it in your projects.

Download: Full Source

kick it on DotNetKicks.com

8 Comments

  • you Rock !! Read Jeff's Async page, and felt exactly same way , and waiting for someone to turn a real life solution ! :)

    Thanks,

    varadhg

  • Delegate.Invoke takes a thread from Thread Pool. When we use Delegate.Invoke on ASP.NET page, it takes a thread from ASP.NET thread pool. So, although the async page releases a thread into the pool, then that thread (or another one) is immediately taken by the Delegate.Invoke thread.

    Now that thread is blocked until the ExecuteReader call finishes. So, when ExecuteReader takes a long time, a precious thread from ASP.NET Thread Pool is still occupied. So, we aren't releasing that thread back to thread pool by calling BeginExecuteReader.

    So, in terms of timeline, here's what happens:

    ASP.NET Takes a thread. Let's say Thread A.
    Then it returns the Thread A to thread pool.
    Delegate.BeginInvoke takes Thread X from thread pool in order to execute the LoadCategories function in background.
    Thread X is blocked until LoadCategories function completes.
    When Delegate invocation is complete, ASP.NET takes Thread B from thread pool and completes the page execution.

    So, during this whole time, one thread was always occupied. In fact, it added an overhead of swtching from Thread A -> Thread X -> Thread B.

    The only way we can truly have a thread returned to pool and made available to other thread requests are when SqlDataReader.BeginExecute is waiting on SQL Server to finish the work and during that moment the thread has been returned to Thread Pool.




  • @Omar: Thanks for clarifying it, it is true that it will occupy a thread from the pool but the same technique is applied in System.IO.Stream class. Check the BeginRead method.

  • Omar is indeed correct.  A quick check with System.Threading.Thread.GetDomain().FriendlyName shows that the new thread's context is in fact ASP.NET's, which means that unless you need to do two things at the same time on the page, async pages aren't really useful.

  • There are two types of threads available to ASP.NET application. Worker thread and IO thread. When you call Delegate.Invoke, it eats up previous worker thread. But Stream.BeginXXX or Webservice.BeginXXX or BeginExecuteReader uses IO threads. Thus ASP.NET does not run out of worker threads.

    So, as long as you use Delegate.Invoke, it will not make ASP.NET applications scalable. Instead it will add additional overhead. The principle here is to return worker threads to ASP.NET and wait on IO threads.

  • This kind of scenario is best suited for loading independent modules on a page , that depends either on external source or internal database. But, sequential tasks like, loading a customer grid on basis of user selection, the async page is not that useful. Again, there is no point of taking the advantage of multi processing for sequential task, unless the query returns huge chunk of data.

    Hope this helps
    Mehfuz

  • By striking the text, are you indicating that the solution is not good?

    If so, is it possible for you to write an update or possible for you to write a different method?

    Thankyou for the explanation of this method though.

  • Ok so - basically Jeff's approach with BeginExcecuteReader/EndExecuteReader is really the only way to do it since we want to use an I/O thread and not a worker thread?

    I think this is what is said here in the end, but i wanted to make sure i was understanding properly.

    I would love to be able to return a DataTable asynchronously, but based on this post and replies, I believe that this cannot happen? That once you start to maniuplate the result to some other type of object you are now pulling a ASP worker thread?

    So in order to remain asynchronous on the database call you have to use the BeginExcecuteReader/EndExecuteReader calls?

    Thanks! :o)

Comments have been disabled for this content.