WINDOWS/.NET DEVELOPER
ASP.NET Configuration
Machine-specific configuration files are a start
DOUGLAS REILLY
When it was first introduced,
a nice ASP.NET feature was the standard way for configuring
applications. In all application - and certainly ASP.NET ones - you
will alway 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 conflgured 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 boot 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.Conftg 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 setting 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 change
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 Classtc ASP there are still some problems. One solution
I've found that resolves all the problems just mentioned is to use
machine-specific configuratton files that can be mapped into a
DataSet as required, then cached to avoid constantly hitting the
filesystem.
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 machine 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 : http://www.ddj.com) contain 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 value 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 scenario 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 thread simultaneouly
filling the cache. In this example, no harm is really done if two
threads are filling the cache at the same rime, 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 l 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 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.
WebForm1
Key=Another different Value Hit file? True
Key2=Value2 Hit file? False
Figure 1: Running the program
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>
Listing Two
CREATE TABLE [dbo] . [ConfigurationInfo] {
[Key] [nvarchar] (5Ø) 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
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):
Data.Set ds=new DataSet () ;
da.Fill (ds, "ConfigruationInfo") :
ds . WriteXml("c:\\work\\Default.Config");
}
finally
{
cn.Close():
}
Listing Four
<?xml version="1.Ø" standalone="yes"?>
<Configuration>
<Configruationlnfo>
<key>Key</key>
<value>Yet Another different Value from
Default</value>
</ConfigruationInfo>
<Configruationinfo>
<key>Key2</key>
<value>Value2</value>
</ConfigruationInfo>
</Configuration>
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='[Ø]'", key);
DataSet
ds=this.GetConfigDataset();
DataRow[]
dr=ds.Tables[Ø] .Select(FilterExpression);
if (
dr.Length>Ø )
{
ret=dr[Ø]["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 + " .Confign") ;
if
( File.Exists(ConfigurationFileName)
{
ds=ConfigToDs(ConfigurationFileName);
}
else
{
ConfigurationFileName=
m.Context.Server.MapPath("Default.Config");
if ( File.Exists (ConfigurationFileName) )
{
ds=ConfigToDs(ConfigurationFileName);
}
}
}
return ds;
}
}
}
Listing Six
ds= (DataSet)m_Context.Cache [c_CacheKeyName];
if ( (ds==null) )
( // Refill the cache... )
// Use the Cache element
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
Listing Eight
string MachineName=System.Environment.MachineName;
string Conhgurat1onFileName=
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);
}
}
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() );
Douglas is the author of Designing Microsoft ASP.NET
Applications and owner of Access Microsystems. Doug can be reached
at [email protected].
Dr Dobb's Journal #374, July 2005, Pg Pg 74-77