June 2006 - Posts

I recently maintained an ASP .Net application where Xml Serialization was used to read and persist configuration data during page requests. When site traffic increased, users encountered System.IO.IOExceptions with the message "The process cannot access the file "c:\SomeFolder\SomeFile.xml" because it is being used by another process. The exception was not always encountered 100% of the time. The problem could be easily reproduced by opening two browser windows, opening the page in each browser, and quickly refreshing each browser side by side.

I was able to narrow down the problem to a method in a class where deserialization takes place on each page request. The following code was used:

XmlSerializer serializer = new XmlSerializer(typeof(Foo));
TextReader reader = new StreamReader(serializedFooPath);
myFoo = (Foo)serializer.Deserialize(reader);
reader.Close();

Basically the TextReader locks the file until it is done using it.  You can easily avoid locking a file by changing how it is accessed. Simply introduce a FileStream object that can control how the file is accessed rather than allowing the TextReader to control it for you. The FileStream object can accomplish this by using the FileShare.Read enumeration value:

XmlSerializer serializer = new XmlSerializer(typeof(Foo));
FileStream fs = new FileStream(serializedFooPath, FileMode.Open, FileAccess.Read, FileShare.Read);
TextReader reader = new StreamReader(fs);
myFoo = (Foo)serializer.Deserialize(reader);
reader.Close();
fs.Close();

According to MSDN, any request to open the file will fail until the file is closed if FileShare.Read is not specified [1].

Making this change eliminated the problem and the IOException no longer occurred. It was an easy quick fix - perhaps a better deserialization strategy would come in handy but this fix was able to fix the bug quickly. Thanks to Jake for helping out by supplying the MSDN reference.

[1] Refer to http://msdn2.microsoft.com/en-us/library/system.io.fileshare.aspx for full documentation of the FileShare enumeration and http://msdn2.microsoft.com/en-us/library/system.io.filestream.aspx for full documentation of the FileStream class.

with no comments
Filed under:

The ASP .Net web caching API is a powerful and useful tool at your disposal. Here are a few tips and suggestions that will help make your code more flexible and maintainable when using the caching API:

  • Define unique keys for cached items in your domain
  • Store expiration durations or times in a configuration file (web.config)
  • Include a configuration setting for enabling and disabling caching
  • Encapsulate caching behavior in a class

Define Unique Keys

You can get or insert items from and into the cache using a key. A key is a String value that uniquely identifies an item stored in the cache. Normally you will want to use a key based on the unique identifier of some domain object you are working with. For example, you may have a Customer class with an Integer property named CustomerId that uniquely identifies the customer in your domain. Using the CustomerId as the basis for the cache key would be a good choice. However, the unique identifier of the domain object alone is not a good, complete choice for the cache key. Consider this example:

//Bad example.  Non-unique keys are used.
Customer customer = GetCustomerById(100);
Employee employee = GetEmployeeById(100);
double customerValue = 10;
double employeeValue = 100;
Cache.Insert(customer.CustomerID.ToString(), customerValue);
Cache.Insert(employee.EmployeeID.ToString(), employeeValue);

The same cache key ("100") is being used to store two different values for two different types of objects, and the employee's value will overwrite the customer's value. In real life you wouldn't do anything like the example above on purpose, but when dealing with the cache across many web forms or web controls in a web application, you may not know or remember what other keys are being used to cache values.

A better approach would be to qualify the cache key with something that describes the data being stored:

//Better example.  Unique keys are used.
Customer customer = GetCustomerById(100);
Employee employee = GetEmployeeById(100);
double customerValue = 10;
double employeeValue = 100;
Cache.Insert("Customer" + customer.CustomerID.ToString(), customerValue);
Cache.Insert("Employee" + employee.EmployeeID.ToString(), employeeValue);

Make Sliding and Absolute Expirations Configurable

Don't hard-code expiration times in your caching code. Someone (either you or a user) will probably change their mind in the future about how long to store data in the cache. Use a customer configuration section or the AppSettings section in web.config to store cache expiration values, as this will allow you to change cache times without re-compiling the application.

The web.config file could use an appSettings section as simple as the example below to configure the number of minutes to store an item in the cache:

<appSettings>
  <add key="customerYtdSalesCacheMinutes" value="30"/>
</appSettings>

