Data Source Controls, Part 4: Caching

 

Nikhil Kothari
Microsoft Corporation

November 2005

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

Summary: This is the fourth article in a series on authoring data source controls. In this article, Nikhil describes how to incorporate caching into data access logic, enabling further performance gains, and improvements in end-user experience. (4 printed pages)

Click here to download the code sample for this article.

Contents

Introduction
The Sample

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

Last time, I added asynchronous data access capabilities into the on-going Weather data source scenario using the new async tasks feature in Microsoft Visual Studio 2005. While this is a powerful capability, the goal of this installment is to present another key aspect of data sources, namely caching, that should be considered before going down the async route. I presented these in reverse simply so I could make the previous statement.

Introduction

Asynchronous data access is great for server performance, perhaps, but end-user experience is just as important (if not more). Asynchronous processing doesn't do anything to get the page back to the user any faster: the user has to wait just as long. So, another infrastructure piece that will be interesting to data source developers is Cache. The architecture really starts becoming valuable when the data source control can hide or encapsulate these sorts of details, incrementally builds in these features in a newer version, or allows the developer to tweak specifics of the behavior, without requiring any changes from the data-bound control. The fact that data is being returned from a cache shouldn't matter to the consumer of the data.

Asynchronous data scenarios are especially suitable for more intelligent caching on the Web server end—especially if there isn't a real-time requirement, and stale data for a short amount of time does the job just as well. For example, in the WeatherDataSource scenario, the weather information can be cached for, say, half an hour. Weather doesn't change that fast... for the most part!

The Sample

Incorporating basic usage of cache isn't terribly complicated. It just requires a few modifications to check and return data from the cache, or to stash in there when it is first retrieved (as shown in bold below).

private sealed class AsyncWeatherDataSourceView : AsyncDataSourceView {

    ...

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

        string zipCode = _owner.GetSelectedZipCode();
        if (String.IsNullOrEmpty(zipCode)) {
            IEnumerable selectResult = null;
            return new SynchronousAsyncSelectResult(selectResult, 
                asyncCallback, asyncState);
        }

        string cacheKey = GetCacheKey(zipCode);
        Weather weatherObject = (Weather)_owner.Context.Cache[cacheKey];

        if (weatherObject != null) {
            IEnumerable data = new Weather[] { weatherObject };
            return new SynchronousAsyncSelectResult(data, 
                 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) {
                string cacheKey = GetCacheKey(weatherObject.ZipCode);
                _owner.Context.Cache.Insert(cacheKey, weatherObject, null,
                DateTime.Now.AddMinutes(30), TimeSpan.Zero);

                return new Weather[] { weatherObject };
            }
        }

        return null;
    }

    private string GetCacheKey(string zipCode) {
        return "AsyncWeatherDataSource:" + zipCode;
    }
}

Here you will see a second use of the SynchronousAsyncSelectResult that I alluded to last time around. When the weather information is present in cache, there is no async work that needs to be done. Hence the existing data is packaged in this custom implementation of IAsyncResult. When asynchronous work is performed, the result is stored into cache before it is handed out.

In the sample, the data source simply caches the weather data for a fixed amount of time, without exposing any knobs to the developer. In other data scenarios, the data source may offer the ability to control cache timeout periods, sliding expiration options, dependencies, and so on. These are simply implementation details around the basic idea of incorporating a caching capability.

The key idea is that data access is fast most of the time, and the async work is only performed when the data is not present in the cache.

There are some gotchas that you should be aware of when using cache.

  • Cached data should not be keyed by the current user. If you have too many users, you'll end up creating too many cache entries, consuming lots of memory, incurring greater costs from the cache system managing all those entries, and potentially causing useful entries to be evicted too soon in order to make room for new ones. The key is to think about data independently from the consumer and user context of the data, unless it absolutely essential (profile data associated with a user, for example).
  • For similar reasons, cached data should not be keyed by things like user agent. As described in this post, the number of user agent types might be infinite. In fact, this guideline holds true for any incoming request data. For example, a particular denial of service (DOS) attack involves sending requests with random HTTP headers, post data, and others. You'll want to validate and/or sanitize the data, before it is used to construct the cache key.

In the final article in this series, I'll cover the design time aspects of the control.

 

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.