Data Source Controls, Part 3: Asynchronous Data Access

 

Nikhil Kothari
Microsoft Corporation

November 2005

Applies to:
   Microsoft Visual Studio 2005
   Microsoft ASP.NET 2.0
   Data Source Controls

Summary: This is the third article in a series on authoring data source controls. In this article, Nikhil looks at how to perform and encapsulate asynchronous data access, while presenting a reusable asynchronous data-access framework that builds upon what is available in Microsoft Visual Studio 2005. (6 printed pages)

Click here to download the code sample for this article.

Contents

Introduction
Background
The Framework
The Sample

This article originally appeared on Nihkil's blog; you can join in on the discussion there.

In parts 1and 2, I built a WeatherDataSource control that runs against the XML API provided by weather.com using WebRequest and WebResponse to access data over HTTP. So far the service was being accessed synchronously. As a result, page processing was blocked until the Web request completed. This worked fine for test pages, and perhaps in small sites, but would fail miserably in a site that receives decent traffic; for example, a portal page where a weather module might be common.

Introduction

There are a fixed number of threads in the thread-pool that can be used to serve requests, and unfortunately the solution isn't simply to bump up the limit (threads consume resources, and the CPU as well). So while one page is blocked waiting for another server, it is also consuming a thread, and potentially causing other incoming requests to wait longer in the queue. This results in slower access times for your site, and reduces CPU utilization. In Visual Studio 2005, we introduced async pages, which allow controls to define tasks they want to complete asynchronously, that is, without blocking the thread used to process the request. I won't describe the details of async pages themselves here; both Dmitry and Fritz Onion have done so in the past. What I'll cover here is how to utilize this capability in data source controls, using an addon framework for implementing asynchronous data sources.

Background

In part 1, I alluded to a somewhat odd design of the DataSourceView class:

public abstract class DataSourceView {
    public virtual void Select(DataSourceSelectArguments arguments,
                               DataSourceViewSelectCallback callback);
    protected abstract IEnumerable ExecuteSelect(
        DataSourceSelectArguments arguments);

    ...
}

You'll notice the public Select method does not actually return any data. Instead it takes in a callback and returns the data via that callback. It simply calls the protected ExecuteSelect (which always performs synchronous data access) to retrieve the data to hand back to the data-bound control. The default implementation of the DataSourceView class does not actually do anything asynchronously. The reason for that is we don't have any asynchronous data source controls out-of-the-box. But the design of the OM does allow implementing asynchronous data access, where data is not available until the asynchronous work has completed. Hence, we have a callback-based model.

Those who are familiar with asynchronous APIs in the framework will notice the absence of the async pattern: public Select, BeginSelect, and EndSelect methods, where the data-bound control chooses which methods to call. However, data-bound controls do not have the ability to decide whether to choose the synchronous API or the asynchronous one. Furthermore, it simply does not make sense to add a property on data-bound controls. Data source controls encapsulate the details of how to access a data store, and whether that happens synchronously or asynchronously should be a decision made by the data source whether it is based on semantics or by a customizable property. The right spot for a potential "bool PerformAsyncDataAccess" property belongs on the data source control itself. This also allows the data source control to perform data access using a single approach even if multiple data-bound controls are bound to the same data sources. I've had to explain these subtle concepts behind the architecture a few times now; hopefully this clarifies the design.

One final note about asynchronous tasks: The page developer does have ultimate say in whether the page should perform any asynchronous work at all (via the Async attribute on the Page directive). Hence any well-written data source control must degenerate to performing synchronous data access as needed.

The Framework

In the framework (I will make this available with the rest of the sample at the end of the series), I have put together AsyncDataSource and AsyncDataSourceView base classes, which can be used to implement data source controls that can perform data access asynchronously. Here is a quick overview of what is in the framework, along with comments to help make sense out of it:

public abstract class AsyncDataSourceControl : DataSourceControl, 
    IAsyncDataSource {
    private bool _performAsyncDataAccess;

    protected AsyncDataSourceControl() {
        _performAsyncDataAccess = true;
    }

    public virtual bool PerformAsyncDataAccess {
        get; set;
    }

    bool IAsyncDataSource.IsAsync {
        get { return _performAsyncDataAccess && Page.IsAsync; }
    }
}

public abstract class AsyncDataSourceView : DataSourceView {

    protected abstract IAsyncResult BeginExecuteSelect(
        DataSourceSelectArguments arguments,
        AsyncCallback asyncCallback,
        object asyncState);

    protected abstract IEnumerable EndExecuteSelect(
        IAsyncResult asyncResult);

