Extending FileSystemWatcher to ASP.NET

This article was originally published in VSJ, which is now part of Developer Fusion.
I received a number of responses to my article in the July/August edition of VSJ, Your fingers on the filesystem, regarding the use of the FileSystemWatcher class in ASP.NET applications. The original article mentioned using FileSystemWatcher to update files on a web server, and the model I envisaged was similar to the Photo Library application described in the article. This would allow files to be copied up to a file-based web server (e.g. IIS) in a controlled manner. Some of the responses I received were asking about the use of FileSystemWatcher within ASP.NET applications, for example to update a product list.

In this article I will show how FileSystemWatcher can be used within web applications, and then go on to describe the Cache mechanism that provides an ASP.NET specific alternative.

Using FileSystemWatcher from ASP.NET

When you create a project with Visual Studio it automatically creates for you a Global.asax file that contains handlers for the principle events in the lifecycle of an application including Start and End. This seems like the obvious place to put our FileSystemWatcher object, but although Global.asax defines a class named Global (derived from HttpApplication), the ASP.NET system will not necessarily use the same instance of this object for all of the events handled there!

Applications in ASP.NET store application-wide information in name/value pairs in the AllKeys collection of the Application object (a member of HttpApplication and thus Global), and ASP.NET does ensure that the Application object is the same for each event handled. To manage the lifecycle of the FileSystemWatcher it must be stored in this collection.

Event handlers for the FileSystemWatcher events may be added to the Global class, but you must ensure that they only access data in the Application collection, and that they do so in a thread-safe manner (see my previous article). Care should be taken when applying synchronisation in a server environment to ensure that performance and scalability are not compromised.

The basic code for adding a FileSystemWatcher to an ASP.NET application looks like this:

protected void Application_Start(
	Object sender, EventArgs e)
{
	FileSystemWatcher fsw =
		new FileSystemWatcher(
		Server.MapPath( “.” ) );
	Application.Add( “myfsw” , fsw );
	// Add event handlers here
	fsw.EnableRaisingEvents = true;
}
Note the use of the Server.MapPath function to obtain the full path of the application directory from the relative path provided by IIS.

To be a good citizen we should ensure that we dispose of this object properly at application end. The following function shows how to do this. First we must retrieve a reference to the object from the application collection, then remove it from the collection, and finally dispose the object:

protected void Application_End(
	Object sender, EventArgs e)
{
	FileSystemWatcher fsw =
		(FileSystemWatcher
		)Application[“myfsw”];
	Application.Remove( “myfsw” );
	fsw.Dispose();
}
The Application collection is in some ways similar to the Session collection described in the February 2004 edition of VSJ in an excellent article by Dino Esposito, Getting Serious about ASP.NET Session Management (www.vsj.co.uk/articles/display.asp?id=286). However, the data in the Application collection is always stored In-Process and is therefore not shareable across multiple machines.

ASP.NET and Data Files

An alternative to using a FileSystemWatcher to monitor the filesystem in ASP.NET is to make use of the built-in features of the Page cache. Unlike the Application and Session collections, the Page.Cache collection allows you to add expiry times and dependencies to the stored items. The best way to explain how this functionality can be used is with an example.

A web shop typically has a product list that is updated infrequently compared with the frequency of page hits, but much more often than the structure of the site. It is not desirable to write new code (HTML or C#) to add these products, especially since this would usually require down-time on the server.

One solution is to provide product information in an XML file – a simple example is:

<?xml version=”1.0” encoding=”utf-8”?>

<ProductList xmlns=”http://
	cyclops-online.co.uk/ProductList/
	ProductList.xsd”>

<Product>

<Title>Mouse House
</Title>

<Price>4.99
</Price>

</Product>

<Product>

<Title>Monitor Mirror
</Title>

<Price>9.99
</Price>

</Product>

<Product>

<Title>Stick-on Panic
			Button
</Title>

<Price>2.99
</Price>

</Product>

</ProductList>
Visual Studio will automatically generate a Schema (XSD) file for this XML file if you select the “Create Schema” option from the “XML” menu that appears when editing an XML file. This schema can be modified if required (I set the price field to be a decimal rather than a string), and then the “Generate Dataset” option on the Schema menu can be used to automatically generate a C# class. This class provides a neat interface to the XML data via ADO.

In order to ensure that each time the page is hit the product list is available for rendering each time it is loaded, I could add the following to the Page_Load function:

private static readonly string
	_productListTag = “ProductList”;
private static readonly string
	_productListFile = “ProductList.xml”;
private ProductList _productList = null;

private void Page_Load(object sender, System.EventArgs e)
{
	productList = new ProductList();
	_productList.ReadXml(Server.MapPath(_productListFile));
}
This code reads the product list each time the page is hit, so updates to the product list are reflected on the site immediately. It also means that for each and every page hit the server will read the XML file, parse its contents, and create the corresponding ADO objects. To avoid this load on the server we could use the Application collection:
private void Page_Load(object sender, System.EventArgs e)
{
	_productList = (ProductList)Application[
		_productListTag];

	if ( null == _productList )
	{
		_productList = new ProductList();
		_productList.ReadXml(Server.MapPath(
			_productListFile));
		Application.Add(_productListTag,
			_productList);
	}
}
This code attempts to load the product list from the Application collection and only reads it from the XML file if this operation fails. This reduces the server load in reading and parsing the XML file significantly, but as the read only takes place infrequently (e.g. when the ASP or IIS process is restarted) updates could take days or even weeks to be reflected on the site (yes, I know it would be months or years on a Linux server!). You could write your own code to handle this either using a FileSystemWatcher as shown above, or by storing a “time of last read” and updating after a fixed interval. Fortunately ASP.NET has a built-in mechanism to do this for us.

Using The ASP.NET Page Cache

Rather than storing our data in the Application collection we can store it in the Page.Cache. Its function is conceptually similar – to store a name/value pair on behalf of the application. The differences become apparent when you look at the parameter lists of the Add functions for Application and Page.Cache however. In addition to the name/key and value parameters, Page.Cache.Add has the following:
  • dependencies – a CacheDependency object indicating what this cache entry’s data is dependent on
  • absoluteExpiration – a DateTime indicating the absolute date and time when the data in this cache entry should be considered invalid (DateTime.MaxValue disables this functionality)
  • slidingExpiration – a TimeSpan indicating the elapsed time since last usage that will render cache entry invalid (TimeSpan.Zero disables this functionality, cannot be used in conjunction with absoluteExpiration)
  • priority – CacheItemPriority enumeration indicating the priority of this item in the ASP.NET cache
  • onRemoveCallback – a callback function enabling the application to take action when the data is removed from the cache
By selecting the parameters to the Add function carefully, we can get ASP.NET to automatically flush the ADO dataset representing the product list from the cache when the file changes:
Cache.Add(	_productListTag, _productList,
	new CacheDependency(Server.MapPath(_productListFile)),
		DateTime.MaxValue, TimeSpan.Zero,
		CacheItemPriority.NotRemovable, null);
The CacheDependency object created and passed as a parameter makes an explicit connection between the XML data file and the cache data. If the XML file changes, the data in the cache will be discarded. The next time the page is requested, the data will not be found in the cache and therefore the updated XML file will be read in.

Taking the Cache Beyond the Page

It is clear that the Page cache offers more functionality than the Application collection, but with more limited scope. How can we take advantage of this functionality for data that is used by more than one page? Fortunately this can be achieved by storing the cached data in the Application collection and merely using the Page.Cache to let us know when the dependency changes, using the onRemoveCallback. This can be done from a single class shared by all pages that need to access the data:
public static ProductList
	GetProductList(
	System.Web.UI.Page page)
{
	ProductList productList =
		(ProductList)page.Application[
		_productListTag];
	if (null == productList)
	{
		productList =
			new ProductList();
		productList.ReadXml(
			page.Server.MapPath(
			_productListFile));
		page.Cache.Add(_productListTag,
			page.Application, new
			CacheDependency(
			page.Server.MapPath(
			_productListFile)),
			DateTime.MaxValue,
			TimeSpan.Zero,
			CacheItemPriority.
			NotRemovable, new
			CacheItemRemovedCallback(
			RemoveCallback));
		page.Application.Add(
			_productListTag,
			productList);
	}
	return productList;
}
private static void RemoveCallback(
	string tag, object obj,
	CacheItemRemovedReason reason)
{
	HttpApplicationState Application =
		(HttpApplicationState)obj;
	if(CacheItemRemovedReason.
		DependencyChanged == reason)
	{
		Application.Remove(tag);
	}
}

Detecting Updates in SQL Server

Many web applications store their data in a database such as SQL Server rather than in XML files, but the CacheDependency object provides no obvious mechanism for detecting changes in SQL Server. This is an irritating omission from ASP.NET, which is set to be corrected in ASP.NET 2.0 (now in Beta, releasing 2005) by a new SqlCacheDependency class that can be used to detect changes in SQL Server tables.

If you can’t wait for ASP.NET 2.0 then the solution is to write an Extended Stored Procedure that can be called from SQL Server to update a file when a table changes (this could be run from a trigger or the stored procedure that performs the update). This file can then be monitored by the web application using a FileSystemWatcher or CacheDependency. For large-scale applications, the file could exist on a share accessible to multiple web servers and multiple database servers.

If you are interested in this technique, then I recommend the excellent article on the subject by Jeff Prosise which can be found in his Wicked Code column in the April 2003 edition of MSDN Magazine. This article contains sample code for the web application, SQL database and the Extended Stored Procedure.

I hope that in this article I have shown the benefits of using filesystem monitoring in ASP.NET applications, and shown how this can be done using either a FileSystemWatcher or the Page.Cache.


Ian Stevenson has been developing Windows software professionally for almost 10 years, in areas ranging from WDM device drivers through to rapid-prototyping of enterprise systems. Ian currently works as a consultant for The Generics Group () and can be contacted at [email protected].

Other ASP Caching Options

This article describes a number of mechanisms for caching data within ASP.NET pages or applications, but the pages are still re-rendered by the ASP.NET engine each time they are requested. Even greater savings of server effort can be made by allowing ASP.NET to cache the rendered page in its entirety. Caching can also be enabled for individual ASP.NET user controls. To find the relevant documentation on these forms of caching, search for “@OutputCache” (declarative control of caching) or “HttpCachePolicy” (programmatic control).

Under the Hood of the ASP CacheDependency

Using Lutz Roeder’s excellent Reflector utility to explore the inner workings of CacheDependency, I discovered that it makes use of functionality provided in the native “aspnet_isapi” DLL, and does NOT use the .NET FileSystemWatcher class. “aspnet_isapi” is the IIS extension DLL that enables ASP.NET functionality and its interface is undocumented, so I can’t find any information on the “DirMonOpen” and “DirMonClose” functions that are called. A little time spent in the debugger proves that IIS makes extensive use of the ReadDirectoryChangesW Win32 API (as used by FileSystemWatcher). It therefore seems likely that CacheDependency piggy-backs on the existing IIS file change mechanism.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“PHP is a minor evil perpetrated and created by incompetent amateurs, whereas Perl is a great and insidious evil perpetrated by skilled but perverted professionals.” - Jon Ribbens