Introduction
ASP.NET web applications that follow the classic configuration settings management technique used nowadays store configuration values in the web.config file. This is an XML configuration file, containing various settings: some of them are targeted to the ASP.NET engine itself (for example: globalization settings, authentication settings, compilation settings); other settings are targeted to the application, and are known as "AppSettings" (from the name of the XML node containing them inside the web.config file). The adoption of a configuration settings management strategy based on web.config is suitable for single web applications, but it quickly becomes unmanageable if you need to maintain the configuration settings for a family of similar web applications, especially when you need to keep aligned some common settings on more and more web.config files. Of course, if the same machine hosts multiple web sites with similar application settings, you always have the chance to store the common settings in the machine.config file, that acts as a "master" configuration file for all the web applications hosted on that machine. But also this approach quickly becomes unusable if your deployment scenario is built on a web farm, with multiple web servers.
I worked for a single project that had lots of web applications, targeted to multiple languages, hosted on multiple, load-balanced web servers, on multiple environments. The number of web applications (and of corresponding web.config files) to be managed was about 105, so I decided to think of a different way to manage those thousands of settings.
This article describes the configuration management solution (alternative to the classic web.config file usage) I adopted. It addresses only the storage of the AppSettings
: all the other configurations, targeted to the web engine (like: globalization, compilation, authentication), are still stored in web.config.
Configuration settings storage
I decided to store all the settings on a Microsoft SQL Server 2000 database table, named WebSettings, in a database of your choice, that from now on I'll call CMS (Configuration Management System). The WebSettings table structure contains three main fields:
WebSite
, a string indicating the web site that is reading a particular configuration setting (a.k.a.: web site identifier).
KeyName
, a string containing the name of the configuration setting (something similar to the value of the original key attribute in an add
node of the web.config's appSettings
).
KeyValue
, a string containing the value of the configuration setting (something similar to the value of the original value
attribute in an add
node of the web.config's appSettings
)
An optional KeyDescr
field can contain a description of the configuration key, specifying the allowed values or some setting guidelines. A primary key on the WebSite
and KeyName
fields will guarantee the uniqueness of each setting.
CREATE TABLE WebSettings (
WebSite varchar(64) NOT NULL ,
KeyName varchar(64) NOT NULL ,
KeyValue varchar(1024) NOT NULL ,
KeyDescr text NULL )
GO
ALTER TABLE WebSettings WITH NOCHECK ADD
CONSTRAINT PK_WebSettings PRIMARY KEY NONCLUSTERED
( WebSite, KeyName )
GO
Each KeyName
/KeyValue
pair in the WebSettings table must be associated with a particular WebSite
. When a particular web site needs a configuration value, a call is given to the function GetWebSetting()
- see the code below - and the correct KeyValue
is retrieved based on the KeyName
supplied and on the requesting web application identity. The GetWebSetting()
function definitely implements a functionality similar to the .NET Framework collection ConfigurationSettings.AppSettings()
in the System.Configuration
namespace. The difference between the two is that GetWebsetting()
always returns a String
value (eventually an empty string if the desired configuration setting value is not found in the WebSettings table), and never the Nothing
value.
The GetWebSetting()
function determines the requesting web application identity by inspecting the value of the WebSiteIdentifier
key in the web.config associated to the current HTTP context. The WebSiteIdentifier
is one of the only two AppSettings
keys needed in the web.config file of the calling web application: it permits the identification of the web site itself when querying the WebSettings table. The second AppSettings
key is named WebSettingsConnString
and contains the connection string needed to connect to the CMS database hosting the WebSettings table:
<appSettings>
<add key=
"WebSiteIdentifier" value="MySite.Europe.English" />
<add key=
"WebSettingsConnString" value="server=...;database=cms;uid=...;pwd=..." />
</appSettings>
Hierarchical configuration
The WebSiteIdentifier
key is a dot-separated string, containing more substrings identifying classes (or sets) of similar applications. In my specific context, the WebSiteIdentifier
was composed of three parts: WebSiteType.Area.Language
. When a configuration KeyName
/KeyValue
pair has to be set for a specific web site, the corresponding WebSite
field in the WebSettings table will be filled with the exact three-part web site identifier of the related web application. For configuration KeyName
/KeyValue
pairs that are common to multiple, similar web applications, you can enter a single entry in the WebSettings table, indicating that it is applicable to a set of web applications; to do so, you will use the special word _DefaultSettings
(be sure to start this word with an "underscore" character) as a part of the three-part-dotted identifier. For example:
A WebSettings.WebSite field filled with... |
will indicate a KeyName/KeyValue pair... |
MySite.Europe.English |
only applicable to the single, specific English European "MySite" web site |
MySite.Europe._DefaultSettings |
applicable to all European "MySite" web sites (any language) |
MySite._DefaultSettings |
applicable to all "MySite" web sites (any language, any area) |
_DefaultSettings |
applicable to all web sites ("MySite" and others) |
The presence of a _DefaultSettings
setting doesn't exclude the possibility to have some specializations for a particular site. In fact, the GetWebSetting()
function always looks for a KeyName
/KeyValue
pair starting from the most specific WebSiteIdentifier
it finds in the WebSettings.WebSite
field. So, for example, the search matching order for my specific hierarchy was as follows:
WebSiteType.Area.Language
WebSiteType.Area._DefaultSettings
WebSiteType._DefaultSettings
_DefaultSettings
Caching
When the GetWebSetting
function is called for the first time, it reads from the CMS database (pointed by the WebSettingsConnString
AppSettings
key) the settings related to the requesting application. These values are then cached in the ASP.NET Cache
object, in order to minimize the database access during subsequent calls to the GetWebSetting()
function. These values remain in the web server cache until one of the following events occurs:
- The Application Domain related to the web application is restarted (this happens, for example, if: the web.config file of the application is modified; the bin directory of the application is modified; the Application Pool hosting the application is recycled)
- The ASP.NET engine flushes the cache to gain some memory for other tasks
Every time the GetWebSetting()
function is called, it checks for configuration values in the cache, if they are present, they are used as they are; otherwise, they are re-read from the database.
If the GetWebSetting()
function is called specifying True
as a second (optional) parameter, the configuration value is anyway read from the database (and also all the other settings for that web application are reloaded).
If you change some values in the WebSettings table, in order to make them take effect, you have to force a cache flushing so that the GetWebSetting()
function re-loads values from the database. You don't need to restart the Application Domain or recycle the Application Pool: to force the configuration settings reload, you can just do a call like the following:
GetWebSetting("", True)
You could implement a special administrative ASPX web page, that - when called - flushes the web settings cache simply invoking the GetWebSetting()
function with the second parameter set to True
. Be sure to call it on all the web servers involved in the change (don't forget that the ASP.NET Cache
object lives in the memory of each server in the farm).
The code
Imports System.Configuration
Public Function GetWebSetting(ByVal Key As String, _
Optional ByVal ForceCacheRefresh As Boolean = False) As String
Dim dtWebSettings As DataTable
If ForceCacheRefresh Then
HttpContext.Current.Cache.Remove("WebSettingsCache")
End If
If HttpContext.Current.Cache("WebSettingsCache") Is Nothing Then
dtWebSettings =
New DataTable
Dim connStr As String =
ConfigurationSettings.AppSettings("WebSettingsConnString") & ""
If connStr = "" Then
Throw New Exception("WebSettingsConnString key missing in web.config")
Exit Function
End If
Dim siteIdentifier As String =
ConfigurationSettings.AppSettings("WebSiteIdentifier") & ""
If siteIdentifier = "" Then
Throw New Exception("WebSiteIdentifier key missing in web.config")
Exit Function
End If
Dim cnn As New SqlConnection(connStr)
Dim cmd As SqlCommand = cnn.CreateCommand()
Dim SQLstr As String
Dim WebSitePart As String = siteIdentifier
SQLstr = "SELECT KeyName, KeyValue FROM WebSettings" &_
" WHERE WebSite='" & siteIdentifier & "'"
Do While WebSitePart.IndexOf(".") >= 0
WebSitePart = WebSitePart.Substring(0, WebSitePart.LastIndexOf("."))
SQLstr & = " OR WebSite='" & WebSitePart & "._DefaultSettings'"
Loop
SQLstr & = " OR WebSite='_DefaultSettings' ORDER BY WebSite DESC"
cmd.CommandText = SQLstr
Dim da As New SqlDataAdapter(cmd)
da.Fill(dtWebSettings)
HttpContext.Current.Cache.Add("WebSettingsCache", _
dtWebSettings, Nothing, Date.MaxValue, TimeSpan.Zero, _
Caching.CacheItemPriority.High, Nothing)
Else
dtWebSettings =
CType(HttpContext.Current.Cache("WebSettingsCache"), DataTable)
End If
If Key <> "" Then
Dim drs As DataRow() =
dtWebSettings.Select("KeyName='" & Key & "'")
If drs.Length = 0 Then
Return ""
Else
Return drs(0)("KeyValue").ToString()
End If
Else
Return ""
End If
End Function