    protected override IEnumerable ExecuteSelect(
        DataSourceSelectArguments arguments) {
        // Implement the abstract ExecuteSelect method 
        // inherited from DataSourceView
        // by using BeginExecuteSelect and EndExecuteSelect
        // to do synchronous data access
        // via blocking.
    }

    private IAsyncResult OnBeginSelect(object sender, 
        EventArgs e, AsyncCallback asyncCallback,
        object extraData);
    private void OnEndSelect(IAsyncResult asyncResult);

    public override void Select(DataSourceSelectArguments arguments,
                                DataSourceViewSelectCallback callback) {
        if (_owner.IsAsync) {
            // Call Page.RegisterAsyncTask using 
            // OnBeginSelect and OnEndSelect
            // as the BeginEventHandler and EndEventHandler
            // methods to indicate the
            // need to do async work. These methods in turn 
            // call on the specific
            // data source implementation by calling the 
            // abstract BeginExecuteSelect and
            // EndExecuteSelect methods that have been 
            // introduced in this class.
        }
        else {
            // Perform synchronous data access
            base.Select(arguments, callback);
        }
    }

    ...
}

The Sample

The new AsyncWeatherDataSource will now derive from AsyncDataSourceControl, and AsyncWeatherDataSourceView will derive from AsyncDataSourceView.

public class AsyncWeatherDataSource : AsyncDataSourceControl {

    // Identical to WeatherDataSource
}

private sealed class AsyncWeatherDataSourceView : AsyncDataSourceView {
    private AsyncWeatherDataSource _owner;
    private WeatherService _weatherService;

    public AsyncWeatherDataSourceView(AsyncWeatherDataSource owner, 
        string viewName)
        : base(owner, viewName) {
        _owner = owner;
    }

    protected override IAsyncResult BeginExecuteSelect(DataSourceSelectArguments arguments,
    AsyncCallback asyncCallback,
    object asyncState) {
        arguments.RaiseUnsupportedCapabilitiesError(this);

        string zipCode = _owner.GetSelectedZipCode();
        if (zipCode.Length == 0) {
            return new SynchronousAsyncSelectResult(/* selectResult */ 
                null,
                asyncCallback, asyncState);
        }

        _weatherService = new WeatherService(zipCode);
        return _weatherService.BeginGetWeather(asyncCallback, asyncState);
    }

    protected override IEnumerable EndExecuteSelect(IAsyncResult asyncResult) {
        SynchronousAsyncSelectResult syncResult = 
            asyncResult as SynchronousAsyncSelectResult;
        if (syncResult != null) {
            return syncResult.SelectResult;
        }
        else {
            Weather weatherObject = 
                 _weatherService.EndGetWeather(asyncResult);
            _weatherService = null;

            if (weatherObject != null) {
                return new Weather[] { weatherObject };
            }
        }

        return null;
    }
}

The key thing to notice is that, when using the framework, all you have to implement is BeginExecuteSelect and EndExecuteSelect. In the implementations of these you'll typically call the BeginXXX and EndXXX methods exposed by various objects in the framework such as WebRequest or IO Stream (and SqlDataCommand, as well, in Visual Studio 2005), and return the IAsyncResult you get back. In the case of the sample, I have a WeatherService helper class that wraps the underlying WebRequest object.

For those really missing the async pattern, you'll see it in action here; both with the BeginExecuteSelect and EndExecuteSelect that you implement, and the Begin and End methods you call to get back and return instances of IAsyncResult.

What is probably most interesting is the SynchronousAsyncSelectResult class (which I agree is an oxymoron in some sense). This class comes along with my framework. It is basically an IAsyncResult implementation that holds data available immediately, and reports true from its IAsyncResult.CompletedSynchronously property. For now, this is used only in the case in which a zip code is not selected, and null needs to be returned (no point starting an async task to simply return null), but as you'll see in the following article, this will be useful in other scenarios, as well.

The page infrastructure hides you from most of the details of doing work asynchronously in the context of Microsoft ASP.NET. I hope that the framework I'll provide should make it possible to write data sources that use this infrastructure with minimal work. Nevertheless, implementing asynchronous behavior is complex by its very nature. Sometimes a read, followed by some questions, and a second read get the point across. You can send questions or discuss this using my comment form below. And stay tuned for even more performant data access that I'll cover in the next article.

 

About the author

Nikhil Kothari is an architect on the Web Platform and Tools team at Microsoft (which delivers IIS, ASP.NET, and Visual Studio Web development tools). Specifically, he is responsible for the overall Web Forms (a.k.a. server controls, a.k.a. page framework) feature area. He's been working on the team since the early XSP and ASP+ days; prior to his current role, he led the development of the page framework and several of the controls you can find on the ASP.NET toolbox today.

© Microsoft Corporation. All rights reserved.