Thursday, June 14, 2007

Dynamic.web.config

I don't know about you, but I absolutely hate it when I change something in my web.config and the application restarts. Maybe I just misspelled something in the <appSettings> or changed some custom cache configuration. The app should be able to survive some configuration settings changes without resetting. So I built my own ConfigurationSettings-like class and named it DynamicConfigurationSettings. It reads a web.config-like file (which I call dynamic.web.config, but could be anything) and whenever the file changes, its values are automatically reloaded (courtesy of FileSystemWatcher and Bjarne Lindberg's FileWaiter). FileWaiter was necessary because FileSystemWatcher raises multiple events when a file is changed, a problem described in this thread. DynamicConfigurationSettings is meant to be used as a drop-in replacement for ConfigurationSettings. It was written for 1.1 but it should be fairly easy to port it to 2.0. Like I said, dynamic.web.config has a schema similar to that of a basic web.config:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="configuration">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="configSections">
          <xs:complexType>
            <xs:sequence>
              <xs:element maxOccurs="unbounded" name="section">
                <xs:complexType>
                  <xs:attribute name="name" type="xs:string" use="required" />
                  <xs:attribute name="type" type="xs:string" use="required" />
                </xs:complexType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
        <xs:element name="appSettings">
          <xs:complexType>
            <xs:sequence>
              <xs:element maxOccurs="unbounded" name="add">
                <xs:complexType>
                  <xs:attribute name="key" type="xs:string" use="required" />
                  <xs:attribute name="value" type="xs:string" use="required" />
                </xs:complexType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

(extracted with Visual Studio). Basically, it supports <appSettings> and <configSections>.
Example:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <configSections>
  <section name="customSection" type="MyApp.CustomSectionHandler, MyAssembly"/>
 </configSections>
  <appSettings>
  <add key="MaxResults" value="300"/>
  </appSettings>
 <customSection>
  <default>hello</default>
  <rules>
   <rule source="something" destination="something else"/>
  </rules>
 </customSection>
</configuration>

To use it, just call DynamicConfigurationSettings.load(@"path\to\dynamic.web.config") in your Application_Start and that's it.

Additionally, whenever it can't find some requested config item, it tries to fetch it from ConfigurationSettings. So, if your code uses DynamicConfigurationSettings, you can override your web.config settings with your dynamic.web.config settings. Example: suppose you have defined MaxResults as 200 in <appSettings> in web.config, and you have the following code:

DynamicConfigurationSettings.load("dynamic.web.config");
int maxResults = Convert.ToInt32(DynamicConfigurationSettings.AppSettings["MaxResults"]);
You'd get 300, as it's defined in dynamic.web.config. BUT, if we remove the definition from dynamic.web.config, you get 200, as defined in web.config.
Before going to the code, a piece of advice/disclaimer:

DON'T use this to store critical data! I only use it to store cache sizes and other non-critical stuff. I tried hard to make it as thread-safe and fault-tolerant as possible, but be aware that it's very dangerous to change configuration at runtime. If you edit you dynamic.web.config and save it while it's not well-formed, and your application crashes, it's not my fault (it shouldn't happen, but still).
I recommend that you wrap every access to a strongly-typed value in DynamicConfigurationSettings (like the maxResults example above) with try..catch and provide sensible defaults, so you won't crash everything if you write something like <add key="MaxResults" value="Eoo"/>

You have been warned. Now here's the code, go have fun :-)
As usual, comments and bugfixes are very welcome.

FileWaiter.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace Utils
{
 /// <summary>
 /// Waits for a file to close
 /// From Bjarne Lindberg (http://www.thescripts.com/forum/post1453548-8.html)
 /// </summary>
 public class FileWaiter
 {
  public string fileName;

  public FileWaiter(string fileName) {
   this.fileName = fileName;
  }

  public void DoWait(int timeout) {
   DoWait(new TimeSpan(0, 0, 0, 0, timeout));
  }

  public void DoWait() {
   DoWait(TimeSpan.MinValue);
  }

  public void DoWait(TimeSpan timeout) {
   Debug.Assert(fileName != null);
   Thread t = new Thread(new ThreadStart(ThreadWait));
   t.Start();
   if (timeout > TimeSpan.MinValue)
    t.Join(timeout);
   else
    t.Join();
  }

  private void ThreadWait() {
   do {
    //Start waiting..
    Thread.Sleep(500);
   } while (File.Exists(fileName) && isFileOpen(fileName));
  }

  protected static bool isFileOpen(string fileName) {
   FileStream s = null;
   try {
    s = File.Open(fileName, FileMode.Open,
                  FileAccess.ReadWrite,
                  FileShare.None);
    return false;
   }
   catch (IOException) {
    return true;
   }
   finally {
    if (s != null)
     s.Close();
   }
  }
 }
}

DynamicConfigurationSettings.cs
using System;
using System.Collections;
using System.Configuration;
using System.IO;
using System.Xml;
using System.Collections.Specialized;
using log4net;

namespace Utils
{
 /// <summary>
 /// Similar to <see cref="System.Configuration.ConfigurationSettings"/>, but accepts changes in configuration file at runtime without restarting the application
 /// </summary>
 public class DynamicConfigurationSettings
 {
  private static readonly ILog log = LogManager.GetLogger(typeof(DynamicConfigurationSettings));
  
  /// <summary>
  /// Gets configuration settings in the configuration section.
  /// </summary>
  public static NameValueCollection AppSettings {
   get {
    lock (padLock) {
     if (_appSettings != null)
      return _appSettings;
     return ConfigurationSettings.AppSettings;     
    }
   }
  }
  
  private static Hashtable configSectionsCache = new Hashtable();

  /// <summary>
  /// Returns configuration settings for a user-defined configuration section.
  /// </summary>
  /// <param name="sectionName">The configuration section to read.</param>
  /// <returns>The configuration settings for sectionName.</returns>
  public static object GetConfig(string sectionName) {
   if (configSectionsCache[sectionName] == null)
    try {
     SectionConfig config = (SectionConfig) sections[sectionName];
     IConfigurationSectionHandler sectionHandler = (IConfigurationSectionHandler) Activator.CreateInstance(Type.GetType(config.handlerName));
     configSectionsCache[sectionName] = sectionHandler.Create(null, null, config.node);
    } catch (Exception) {
     configSectionsCache[sectionName] = ConfigurationSettings.GetConfig(sectionName);
    }
   return configSectionsCache[sectionName];
  }

  /// <summary>
  /// Loads configuration from xml document
  /// </summary>
  /// <param name="config">Document to load</param>
  public static void load(XmlDocument config) {
   XmlNodeList configSections = config.SelectNodes("/configuration/configSections/section");
   
   lock (padLock) {
    sections = new Hashtable();
    
    foreach (XmlNode section in configSections) {
     SectionConfig sc = new SectionConfig();
     string sectionName = section.Attributes["name"].InnerText;
     sc.node = config.SelectSingleNode("/configuration/" + sectionName);
     sc.handlerName = section.Attributes["type"].InnerText;
     sections[sectionName] = sc;
    }
   
    XmlNode nodeAppSettings = config.SelectSingleNode("/configuration/appSettings");
    NameValueSectionHandler nvsh = new NameValueSectionHandler();   
    NameValueCollection nvc = (NameValueCollection) nvsh.Create(null, null, nodeAppSettings);
    _appSettings = new NameValueCollection();
    _appSettings.Add(ConfigurationSettings.AppSettings);   
    foreach (string k in nvc.Keys)
     _appSettings[k] = nvc[k];    
    configSectionsCache = new Hashtable();
   }

  }
  
  /// <summary>
  /// Loads configuration from xml file and reloads when it changes
  /// </summary>
  /// <param name="filename"></param>
  public static void load(string filename) {
   _filename = filename;
   XmlDocument config = new XmlDocument();
   try {
    config.Load(filename);
    load(config);
    if (fsw != null) {
     fsw.Dispose();
    }
    fsw = new FileSystemWatcher(Path.GetDirectoryName(filename));
    fsw.Changed += new FileSystemEventHandler(fsw_Changed);
    fsw.EnableRaisingEvents = true;       
   } catch (Exception e) {
    log.ErrorFormat("Error loading dynamic.web.config: {0}\n{1}", filename, e.ToString());
   }
  }
  
  /// <summary>
  /// Loads configuration from xml stream
  /// </summary>
  /// <param name="s"></param>
  public static void load(Stream s) {
   XmlDocument config = new XmlDocument();
   StreamReader sr = new StreamReader(s);
   try {
    config.LoadXml(sr.ReadToEnd());
    load(config);
   } catch (Exception e) {
    log.Error("Error loading dynamic.web.config:", e);
   } finally {
    sr.Close();
   }
  }
  
  /// <summary>
  /// Loads configuration from xml string
  /// </summary>
  /// <param name="s"></param>
  public static void loadString(string s) {
   XmlDocument config = new XmlDocument();
   try {
    config.LoadXml(s);
    load(config);          
   } catch (Exception e) {
    log.Error("Error loading dynamic.web.config:", e);
   }
  }

  /// <summary>
  /// Handles changes in config file
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private static void fsw_Changed(object sender, FileSystemEventArgs e) {
   if (e.Name.ToLower() == Path.GetFileName(_filename.ToLower()) && e.ChangeType == WatcherChangeTypes.Changed) {
    FileWaiter fw = new FileWaiter(e.FullPath);
    fw.DoWait();
    load(e.FullPath);
   }
  }

  private static NameValueCollection _appSettings;  
  private static FileSystemWatcher fsw;
  private static string _filename;
  private static Hashtable sections;
  private static object padLock = new object();
  
  private class SectionConfig {
   public string handlerName;
   public XmlNode node;
  }

 }
}

1 comment:

CK said...

Good article! You've been DZoned. :-)