Fervent Coder

Coding Towards Utopia...by Rob Reynolds
posts - 278 , comments - 431 , trackbacks - 0

My Links

News


Rob Reynolds

Subscribe to Fervent Coder RSS
Subscribe to Fervent Coder by Email

About Me

I manage several open source projects. Need...
   ...package management for Windows?
   ...automated builds?
   ...database change management (migrations)?
   ...your application to check email?
   ...a monitoring utility?

I also write for



Like what you are reading? Want to buy me a cup of coffee?
PayPal - The safer, easier way to pay online!

Archives

Post Categories

Image Galleries

Sites

.NET Windows Services: How to Receive Configuration Updates without Restarting the Service

Something I have been working on recently is a utility that will watch the configuration file and let me know when it changes.  It is a work in progress (I am always tweaking  code) even though it works now and I am always open for feedback.  I was heavily influenced by looking at how log4net implemented the same thing with the XMLConfigurator (although they used a custom way of getting the file).

Basics of How to Receive Configuration Updates Without Restarting a Service

To start setting up a configuration watcher, we first need to set up an instance of System.IO.FileSystemWatcher.  This allows us to look at any file changes that happen to the configuration that we care about.

private FileSystemWatcher _fileWatcher;
 
internal void InitializeWatcher(FileInfo configurationFile, IList<string> configurationSections)
{
      _fileWatcher = new FileSystemWatcher
                       {
                           Path = _configurationFile.DirectoryName,
                           Filter = _configurationFile.Name,
                           NotifyFilter = (NotifyFilters.CreationTime | NotifyFilters.LastWrite |
                                           NotifyFilters.FileName)
                       };
 
   
    _fileWatcher.Changed += OnChange;
    _fileWatcher.EnableRaisingEvents = true;
}

Once we are watching a file using the FileSystemWatcher, we subscribe to the FileWatcher.Changed event as we have above.

Next we want to call ConfigurationManager.RefreshSection(sectionName).  This allows us to actually dispose of the cache in the configuration sections. That means the next time they are called they will have the new values.

private void OnChange(object source, FileSystemEventArgs e)
{
    foreach (string section in _configurationSections)
    {
        try
        {
            ConfigurationManager.RefreshSection(section);
        }
        catch (Exception ex)
        {
            _logger.ErrorFormat("There was an error setting {0} section in the configuration file. The error: {1}{2}", section,Environment.NewLine,ex.ToString());
        }
    }
}
 

Now we need to raise an event (that the configuration has been refreshed) for the application/service to subscribe to so that it can do it's processing.

private void AfterFileSyncDelay(object state)
{
    InvokeConfigurationChanged(_eventArgs);
}
 
public event ConfigurationChangedEventHandler ConfigurationUpdated;
 
private void InvokeConfigurationChanged(FileSystemEventArgs e)
{
    ConfigurationChangedEventHandler configurationChangedHandler = ConfigurationUpdated;
    if (configurationChangedHandler != null) configurationChangedHandler(this, e);
}

Now we need to use the configuration watcher and subscribe to updates. This is in the service itself.

Private WithEvents _configWatcher As IConfigurationWatcher
 
Private Sub InitializeConfigurationWatcher()
    Dim configSections As IList(Of String) = New List(Of String)
    configSections.Add("appSettings")
    configSections.Add("castle")
    _configWatcher = New ConfigurationWatcher(configSections)
End Sub

Then we do things when the configuration watcher tells us the code has changed.

Private Sub ConfigWatcherConfigurationUpdated(ByVal sender As Object, ByVal e As FileSystemEventArgs) _
        Handles _configWatcher.ConfigurationUpdated
    ' do things here settings
End Sub

That is pretty much all that needs to be completed to be able to subscribe to and receive notifications of when a config file changes.

The Configuration Watcher

Below is the entire class.  Notice how it decides to use a configuration file if one is not passed into the constructor.

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Threading;
using log4net;
 
    
    public sealed class ConfigurationWatcher : IConfigurationWatcher
    {
        private FileInfo _configurationFile;
        private IList<string> _configurationSections;
        private FileSystemWatcher _fileWatcher;
        private Timer _fileSyncDelay;
        private FileSystemEventArgs _eventArgs;
        private const int FILE_SYNC_DELAY = 5000; //5 seconds
        private readonly ILog _logger = LogManager.GetLogger(typeof(ConfigurationWatcher));
 
        internal void InitializeWatcher(FileInfo configurationFile, IList<string> configurationSections)
        {
            _configurationFile = configurationFile;
            if (_configurationFile == null)
            {
                string file = Assembly.GetEntryAssembly().Location + ".config";
                _configurationFile = new FileInfo(file);
                _logger.InfoFormat("Setting configuration file to a default of {0}. Exists? {1}", file,_configurationFile.Exists.ToString());
            }
            _configurationSections = configurationSections;
            if (_configurationSections == null || _configurationSections.Count == 0)
            {
                _configurationSections = new List<string> { "appSettings" };
                _logger.DebugFormat(@"Setting area to refresh to a default of ""appSettings."" ");
            }
 
            _logger.DebugFormat("Creating new System.IO.FileSystemWatcher to watch the configuration file and subscribing to the Changed event.");
            _fileWatcher = new FileSystemWatcher
                               {
                                   Path = _configurationFile.DirectoryName,
                                   Filter = _configurationFile.Name,
                                   NotifyFilter = (NotifyFilters.CreationTime | NotifyFilters.LastWrite |
                                                   NotifyFilters.FileName)
                               };
 
           
            _fileWatcher.Changed += OnChange;
            _fileWatcher.EnableRaisingEvents = true;
            _fileSyncDelay = new Timer(AfterFileSyncDelay, null, -1, -1);
        }
 
        public ConfigurationWatcher()
        {
            InitializeWatcher(null, null);
        }
        
        public ConfigurationWatcher(FileInfo configurationFile)
        {
            InitializeWatcher(configurationFile, null);
        }
 
        public ConfigurationWatcher(IList<string> configurationSections)
        {
            InitializeWatcher(null, configurationSections);
        }
 
        public ConfigurationWatcher(FileInfo configurationFile, IList<string> configurationSections)
        {
            InitializeWatcher(configurationFile, configurationSections);
        }
 
        private void AfterFileSyncDelay(object state)
        {
            InvokeConfigurationChanged(_eventArgs);
        }
 
        private void OnChange(object source, FileSystemEventArgs e)
        {
            foreach (string section in _configurationSections)
            {
                try
                {
                    ConfigurationManager.RefreshSection(section);
                }
                catch (Exception ex)
                {
                    _logger.ErrorFormat("There was an error setting {0} section in the configuration file. The error: {1}{2}", section,Environment.NewLine,ex.ToString());
                }
               
            }
 
            _eventArgs = e;
            _fileSyncDelay.Change(FILE_SYNC_DELAY, -1);
        }
 
        public event ConfigurationChangedEventHandler ConfigurationUpdated;
 
        private void InvokeConfigurationChanged(FileSystemEventArgs e)
        {
            _logger.DebugFormat("Raising ConfigurationUpdated event for {0}", e.Name);
            ConfigurationChangedEventHandler configurationChangedHandler = ConfigurationUpdated;
            if (configurationChangedHandler != null) configurationChangedHandler(this, e);
        }
 
    }

Using The Configuration Watcher

When using the configuration watcher, when you change the config file, it will refresh the sections that you passed into the constructor to refresh. You can actually dispose your inversion of control container and rebuild it after receiving notification of changes to your config file.  Let's look at the service entry point below.

When I start the service, I initialize the configuration watcher and the Windsor container.  If I am running the service as a console ("/console") then it holds the service running for me with a Console.ReadKey().

Imports System
Imports Fervent.Tools.SystemConfiguration
Imports System.IO
Imports System.Collections.Generic
Imports log4net.Config
Imports Castle.Windsor.Configuration.Interpreters
Imports System.ServiceProcess
Imports System.ServiceModel
Imports Castle.Windsor
Imports log4net
 
<Assembly: XmlConfigurator(Watch:=True)> 
 
Public Class ProjectService
    Inherits ServiceBase
 
    Private ReadOnly _logger As ILog = LogManager.GetLogger(GetType(ProjectService))
    Private WithEvents _configWatcher As IConfigurationWatcher
    Private _windsorContainer As IWindsorContainer
 
    Protected Overrides Sub OnStart(ByVal args() As String)
        _logger.Info("Starting service...")
        Try
            InitializeConfigurationWatcher()
            InitializeIOC()
 
            ' do something
 
            If ((args.Length > 0) AndAlso (Not Array.IndexOf(args, "/console") = -1)) Then
                _logger.Info("Running service as a console.")
                Console.WriteLine("Press any key to continue")
                Console.ReadKey(True)
            End If
        Catch ex As Exception
            _logger.ErrorFormat("Service had an error on {0} (with user {1}):{3}{4}", Environment.MachineName, Environment.UserName, Environment.NewLine, ex.ToString())
            Throw
        End Try
    End Sub
 
    Private Sub InitializeConfigurationWatcher()
        Dim configSections As IList(Of String) = New List(Of String)
        configSections.Add("appSettings")
        configSections.Add("castle")
        _configWatcher = New ConfigurationWatcher(configSections)
    End Sub
 
    Public Sub InitializeIOC()
        _logger.Debug("Attempting to initialize IOC container.")
        _windsorContainer = New WindsorContainer(New XmlInterpreter())
    End Sub
 
    Protected Overrides Sub OnStop()
        _logger.Info("Ending service...")
        DisposeIOC()
    End Sub
 
    Public Sub DisposeIOC()
        _logger.Debug("Disposing the IOC container.")
        _windsorContainer.Dispose()
    End Sub
 
    Public Sub RunConsole(ByVal args As String())
        OnStart(args)
        OnStop()
    End Sub
 
    Private Sub ConfigWatcherConfigurationUpdated(ByVal sender As Object, ByVal e As FileSystemEventArgs) Handles _configWatcher.ConfigurationUpdated
        _logger.Info("Configuration has been updated and all configuration items have been refreshed. Attempting to dispose and refresh the IOC container.")
        DisposeIOC()
        InitializeIOC()
        ' other things to do to refresh other configuration settings
    End Sub
 
End Class

When we initialize the watcher, we tell it the configuration sections we want to refresh when a change is detected on the configuration. You can see when we get a notification that the configuration changes, we have the code dispose the container and rebuild it. Very cool stuff.

Notes: My only complaint with the Configuration Refresh is that I can't get it to work with the debugger or unit testing.  I imagine that means something will not let me dump the cache of the configuration file.  It works once you start running the application.  I would like to test that this actually works and I have tests set up to test this as if it actually did work, but I am not getting back changes at all unless actually running the application.  Any feedback would be nice.  I also find that you cannot refresh the configuration base element ("configuration") and have all the sections refreshed.

Update: Here is the code with a sample console.

 

Print | posted on Friday, September 5, 2008 1:06 AM | Filed Under [ Code ]

Feedback

Gravatar

# re: .NET Services: How to Receive Configuration Updates without Restarting the Service

I purposely showed the service itself as VB.NET to separate it from the ConfigurationWatcher
9/5/2008 1:15 AM | Robz
Gravatar

# re: .NET Windows Services: How to Receive Configuration Updates without Restarting the Service

I finally figured out what my problem was with not being able to test it with MbUnit. It creates a copy of your config file as config.temp and you have to call it instead. It doesn't work the same way when you call MbUnit on the command line during a release build tests.
9/26/2008 12:13 AM | Robz
Comments have been closed on this topic.

Powered by: