October 2016

Volume 31 Number 10

[Windows Service]

Create a Customizable FileSystemWatcher Windows Service

By Diego Ordonez

The FileSystemWatcher class is a very powerful tool that’s been a part of the Microsoft .NET Framework since version 1.1, and according to its official definition (bit.ly/2b8iOvQ), it “listens to the file system change notifications and raises events when a directory, or file in a directory, changes.”

This class is able to detect events in the file system, such as create, modify, or delete files and folders; it’s fully customizable; and its constructor accepts parameters like folder location and file exten­sion to listen for, and a Boolean parameter to specify whether the listening process should work recursively through the folder structure. However, including those parameters in your source code isn’t a good approach because they won’t help when the application needs to include new folders and file extensions, which, moreover, will require coding, building and redeployment. Unless you’re sure your application will hardly ever change those settings, a better idea is to implement a mechanism that can change the configuration without modifying the source code.

In this article I explore how to write an application that uses the FileSystemWatcher class just once, but then, via XML serialization, allows further modifications to the application’s settings, such as folder names, file extensions and actions to be executed upon raising an event. In this way, all the changes can be easily achieved simply by updating an XML file and restarting the Windows service.

For simplicity’s sake, I’m not going to explain the details about how to run this C# console application as a Windows service, but many resources are available online regarding this matter.

The Structure of Customized Folder Settings

Because I plan to deserialize the XML settings file into a well-structured C# class, the first component of the application must be the definition of the parameters FileSystemWatcher requires to operate. Figure 1 shows the code that defines that class.

Figure 1 Definition of the CustomFolderSettings Class

/// <summary>
/// This class defines an individual type of file and its associated
/// folder to be monitored by the File System Watcher
/// </summary>
public class CustomFolderSettings
  /// <summary>Unique identifier of the combination File type/folder.
  /// Arbitrary number (for instance 001, 002, and so on)</summary>
  public string FolderID { get; set; }
  /// <summary>If TRUE: the file type and folder will be monitored</summary>
  public bool FolderEnabled { get; set; }
  /// <summary>Description of the type of files and folder location –
  /// Just for documentation purpose</summary>
  public string FolderDescription { get; set; }
  /// <summary>Filter to select the type of files to be monitored.
  /// (Examples: *.shp, *.*, Project00*.zip)</summary>
  public string FolderFilter { get; set; }
  /// <summary>Full path to be monitored
  /// (i.e.: D:\files\projects\shapes\ )</summary>
  public string FolderPath { get; set; }
  /// <summary>If TRUE: the folder and its subfolders will be monitored</summary>
  public bool FolderIncludeSub { get; set; }
  /// <summary>Specifies the command or action to be executed
  /// after an event has raised</summary>
  public string ExecutableFile { get; set; }
  /// <summary>List of arguments to be passed to the executable file</summary>
  public string ExecutableArguments { get; set; }
  /// <summary>Default constructor of the class</summary>       
  public CustomFolderSettings()

Now let’s look at how an XML file can be translated into this C# class using the deserialization process. Please note that there won’t be one single instance of the class CustomFolderSettings; instead there will be a list (List<CustomFolderSettings>) allowing the Windows service to listen for many different folder locations and file extensions.

Figure 2 shows an example of an XML settings file from which I can provide the FileSystemWatcher with all the arguments it needs to work. It’s important to understand at this point that the information contained in the XML file (Figure 2) will feed the C# class (Figure 1).

Figure 2 Structure of the XML Settings File

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfCustomFolderSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <CustomFolderSettings FolderID="ExampleKML_files">
    <FolderDescription>Files in format KML corresponding to the example project
    <!-- The block {0} will be automatically replaced with the
      corresponding file name -->
    <ExecutableArguments>/C echo It works properly for .KML extension-- File {0}
      &gt; c:\temp\it_works_KML.txt</ExecutableArguments>
  <CustomFolderSettings FolderID="ExampleZIP_files">
    <FolderDescription>Files in format ZIP corresponding to the example project
    <!-- The block {0} will be automatically replaced with the
      corresponding file name -->
    <ExecutableArguments>/C echo It works properly for .ZIP extension -- File {0}
      &gt; c:\temp\it_works_ZIP.txt</ExecutableArguments>

Let’s take a closer look at the parameters contained in the XML file now. First, note that the XML root element is <ArrayOfCustomFolderSettings>, which allows as many elements <CustomFolderSettings> as required. This is the key to being able to concurrently monitor several folder locations and file extensions.

Second, notice that the parameter <FolderEnabled> is true for the first folder, but false for the second one. This is an easy way to disable one of the FileSystemWatchers without having to delete it from the XML file, meaning that even if the configuration is present, the class will omit it when it’s running.

Finally, it’s important to understand how to specify which action will be triggered upon detection of a file that has been created, deleted or modified, which is the final goal of the FileSystemWatcher class.

The parameter <ExecutableFile> contains the application that will be launched, in this example the DOS command line (CMD.EXE).

The parameter <ExecutableArguments> contains the options that will be passed to the executable as arguments. Here’s the example from Figure 2:

>/C echo It works properly for .ZIP extension -- File {0} &gt;

This will translate into the following at running time:

CMD.EXE /C echo it works properly for .ZIP extension –– File
  d:\tests\file_modified_detected.doc > c:\temp\it_works_ZIP.txt

It will write the string into the file c:\temp\it_works_ZIP.txt, and the value {0} in the XML will be replaced by the actual name of the file FileSystemWatcher has detected. If you’re familiar with the C# method string.Format, you won’t have any problems figuring it out.

Well, at this point I have one XML configuration file and one C# class with matching attributes, so the  next step is to deserialize the XML information into a list of classes (List<CustomFolderSettings>). Figure 3 shows the method that performs this key step.

Figure 3 Deserialization of the XML Settings File

/// <summary>Reads an XML file and populates a list of <CustomFolderSettings> </summary>
private void PopulateListFileSystemWatchers()
  // Get the XML file name from the App.config file
  fileNameXML = ConfigurationManager.AppSettings["XMLFileFolderSettings"];
  // Create an instance of XMLSerializer
  XmlSerializer deserializer =
    new XmlSerializer(typeof(List<CustomFolderSettings>));
  TextReader reader = new StreamReader(fileNameXML);
  object obj = deserializer.Deserialize(reader);
  // Close the TextReader object
  // Obtain a list of CustomFolderSettings from XML Input data
  listFolders = obj as List<CustomFolderSettings>;

Once this method executes, a list containing all the required FileSystemWatcher instances will be available, so the next step is to start the FileSystemWatcher class, which starts the listening process.

Of course, the method needs to know where the XML settings file is, and I use the App.config file to define the location of the XML file. Here’s the content of App.config:

<?xml version="1.0" encoding="utf-8" ?>
    <add key="XMLFileFolderSettings" value=
      "C:\Work\CSharp_FileSystemW\CustomSettings.xml" />
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />

It’s important to remember that any changes in the XML settings file or in the App.config file will require restarting the Windows service in order for those changes to be applied.

Starting the FileSystemWatcher Process (Listening for Changes)

At this point, all the settings required for the several (or at least one) instances of the FileSystemWatcher are available in the list created in Figure 3.

Now it’s time to start the listening process. For this, I need to loop through the list and start the instances one-by-one. The code in Figure 4 shows how to perform the initialization process and how to assign all the parameters retrieved from the XML file.

Figure 4 Initialization of the FileSystemWatcher Instances

/// <summary>Start the file system watcher for each of the file
/// specification and folders found on the List<>/// </summary>
private void StartFileSystemWatcher()
  // Creates a new instance of the list
  this.listFileSystemWatcher = new List<FileSystemWatcher>();
  // Loop the list to process each of the folder specifications found
  foreach (CustomFolderSettings customFolder in listFolders)
    DirectoryInfo dir = new DirectoryInfo(customFolder.FolderPath);
    // Checks whether the folder is enabled and
    // also the directory is a valid location
    if (customFolder.FolderEnabled && dir.Exists)
      // Creates a new instance of FileSystemWatcher
      FileSystemWatcher fileSWatch = new FileSystemWatcher();
      // Sets the filter
      fileSWatch.Filter = customFolder.FolderFilter;
      // Sets the folder location
      fileSWatch.Path = customFolder.FolderPath;
      // Sets the action to be executed
      StringBuilder actionToExecute = new StringBuilder(
      // List of arguments
      StringBuilder actionArguments = new StringBuilder(
      // Subscribe to notify filters
      fileSWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName |
      // Associate the event that will be triggered when a new file
      // is added to the monitored folder, using a lambda expression                   
      fileSWatch.Created += (senderObj, fileSysArgs) =>
        fileSWatch_Created(senderObj, fileSysArgs,
         actionToExecute.ToString(), actionArguments.ToString());
      // Begin watching
      fileSWatch.EnableRaisingEvents = true;
      // Add the systemWatcher to the list
      // Record a log entry into Windows Event Log
        "Starting to monitor files with extension ({0}) in the folder ({1})",
        fileSWatch.Filter, fileSWatch.Path));

In this code, the FileSystemWatcher is listening only for a creation event; however, other events are available, as well, such as Deleted and Renamed.

I want to especially point to the line where a function subscribes to the FileSystemWatcher Created event. Here, I use a lambda expression for an important reason: Because I have a list of instances of the FileSystemWatcher class, I need to associate a specific executable to each instance. If I handle this differently (that is, by not using a lambda expression but directly assigning the function), only the last executable will be kept and all the FileSystemWatcher instances will perform the same action.

Figure 5 shows the code for the function that actually performs the action based on individual criteria for each single instance of the FileSystemWatcher.

Figure 5 Performing an Action Based on the Criteria for Each Instance

/// <summary>This event is triggered when a file with the specified
/// extension is created on the monitored folder</summary>
/// <param name="sender">Object raising the event</param>
/// <param name="e">List of arguments - FileSystemEventArgs</param>
/// <param name="action_Exec">The action to be executed upon detecting a change in the File system</param>
/// <param name="action_Args">arguments to be passed to the executable (action)</param>
void fileSWatch_Created(object sender, FileSystemEventArgs e,
  string action_Exec, string action_Args)
  string fileName = e.FullPath;
  // Adds the file name to the arguments. The filename will be placed in lieu of {0}
  string newStr = string.Format(action_Args, fileName);
  // Executes the command from the DOS window
  ExecuteCommandLineProcess(action_Exec, newStr);

And, finally, Figure 6 shows the ExecuteCommandLineProcess function, which is a very standard way to execute command-line instructions (a DOS console).

Figure 6 Executing Command-Line Instructions

/// <summary>Executes a set of instructions through the command window</summary>
/// <param name="executableFile">Name of the executable file or program</param>
/// <param name="argumentList">List of arguments</param>
private void ExecuteCommandLineProcess(string executableFile, string argumentList)
  // Use ProcessStartInfo class
  ProcessStartInfo startInfo = new ProcessStartInfo();
  startInfo.CreateNoWindow = true;
  startInfo.UseShellExecute = false;
  startInfo.FileName = executableFile;
  startInfo.WindowStyle = ProcessWindowStyle.Hidden;
  startInfo.Arguments = argumentList;
    // Start the process with the info specified
    // Call WaitForExit and then the using-statement will close
    using (Process exeProcess = Process.Start(startInfo))
      // Register a log of the successful operation
        "Succesful operation --> Executable: {0} --> Arguments: {1}",
        executableFile, argumentList));
  catch (Exception exc)
    // Register a Log of the Exception

Starting and Stopping FileSystemWatcher Within a Windows Service

As initially stated, this application is designed to be run as a Windows service, so I need a way to start or stop FileSystemWatcher instances automatically when the Windows service starts, stops or restarts. Even though I’m not going to dig into the Windows Service definition here, it’s worth mentioning the two main methods of the Windows service implementation: OnStart and OnStop. Initially, every time the Windows service starts, it has to perform two actions: Populate the list of FileSystemWatcher instances from the XML file (Figure 3), and then start the instances (Figure 4).

Here’s the code required to start the process from the Windows service:

/// <summary>Event automatically fired when the service is started by Windows</summary>
/// <param name="args">array of arguments</param>
protected override void OnStart(string[] args)
  // Initialize the list of FileSystemWatchers based on the XML configuration file
  // Start the file system watcher for each of the file specification
  // and folders found on the List<>

And, finally, the method in Figure 7implements the logic to stop the FileSystemWatcher; it requires stopping or restarting the Windows service.

Figure 7 Stopping the FileSystemWatcher

/// <summary>Event automatically fired when the service is stopped by Windows</summary>
protected override void OnStop()
  if (listFileSystemWatcher != null)
    foreach (FileSystemWatcher fsw in listFileSystemWatcher)
      // Stop listening
      fsw.EnableRaisingEvents = false;
      // Dispose the Object
    // Clean the list

Wrapping Up

FileSystemWatcher is a powerful class that allows you to monitor (listen to) changes occurring in the file system, such as creating, deleting, and renaming files and folders, as well as modifying them. This application, which is intended to run as a Windows service, has been designed to allow for easy modification of the files and folders to be monitored, including file extensions. The approach I followed uses a very handy concept available in the .NET Framework, serialization and deserialization, making it possible to feed the FileSystemWatcher class from an XML file without requiring any change to the source code. Instead, after any modification in the XML settings file, it’s just a matter of restarting the Windows service and, voilà, the changes are applied.

Diego Ordonez is a civil engineer with more than 15 years of experience in IT working mainly with GIS and CAD technologies as an analyst, developer and architect. He is a Microsoft Certified Professional Developer in C#, ASP.NET, ADO.NET, SQL Server and he really enjoys learning and applying technologies around the .NET Framework. He lives in Calgary, Alberta, Canada, with his wife and two lovely daughters and works for Altus Geomatics as a GIS team lead (bit.ly/2aWfi34).

Thanks to the following Microsoft technical expert for reviewing this article: James McCaffrey
Dr. James McCaffrey works for Microsoft Research in Redmond, Wash. He has worked on several Microsoft products including Internet Explorer and Bing. Dr. McCaffrey can be reached at jammc@microsoft.com.

Discuss this article in the MSDN Magazine forum