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.
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;
}
}
}
}
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).