Thursday, April 19, 2012

C#: Watching an app.config file for changes (and responding)

Hello World,

We had this windows service that had configuration in it's app.config that governed it's behaviour. A requirement from this service was that it should watch the config file and respond to changes made without having to be re-started (a restart would re-load the app domain and re-read the config anyways)
Of several possible solutions, my approach was to watch the file for changes using .NET's FileSystemWatcher class and then consume it's OnChange event to refresh/reload config values.

Draft 1: A prototype console application that implements the above solution


   1:  class Program
   2:      {
   3:          private static bool _stopService = false;
   4:   
   5:          static void Main(string[] args)
   6:          {
   7:              Console.WriteLine("Starting Worker Process");
   8:              Thread myWorkerThread = new Thread(Run) { Name = "Run Worker Thread" };
   9:              myWorkerThread.Start();
  10:              Console.WriteLine("Press any key to stop...");
  11:              Console.ReadLine();
  12:              _stopService = true;
  13:          }
  14:   
  15:          static void Run()
  16:          {
  17:              string assemblyDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
  18:              NotifyFilters notifyFilters = NotifyFilters.LastWrite; 
  19:   
  20:              FileSystemWatcher fileSystemWatcher = new FileSystemWatcher() { 
										Path = assemblyDirectory, 
										NotifyFilter = notifyFilters, 
										Filter = "*.config" };
  21:              fileSystemWatcher.Changed += OnChanged;
  22:              fileSystemWatcher.EnableRaisingEvents = true;
  23:   
  24:              Console.WriteLine("Watching for changes...");
  25:              while (!_stopService)
  26:              {
  27:                  Thread.Sleep(5 * 1000);
  28:              }
  29:          }        
  30:   
  31:          static void OnChanged(object source, FileSystemEventArgs e)
  32:          {
  33:              Console.WriteLine("Change event handler invoked...");
  34:              Console.WriteLine("Value before refresh: {0}", ConfigurationManager.AppSettings["myKey"]);
  35:              ConfigurationManager.RefreshSection("appSettings");
  36:              Console.WriteLine("Value after change: {0}", ConfigurationManager.AppSettings["myKey"]);
  37:          }
  38:      }
 
 
The application config file looks like this:
   1:  <xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:    <appSettings>
   4:      <add key="myKey" value="testValue"/>
   5:    <appSettings>
   6:  <configuration>


Notes on the implementation:

  1. By launching the FileSystemWatcher on a worker thread, I am attempting to simulate the scenario in the actual service, so ignore those bits for now
  2. Line number 22 flags the FileSystemWatcher to raise events, needless to say, if you miss setting this flag, the events would never be raised
  3. You can get details of what has triggered the event in terms of full path of the object that was watched, change type (viz. changed, deleted, created etc.) through the FileSystemEventArgs
Issues:
When run, if you modify the app.config in your program's output directory and save it, you'll see the following output:





Did you see? The OnChanged event has been raised twice for a single change. I began researching this and found that this is a known behaviour (by design) as documented stackoverflow and here
Now, the resolution (at least the one I got to work):
Draft 2: In the OnChanged event, use a try finally block to set EnableRaisingEvents to false (in the try) and then set it back to true in the finally block:

   1:  class Program
   2:      {
   3:          private static bool _stopService = false;
   4:          private static FileSystemWatcher _fileSystemWatcher;
   5:   
   6:          static void Main(string[] args)
   7:          {
   8:              Console.WriteLine("Starting Worker Process");
   9:              Thread myWorkerThread = new Thread(Run) { Name = "Run Worker Thread" };
  10:              myWorkerThread.Start();
  11:              Console.WriteLine("Press any key to stop...");
  12:              Console.ReadLine();
  13:              _stopService = true;
  14:          }
  15:   
  16:          static void Run()
  17:          {
  18:              string assemblyDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
  19:              NotifyFilters notifyFilters = NotifyFilters.LastWrite; 
  20:   
  21:              _fileSystemWatcher = new FileSystemWatcher() { 
			Path = assemblyDirectory, 
			NotifyFilter = notifyFilters, 
			Filter = "*.config" };
  22:              _fileSystemWatcher.Changed += OnChanged;
  23:              _fileSystemWatcher.EnableRaisingEvents = true;
  24:   
  25:              Console.WriteLine("Watching for changes...");
  26:              while (!_stopService)
  27:              {
  28:                  Thread.Sleep(5 * 1000);
  29:              }
  30:          }        
  31:   
  32:          static void OnChanged(object source, FileSystemEventArgs e)
  33:          {
  34:              try
  35:              {
  36:                  _fileSystemWatcher.EnableRaisingEvents = false;
  37:                  Console.WriteLine("Change event handler invoked...");
  38:                  Console.WriteLine("Value before refresh: {0}", ConfigurationManager.AppSettings["myKey"]);
  39:                  ConfigurationManager.RefreshSection("appSettings");
  40:                  Console.WriteLine("Value after change: {0}", ConfigurationManager.AppSettings["myKey"]);
  41:              }
  42:              finally
  43:              {
  44:                  _fileSystemWatcher.EnableRaisingEvents = true;
  45:              }
  46:          }
  47:      }

So now the output looks like this:



Great! Another day saved!

Happy Coding!

PS: The code formatting above is powered by Manoli.net