Insert items into the cache using the app setting value:

...
//calculate the absolute expiration DateTime
DateTime absoluteExp =
    DateTime.Now.AddMinutes(YtdSalesCacheMinutes);
//Use a sliding expiration of zero
TimeSpan slidingExp = TimeSpan.Zero;
Cache.Insert(
    someKey, someValue, null, absoluteExp, slidingExp);
...

public int YtdSalesCacheMinutes
{
  get 
  { 
    return Convert.ToInt32(
      ConfigurationManager.AppSettings["customerYtdSalesCacheMinutes"]); 
  }
}

Enable and Disable Caching Through Configuration

There may be times when you'll want to disable caching within your app (e.g. during development or debugging). Use a configuration setting and add some simple logic to your code to handle this:

<!-- web.config -->
<appSettings>
  <add key="customerYtdSalesCacheEnabled" value="true"/>
</appSettings>

// C# code //
...
if (YtdSalesCacheEnabled)
    Cache.Insert(key, myValue, null, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null);
...

public bool YtdSalesCacheEnabled
{
    get
    {
        string setting = 
            ConfigurationManager.AppSettings["customerYtdSalesCacheEnabled"];
        if (string.Compare(setting, "true", true) == 0)
            return true;
        return false;
    }
}

Encapsulate Caching Behavior in a Class

Insert and obtain cached values to and from the ASP .Net cache with a class designated for that behavior. This will provide the full benefit of an object-oriented approach. The most obvious and immediate benefits are that caching can be re-used from anywhere within the application, cache keys can be generated consistently, and configuration settings can be obtained in a single place. The design of such a class could take on many different forms to suit the needs of different applications, but here is one example:

using System;
using System.Web.Caching;
using System.Configuration;

namespace MJH.Caching
{
    public class CustomerCacheManager
    {
        private const string CUST_YTD_SALES_KEY = "CustomerYTDSales-{0}";

        /// <summary>
        /// Obtains a customer's YTD sales value.
        /// </summary>
        /// <param name="cache"></param>
        /// <param name="customer"></param>
        /// <returns></returns>
        public double GetCustomerYtdSales(Cache cache, Customer customer)
        {
            //get the cache key
            string key = GetCustYtdKey(customer.CustomerID);
            
            //return the value from the cache if it exists
            if (cache[key] != null)
                return (double)cache[key];

            //calculate the (time consuming) YTD sales value
            double ytdSalesValue = customer.GetYtdSalesValue();

            //if caching is enabled, store the calculated
            //value in the cache
            if (YtdSalesCacheEnabled)
            {
                DateTime absoluteExp = 
                    DateTime.Now.AddMinutes(YtdSalesCacheMinutes);
                TimeSpan slidingExp = TimeSpan.Zero;
                cache.Insert(
                    key,                        //item key
                    ytdSalesValue,              //item value
                    null,                       //dependencies
                    absoluteExp,                //absolute expiration
                    slidingExp);                //sliding expiration
            }
            return ytdSalesValue;
        }

        /// <summary>
        /// Generates a cache key for a customer
        /// YTD sales value.
        /// </summary>
        /// <param name="customerId"></param>
        /// <returns></returns>
        private string GetCustYtdKey(int customerId)
        {
            return string.Format(CUST_YTD_SALES_KEY, customerId.ToString());
        }

        /// <summary>
        /// Gets the nubmer of minutes that YTD sales
        /// values should be cached.
        /// </summary>
        public int YtdSalesCacheMinutes
        {
            get { 
                return Convert.ToInt32(
                    ConfigurationManager.AppSettings["customerYtdSalesCacheMinutes"]); 
            }
        }

        /// <summary>
        /// Gets whether caching is enabled.
        /// </summary>
        public bool YtdSalesCacheEnabled
        {
            get
            {
                string setting = ConfigurationManager.AppSettings["customerYtdSalesCacheEnabled"];
                if (string.Compare(setting, "true", true) == 0)
                    return true;
                return false;
            }
        }
    }
}
with 1 comment(s)
Filed under:

Make sure you check out SeeWindowsVista.com to see a lot of cool things you can do with WPF. Lots of examples of 2D and 3D, including the earlier demos/proof-of-concepts (e.g. NASCAR, North Face, etc).

with 1 comment(s)
Filed under: