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.
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.
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.
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.
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); } }
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]
http://www.developerfusion.com/article/84362/extending-filesystemwatcher-to-aspnet/