Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

ASP.NET Configuration


July, 2005: ASP.NET Configuration

Douglas is the author of Designing Microsoft ASP.NET Applications and owner of Access Microsystems. Doug can be reached at [email protected].


When it was first introduced, a nice ASP.NET feature was the standard way for configuring applications. In all applications—and certainly ASP.NET ones—you will always need to configure certain aspects of the application.

In classic ASP, you used an application event handler in the Global.asa file to set application-level variables. While this was fine when you first approached the application, there was a learning curve as you configured applications in slightly different ways. ASP.NET lets you use the appSettings section of the Web.Config file. In Listing One, the add tag has attributes key and value. This example shows some of the common uses—storing an application and facility name and connection string. Another good example of a string that can be stored in a configuration file is a URL to be used by a web service, which might be reasonably different in development versus production. While all of these examples are clearly string values, it is not that difficult to take string values like "true" and "false" and parse them into a bool value, or to do the same with numbers or dates. Retrieving a value is simple as well. In C#, you would do this:

using System.Configuration;
string strCn=
ConfigurationSettings.AppSettings
["CnStr"].ToString();

This is an acceptable way to get configuration settings and works well in a number of scenarios. However, there are situations where using the Web.Config file is not convenient. For instance, whenever you change the Web.Config file, the application resets. So if you have a configuration setting that needs to change periodically, making the changes on live sites can be problematic. In many respects, this is a good thing, as it ensures that changes in Web.Config take effect immediately. Many settings in Web.Config really do require that the application be restarted; however, many of the changes in appSettings do not require an application restart (though having changes reflected immediately is also a nice feature).

Next, there is the issue of deploying the application. On more than one occasion, when an application I am working on moves from development to staging to production servers, the wrong configuration information moves to the wrong server, and the application breaks. While for many application updates, you can simply deploy all files except the Web.Config file, on occasion, you will need to add values to the Web.Config; even if the value is the same for all servers, you cannot simply copy files over, as there are other server-specific settings already in the Web.Config. I have a client where I must do application deployments without ever having any access to the production server. Deploying changes might mean requiring the webmaster of your production site to edit the Web.Config manually. This does not work out well much of the time.

A Configuration Solution

So while the standard ASP.NET Configuration handling is better than in Classic ASP, there are still some problems. One solution I've found that resolves all the problems just mentioned is to use machine-specific configuration files that can be mapped into a DataSet as required, then cached to avoid constantly hitting the filesystem.

Initially, I had considered a system where inside the Web.Config appSettings section, I would declare what sort of server this was using a named configuration. For instance, I might have "Development," "Staging," and "Production," then use configuration files named "Development .Config," "Staging.Config," and "Production.Config," respectively. While this almost always works, it has the unfortunate side effect of requiring a server that is changing roles to have the Web.Config touched, and thus, the application is restarted. When you are changing roles, restarting the application is not a terrible idea. Needing to touch the Web.Config was a show stopper for me.

My solution uses a configuration file named for the machine. Thus, my home workstation, named "Xeon," would use Xeon.Config. To handle the simplest case, I also have a default configuration file, not surprisingly named "Default.Config." The only scenario where this solution is awkward is when you have a dozen or so production machines on a web farm, and each needs an identical copy of the configuration file appropriately named. In practice, my solution to this is to allow Default.Config to act as the file for use by the production servers on the web farm, and specify individual configuration files on development and staging servers. Seldom are there web farms of development and staging servers.

The Visual Studio .NET 2003 Solution (available electronically; see "Resource Center," page 3) contains three projects. The first is ConsoleApplication1. This is an application I created simply to take a database schema I wanted to use and stream it from a DataSet to an XML file and then back to the DataSet when needed. Listing Two is the Create script for the table I wanted to mirror in a DataSet. I decided that a 50-character Key would be long enough, and allowed 255 characters for the Value to handle the longest connection string I could imagine. Once I added a couple of sample values to the table, I ran Listing Three in the ConsoleApplication1 application to create a sample configuration file. This created a file named Default.Config in the c:\work folder. Obviously, to use this code, you should change the connection string and the path to the file to be created. Once created, I hand modified the resulting file slightly to end up with a file that looked like Listing Four. This XML is a bit more verbose than the appSettings section of the Web.Config, but I consider it a reasonable trade off to ensure that the file can be read into a DataSet.

My next step was to create a new project as a class library. I called this project "ConfigurationLib," and added a single source file, ConfigurationClass.cs; see Listing Five.

I declared a number of class instance variables, including m_Context of type HttpContext, which lets the class know details about the current request inside the class instance. I use the HttpContext to access the cache as well as to use MapPath(). Another instance variable (exposed publicly as a Read Only property) is the bool variable m_HitFile. This is used for letting my feedback know whether the actual underlying file was read on a given request. Generally, this is not important to know; however, for demonstrating the class, it is a good thing. Finally, there is a constant string c_CacheKeyName, which is used as an index into the Cache collection. Placing key names like this in a constant ensures the same name is used throughout. I use the key name "ConfigurationCache_DDJ," which is unlikely to cause a name collision.

There is a single constructor that takes an HttpContext object. The constructor simply sets the required member variables and returns.

The whole reason for this class is to get values, so not surprisingly, the first public method is named GetValue(). Using a getter, rather than using an operator overload, is a decision made to ensure that the library is usable by any .NET-compliant system. GetValue() first retrieves a DataSet containing the configuration information by calling GetConfigDataset(). This is where most of the work of the class is done.

Using the Cache

One of the most important performance improvements in ASP.NET is the ability to cache objects for later use. Caching in ASP.NET can be done declaratively for entire pages, page fragments (User Controls), as well as programmatically for any arbitrary object. Caching is most useful for scenarios where a page or page fragment or some other object is expensive (in terms of processing time) to generate, and can reasonably be reused. An item in cache is kept in cache until a specified expiration time, or based upon a change to a file on disk. The next version of ASP.NET will support cache dependencies that are based upon changes in a database. One thing to remember about the cache is that when you place an item in cache with a time or file dependency, the cache item can be cleared from the cache at any time when there is memory pressure. In this respect, the cache is like a smart Application object, where the least recently used items can be purged if the system needs the memory for other purposes. Note also that the cache, unlike session state, cannot be scaled across multiple servers. This is not a bad thing, in this case, because we want the configuration information to be machine specific.

Initially, I used a timed dependency when inserting items into the cache, but eventually realized that using a file cache dependency was a better option. By doing this, I read the configuration file into a DataSet and placed the DataSet into the cache, so that when the configuration file is modified, the database will be cleared from the cache and signal that the program needs to reread the file. The code involved is code that is very often done slightly incorrectly. First, the correct code to check whether an item is in the cache is Listing Six. Listing Seven is an alternate way to do this (and one that introduces a subtle bug). This code looks similar, but introduces a problem. What happens if between the if statement and the assignment and use of the ds variable, the file changes, or for some other reason the item is removed from the cache? The answer is, "nothing good." I have seen the incorrect pattern used in both production applications as well as a number of books. You should also probably serialize the refill of the cache, so that you do not have two threads simultaneously filling the cache. In this example, no harm is really done if two threads are filling the cache at the same time, and while reading in the configuration file may be a little slow, it is not slow enough to justify the additional overhead.

The Rest of the Code

If the cache does not contain the DataSet identified by c_CacheKeyName, then the cache needs to be refilled. Listing Eight does this. In this code, I initially get the machine name to use in finding the machine-specific configuration file. When I first wrote this code, I was using the name as provided as SERVER_NAME in the Request.ServerVariables collection. The problem with using the SERVER_NAME is that a site can often be reached using a number of URLs. For instance, my machine is named "Xeon," and I can reach my test site for this program as either http://xeon/ConfigurationTest/WebForm1.aspx or as http://localhost/ConfigurationTest/ WebForm1.aspx. For my purposes, this is not ideal. Instead I use Environment.MachineName, which returns the same name no matter how you reach the site. Of course, if you have multiple web sites on your machine (multiple wwwroots), each reached using a different DNS name, perhaps using the SERVER_NAME would be better for you.

Once I have the machine name, I look for a configuration file, using the static Exists method on the File object. If the machine-specific file exists, I pass that filename into ConfigToDs(). Otherwise, I check for Default.Config and pass that filename to ConfigToDs(). The heart of ConfigToDs() is:

ds.ReadXml(ConfigurationFilename);
this.m_Context.Cache.Insert
(ConfigurationClass.c_CacheKeyName, ds,new CacheDependency
(ConfigurationFilename));

Once the DataSet is filled by reading the configuration file, I insert the DataSet into the cache. Here I use Insert rather than Add, because Add fails if the item already exists in the cache. I am interested in the object always being added into the cache, even overwriting an existing value. The last parameter passed to Insert is a new CacheDependency object, which causes the cache to expire whenever the named file changes.

Using the ConfigurationClass

When you need to use the ConfigurationClass in a web application, you need to place the DLL created in the ConfigurationLib project into the bin folder of the Web Application, and then if using Visual Studio .NET, add a reference to the web project. The ConfigurationTest project (available electronically) is set up to use the ConfigurationClass. The Page_Load() of WebForm1.aspx contains Listing Nine. When first run, the page should look like Figure 1. Subsequent runs (until the configuration file is modified or the cached DataSet is cleared for some other reason, or when the application is reset) will show false for "Hit file?" for each of the values.

Possible Enhancements

There are a number of possible enhancements you might make to this class. One thing I discovered in testing is that if you first have a Default.Config file available, and you add the correct machine-specific file to the folder, it will not be read unless you then touch Default.Config. In a production system, it might be convenient to have a method that would simply clear the DataSet from the cache so that the class discovers the new file.

In some situations, it might be better to have configuration files located in a folder other than the default folder. For such situations, it might be reasonable to create a Web.Config entry to point to the ConfigurationFolder. It might also be useful to have read/write access to the set of configuration options, though I would argue that for items that need convenient read/write access, you should place them in a database. The ideal items for this configuration class are things that don't change too often and that cannot reasonably be placed in a database, such as a connection string.

Finally, having typesafe getter methods might be nice. Given a GetBoolValue(), your client calling code would be relieved of the need to parse the string value to get a Boolean value.

DDJ



Listing One

<appSettings>
    <add key="ApplicationName" value="DDJ ASP.NET Configuration" />
    <add key="Facility" value="DDJ" />
    <add key="ShortAppName" value="Hospital List" />
    <add key="CnStr" 
value="Server=xeon;database=ConfigurationDb;User ID=User;Pwd=password" />
</appSettings>
Back to article


Listing Two
CREATE TABLE [dbo].[ConfigurationInfo] (
    [Key] [nvarchar] (50) NOT NULL ,
    [Value] [nvarchar] (255) NOT NULL 
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[ConfigurationInfo] WITH NOCHECK ADD 
    CONSTRAINT [PK_ConfigurationInfo] PRIMARY KEY  CLUSTERED 
    (
        [Key]
    )  ON [PRIMARY] 
GO
Back to article


Listing Three
SqlConnection cn=new 
SqlConnection("Server=xeon;database=ConfigurationDb;
                                      User ID=User;Pwd=password");
cn.Open();
try
{
    SqlCommand cmd=new SqlCommand("SELECT [key],[value] 
                                        FROM ConfigurationInfo",cn);
    SqlDataAdapter da=new SqlDataAdapter(cmd);
    DataSet ds=new DataSet();
    da.Fill(ds,"ConfigruationInfo");

    ds.WriteXml("c:\\work\\Default.Config");
}
finally
{
    cn.Close();
}
Back to article


Listing Four
<?xml version="1.0" standalone="yes"?>
<Configuration>
  <ConfigruationInfo>
    <key>Key</key>
    <value>Yet Another different Value from Default</value>
  </ConfigruationInfo>
  <ConfigruationInfo>
    <key>Key2</key>
    <value>Value2</value>
  </ConfigruationInfo>
</Configuration>
Back to article


Listing Five
using System;
using System.Data;
using System.IO;
using System.Web.Caching;
using System.Web;
using System.Xml;

namespace ConfigurationLib
{
    /// <summary>
    /// Summary description for Class1.
    /// </summary>
    public class ConfigurationClass
    {
        private System.Web.HttpContext m_Context;   
        private bool m_HitFile;
        private const string c_CacheKeyName="ConfigurationCache_DDJ";
        
        public bool HitFile
        {
            get { return this.m_HitFile; }
        }
        public ConfigurationClass(System.Web.HttpContext CurrentContext)
        {
            this.m_HitFile=true;
            this.m_Context=CurrentContext;
        }
        public string GetValue(string key)
        {
            string ret=string.Empty;
            string FilterExpression=string.Format("Key='{0}'",key);
            DataSet ds=this.GetConfigDataset();
            DataRow[] dr=ds.Tables[0].Select(FilterExpression);
            if ( dr.Length>0 )
            {
                ret=dr[0]["Value"].ToString();
            }
            return ret;
        }
        private DataSet ConfigToDs(string ConfigurationFilename)
        {
            DataSet ds=new DataSet();
            try
            {
                ds.ReadXml(ConfigurationFilename);
                this.m_Context.Cache.Insert(ConfigurationClass.c_CacheKeyName,
                    ds,new CacheDependency(ConfigurationFilename));
            }
            catch
            {
                // Ignore...
            }
            return ds;
        }
        private DataSet GetConfigDataset()
        {
            DataSet ds=null;
            this.m_HitFile=false;
            ds=(DataSet)m_Context.Cache[c_CacheKeyName];
            if ( (ds==null) )
            {
                this.m_HitFile=true;
                string MachineName=System.Environment.MachineName;
                string ConfigurationFileName=
                    m_Context.Server.MapPath(MachineName + ".Config");
                
                if ( File.Exists(ConfigurationFileName) )
                {
                    ds=ConfigToDs(ConfigurationFileName);
                }
                else
                {
                    ConfigurationFileName=
                              m_Context.Server.MapPath("Default.Config");
                    if ( File.Exists(ConfigurationFileName) )
                    {
                        ds=ConfigToDs(ConfigurationFileName);
                    }
                }
            }
           return ds;
        }
    }
}
Back to article


Listing Six
ds=(DataSet)m_Context.Cache[c_CacheKeyName];
if ( (ds==null) )
{ // Refill the cache... }
// Use the Cache element
Back to article


Listing Seven
// INCORRECT CODE!
if ( (m_Context.Cache[c_CacheKeyName]==null) )
{ // Refill the cache... }
ds=(DataSet)m_Context.Cache[c_CacheKeyName];
// Use the Cache element
Back to article


Listing Eight
string MachineName=System.Environment.MachineName;
string ConfigurationFileName=
    m_Context.Server.MapPath(MachineName + ".Config");
if ( File.Exists(ConfigurationFileName) )
{
    ds=ConfigToDs(ConfigurationFileName);
}
else
{
    ConfigurationFileName=m_Context.Server.MapPath("Default.Config");
    if ( File.Exists(ConfigurationFileName) )
    {
        ds=ConfigToDs(ConfigurationFileName);
    }
}
Back to article


Listing Nine
ConfigurationClass c=new ConfigurationClass(HttpContext.Current);
string Value=c.GetValue("Key");
Response.Write("Key=" + Value + " Hit file? " + c.HitFile.ToString());
Value=c.GetValue("Key2");
Response.Write("<br>Key2=" + Value + " Hit file? " + c.HitFile.ToString());
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.