.NET Instrumentation Workshop
Home About Workshops Articles Writing Talks Books Contact

2. Conditional Code

As the name suggests, conditional code is only executed under specified conditions. Of course, language constructs like if else, switch and do while allow you to provide conditional code, but the value that is tested will be provided by the application. For the purpose of this discussion the condition will be external, that is provided outside of the application. This section covers two types of external conditions: values provided at compile time, and values provided at run time.

Compile time conditions are used in two ways: to determine what code is generated and to determine what code is called. In the first case the assembly will not contain code that does not fit the condition. This does not necessarily mean that the assembly will only contain code that is called (because, of course, the logic of your code may not call all code in the assembly) but it almost means that. In contrast, conditional code is marked with a condition and calling code will only be generated if the condition is met. This means that the assembly will contain the code regardless of the conditions. 

Runtime conditions are values supplied when the application is run. The assembly can then use the conditions to determine which code is executed. To a certain extent all code uses runtime conditions, but for the purpose of this discussion I will cover the facilities in the framework to allow you to use these conditions to change how your code works.

2.1 Conditional Compilation

In C# this is straightforward, C# has a simple preprocessor that processes commands in your C# source file. Conditional compilation is based upon symbols (not to be confused with debugging symbols, which contain information to help the debugger). A symbol is simply a named value that can be defined (ie it exists) or not defined. There are two ways to define a symbol. The first way is through the compiler command line, the second way is in the actual code using the #define directive.

To test this out create a file called app.cs and type the following code:

using System;
 
class App
{
   static void Main()
   {
#if DEBUG
      Console.WriteLine("this is debug code");
#else
      Console.WriteLine("this is not debug code");
#endif
   }
}

The directives tell the compiler to include the first WriteLine only if the DEBUG symbol is defined, and only include the second WriteLine only if the DEBUG symbol is not defined. By convention you place directives at the first column, but this is not mandatory. Compile this code and run it:

csc app.cs
app 

This should print this is not debug code because the symbol DEBUG is not defined. To define this symbol use the /d command line switch:

csc /d:DEBUG app.cs

When you run this new version you'll find that the message this is debug code is printed. Now try this test: compile with the following:

csc /debug app.cs

What do you find? You should see the message this is not debug code, the same as before. The reason is that when you use /debug the DEBUG symbol is not automatically defined. This seems counter intuitive, but keep this in mind if you write your own makefiles or compile from the command line. The second way of defining a symbol is to use the #define directive. This directive will define the symbol (ie specifies that it exists) but you cannot define a value to it unlike the C #define preprocessor directive. The directive must appear at the top of the file before any code (but you can have comments before this directive). Add the following line:

#define DEBUG
using System;

Now compile the code without mentioning the symbol (csc app.cs) and run the code to confirm that the symbol is defined. To be honest, I find little use for this directive because if I have conditional code within my file I want to control how it is included by defining the symbol externally and there seems little point in specifying this in the file. There are two cases when it might be useful to define symbols in a file. The first is if you want to try out some new code and don't want to delete the code it replaces (just in case the new code does not work). This is effectively equivalent to commenting out the old code. The other situation is if you use code compiled with the [Conditional] attribute as described later. Note that during this workshop I will use this directive extensively, but that is because I am trying to teach you how to use instrumentation code, so the code I will present is written specifically to allow the instrumentation to be defined all the time. Clearly that means that the code is contrived.

The antithesis of the #define directive is the #undef directive which will make sure that a symbol is not defined. Change the previous line to:

#undef DEBUG
using System;

and compile this code with /d to define the DEBUG symbol. When you run this process you'll find that you'll get a result as if the symbol had not been defined, in other words /d tells the compiler that the symbol is defined and then #undef tells the compiler that it isn't defined. I find this directive useful particularly if I use makefiles or response files to define symbols because #undef provides a fine-grain control.

If you have a Visual Studio project then you can use the project properties to determine the symbols that are defined. For example:

You can use the top edit box to give a semicolon delimited list of the symbols that you want defined. Two standard symbols are DEBUG and TRACE and these get their own check boxes. Both of these symbols are defined for Debug configuration builds. The screenshot here is for a Release configuration build, but notice that TRACE is defined by default. I will have a lot more to say about this later.

The C# compiler provides other directives that are worth pointing out here. Firstly there is an #elif directive that is essentially else if, so you can build up a series of lines testing for the existence of many different symbols. You can also use simple Boolean expressions with ==, !=, &&, ||, ! and parenthesis. Bear in mind that a symbol is treated as having a Boolean value of true if it exists and false if it does not exist. So the following is the same as the code above:

#if !DEBUG
      Console.WriteLine("this is not debug code");
#else
      Console.WriteLine("this is debug code");
#endif

The #warning directive will generate a warning during compilation. This is useful if you want to add a message that will be displayed whenever the code is compiled. For example, remove the #undef you added earlier and add the following line:

   static void Main()
   {
#if DEBUG
#warning using debug code
      Console.WriteLine("this is debug code");
#else
      Console.WriteLine("this is not debug code");
#endif
   }
 
When you compile this code you'll get a message like this:

app.cs(8,10): warning CS1030: #warning: 'using debug code'

Notice that the line number (8) includes the line for the #if directive. You can change the line numbering using the #line directive:

   static void Main()
   {
#if DEBUG
#line 7
#warning using debug codebsp;  Console.WriteLine("this is debug code");
#else
      Console.WriteLine("this is not debug code");
#endif
   }

When you compile this code you'll see that the warning will be given for line 7, so the #line directive changes the line numbering starting at the next line. The #error directive is similar, except that it will generate a compiler error rather than a warning.

A final way to change the code that is called using command line arguments is to provide an alternative version of the entry point. The convention in C# is that the entry point method is a static method called Main. At compile time the compiler will look for a class in the file(s) being compiled for a suitable method and use that method as the entry point. The corollary is that only one class can have a method called Main. However, you can define more than one Main method as long as you tell the compiler which one will be used as the entry point. Create a new file called main.cpp:

using System;

class App
{
   static void Main()
   {
      Console.WriteLine("App.Main");
      App2.Main();
   }
}

class App2
{
   public static void Main()
   {
      Console.WriteLine("App2.Main");
   }
}

Compile this code like this: csc main.cs. You will get two errors: each saying that there are too many Main methods in the file. Change the command line to indicate which one you want to use:

csc /main:App main.cs

This time the code will compile and if you run this process you'll see that App.Main will be called followed by App2.Main. App2.Main is called because App.Main calls it, however, it can only be called by another class if the method is public. The entry point method does not have to be public, as shown with App.Main. If you would prefer Main2 to be the entry point then you can specify this method instead:

csc /main:App2 main.cs

Providing different entry point methods means that you can have different initialization code, and yu can choose which code that is used through the command line.

2.2 Conditional Code

The framework provides a pseudo-custom attribute called [Conditional] that can be used on classes or methods. This attribute provides a .custom metadata on the item (which is why the attribute is a custom attribute) but it is not recognised by the runtime (which is why it is a pseudo-attribute). This attribute is a message to the compiler, it has no effect at all on the runtime.

To see why this attribute is important consider how you would use conditional compilation to add a debugging method. To do this create a file (app.cs) and add the following:

using System;

class App
{
   static void Main()
   {
      DumpData();
   }
#if DEBUG
   static void DumpData()
   {
      Console.WriteLine("this is debugging information");
   }
#endif
}

The idea is that if the DEBUG symbol is defined then the debugging method, DumpData, will be called. Compile this code and run it to confirm that this is the case:

csc /d:DEBUG app.cs

The problem occurs when you create a release mode application. Recompile this code but do not define the DEBUG symbol, you will get this error:

app.cs(9,7): error CS0103: The name 'DumpData' does not exist in the current context

The problem is that the body of the method will not exist in release mode, so when the compiler comes to the line that calls the method it will not find the code to call. You can mitigate this issue with the following code:

   static void Main()
   {
#if DEBUG
      DumpData();
#endif
   }
#if DEBUG
   static void DumpData()
   {
      Console.WriteLine("this is debugging information");
   }
#endif

The problem is that the more directives yo have the more unreadable the code becomes.

The [Conditional] attribute is used to tell the compiler to call the method only if the specified symbol is defined. Contrast this to conditional compilation which says that the compiler should compile the code only if the specified symbol is defined.

Change the code by removing the conditional compilation directives and adding the following lines:

using System;
using System.Diagnostics;

class App
{
   static void Main()
   {
      DumpData();
   }
   Conditional("DEBUG"]
   static void DumpData()
   {
      Console.WriteLine("this is debugging information");
   }
}

Now compile the application twice and run it each time. For the first compilation define the DEBUG symbol, and for the second time do not define the DEBUG symbol. The compilation will succeed each time. You should find that in the first case DumpData will be called, but in the second case it will not. Using this second version (compiled without the DEBUG symbol), view the code with ILDASM. You will see code like this:

.class private auto ansi beforefieldinit App extends [mscorlib]System.Object
{
   .method private hidebysig static void Main() cil managed
   {
      .entrypoint
      // Code size 2 (0x2)
      .maxstack 8
      IL_0000: nop
      IL_0001: ret
   } // end of method App::Main

   .method private hidebysig static void DumpData() cil managed
   {
      .custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string)
         = ( 01 00 05 44 45 42 55 47 00 00 ) // ...DEBUG..
      // Code size 13 (0xd)
      .maxstack 8
      IL_0000: nop
      IL_0001: ldstr "this is debugging information"
      IL_0006: call void [mscorlib]System.Console::WriteLine(string)
      IL_000b: nop
      IL_000c: ret
   } // end of method App::DumpData

   .method public hidebysig specialname rtspecialname
   instance void .ctor() cil managed
   {
      // Code size 7 (0x7)
      .maxstack 8
      IL_0000: ldarg.0
      IL_0001: call instance void [mscorlib]System.Object::.ctor()
      IL_0006: ret
   } // end of method App::.ctor

} // end of class App

Notice that the assembly contains the conditional method, DumpData, but there is no code in the Main method to call it. The corresponding Main method for the DEBUG version of the process is:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   // Code size 8 (0x8)
   .maxstack 8
   IL_0000: nop
   IL_0001: call void App::DumpData()
   IL_0006: nop
   IL_0007: ret
} // end of method App::Main

In this case there is a call to the method.

The [Conditional] attribute tells the compiler to include that code marked with this attribute in the final assembly, but it says that the compiler should only make calls that method only if the specified symbol is defined for the compilation unit that calls the method. In this example the conditional method is defined in the same assembly and the same file as the code that might call it, but this does not have to be the case. The calling code and conditional code can be in different source files and they can be in different assemblies.

Note that a method like this presents a possibility of information disclosure because typically a method like this will be used to validate your data, or to perform some additional processing of your data. In most cases you will not want your customers to see such debugging information, but since the code will be part of your final assembly your customers can use a decompiler to view this code.

Since [Conditional] affects how code is called it is most useful on library code. The framework uses this attribute on methods on the Debug class (so that they are called only when the DEBUG symbol is defined) and on methods on the Trace class (so that the methods are only called when TRACE is defined).

Out of interest I decided to search through all the framework classes and see which classes, and which class members are marked with [Conditional]. The code to do this is pretty straightforward with reflection, all you have to do is load all the types in an assembly and on each type call GetCustomAttributes for ConditionalAttribute. Then if the attribute is obtained you can access the ConditionalString property. You can read the list here. It is not particularly exciting, but it is interesting to see that Microsoft is not fallible - the internal class, System.Net.GlobalLog, has [Conditional] methods based on the symbol TRAVE. Since C and V are next to each other on the keyboard I suspect that this is a typing error.

2.3 Switches

Your application can be started with command line switches and these are passed as a string array to the Main method of your application. Typically, you will use your Main method to process the switches passed through the command line and then use these values to alter how the application works. If you want the switches to be made available to code throughout the application you will have to find some mechanism to share this data, perhaps using static properties.

You can also get access to all command line parameters anywhere in your application as a single string through the Environment.CommandLine property. However, note that this is the complete command line, including the command that started the process. The Environment.GetCommandLineArgs method will return a string array where each item is a command line parameter (the command line string is parsed treating spaces as the parameter separator, except when parameters are enclose in quotes). This is not the same as the array passed to the Main method because, like the CommandLine property, the first parameter is the command used to start the process.

The CommandLine property and GetCommandLineArgs method are static and will be available anywhere in your process.

2.4 .NET Switches

The framework also provides switch classes to allow you to use information in the application configuration file. The relevant section of the configuration file is the <switches> section of the <system.diagnostics> section. To get a switch value you use a class derived from the abstract Switch class. The constructor of the derived class takes the name of the switch element to read, and this is passed to the base class constructor. The Switch class will look under the <switches> node for an <add> node that has a name attribute with the name you passed to the constructor. The Switch class then looks for a value attribute and then makes the value of this attribute available through the protected property, SwitchSetting.

The framework provides switch classes for the configuration of trace sources. I will cover trace sources later in this workshop, so I will not cover that aspect here. Version 1.1 and 1.0 of the framework used Switch classes to provide configuration information, and that is the action that I will cover here. The switch classes have changed since they appeared in the first beta version of .NET (in those days, Switch also gave access to environment variables and registry keys, so you had three options where to store your switches) and as a consequence the actual implementation is a little odd. The actual value of the switch is accessed through SwitchSetting and is returned as an integer. However, the switch value (the value attribute of the <add> node with the appropriate name attribute) is actually a string value. This is converted to an integer with a call to Int32.Parse. In .NET 3.0/2.0 classes derived from Switch can get access to the string value through the Value property. If the value attribute cannot be converted to an integer then the SwitchSetting will be a value of zero.

The Switch constructor takes two parameters, the name of the switch and a description. The second parameter is not used in this implementation and you can pass an empty string. There is an additional constructor that has three parameters where the third parameter is the default value for the switch if the there is no switch in the configuration file.

Switch is abstract, and although the framework provides several derived classes only one is relevant to this section: BooleanSwitch. As the name suggests, the class gives access to the switch as a Boolean through the Enabled property. For example, create a file called app.cs and add this code:

using System;
using System.Diagnostics;

class App
{
   public static BooleanSwitch isDebug;
   static void Main()
   {
      isDebug = new BooleanSwitch("debug", "");

      if (isDebug.Enabled)
      {
         Console.WriteLine("debug code enabled");
      }
      else
      {
         Console.WriteLine("debug code disabled");
      }
   }
}

This has a static member initialized in the Main function. Since it is public and static it means that this member can be accessed by any object in the application. This switch is associated with a node with a name of debug and if there is such a switch, and it has a value of 1, then the Enabled property will be true. As you can see, you can access this object throughout your code to determine if the application should run debugging code.

Compile this and run it. You'll find that debug code disabled will be printed because there is no switch of the specified name. To get the debugging code called you need to create a configuration file with an appropriate switch. Create a file called app.exe.config and add the following:

<configuration>
   <system.diagnostics>
      <switches>
         <add name="debug" value="1"/>
      </switches>
   </system.diagnostics>
</configuration>

Now run the application again and confirm that this time the message debug code enabled will be printed. If you change the value to 0 then you'll get the previous message, debug code disabled.

As you can see, you can put extra features in your application and selectively enable or disable them using a BooleanSwitch and a configuration file.

There is an important point to be made about switches which is not apparent from this short piece of code. When you first access a section in a configuration file the contents of the entire section will be cached in memory, so that the next time an entry in the section is read, the cached value will be used. Furthermore, when you initialize a switch the switch value will be saved in a class static member, so that if you create a new Switch object for the same switch then the cached value will be used. This means that if you have a long running process and your code has already read a switch and then you change the configuration file then the existing switch object will not reflect that change, and even if you create a new switch object it will not show the change. There is one method that will clear the switch cache (Trace.Refresh) but you have to explicitly call it and re-recreate your switch objects.

Another switch class the framework provides is called TraceSwitch. This allows you to read a value between 0 and 4 from a switch in the configuration file. You can get the actual value (as an enumeration, TraceLevel) through the Level property or you can access one of the four Boolean properties.

TraceLevel looks like this:

public enum TraceLevel { Off, Error, Warning, Info, Verbose }

The Boolean properties on TraceSwitch correspond to whether Level is equal to one of the last four values: TraceError, TraceWarning, TraceInfo and TraceVerbose.

You can, of course, create your own switch class. As I mentioned earlier the switch can only return an integer value because historically SwitchSetting returned the switch value in this way. However, the actual string that is read is stored in the object and is accessible through the Value property, but because this is protected it means that it can only be accessed by derived classes. It is trivial to derive a class that gives access to this value:

class MySwitch : Switch
{
   public MySwitch(string name, string desc) : base(name, desc) {}
   public new string Value { get { return base.Value; } }
   protected override void OnValueChanged() { }
}

The Value property is straightforward all: it does is give public access to the protected inherited property. However, the OnValueChanged needs some explanation. This is the method that converts the value's string value into an integer for SwitchSetting. The default implementation will use In32.Parse and this will throw an exception if the value is not one that can be converted to an integer. In this implementation we do not use SwitchSetting so the implementation of OnValueChanged does nothing.

The MySwitch class treats the switch as a name-value pair, which is why I have exposed the Value property as a public property. Switch has the ability to give access to other attributes on the XML node. This feature is for trace sources, but it is worth mentioning here. To allow other attributes in the configuration file node to be read you need to specify their names. To do this you have to override the protected GetSupportedAttributes method which returns a string array with the attribute names. Now when an instance of your switch is created the name and value of each attribute is available through the Attributes property.

2.5 Switch Attributes

The .NET framework also provides switch attributes. To be honest I think these are more trouble than they are worth, but I will document them here anyway. If you use switches in your application you can add a [Switch] attribute to indicate the name and type of the switch to an assembly, a class or a type member. The SwitchAttribute class has a static member called GetAll which you can use to get an array of all the SwitchAttribute objects in the assembly: that is, the attributes that have been applied to the assembly, to any type in the assembly or any type member. The SwitchAttribute class has three properties: SwitchType, SwitchName and SwitchDescription. You can use reflection with the SwitchType type object to create an instance of the appropriate type passing SwitchName and SwitchDescription to the type's constructor.

Of course, when you create a Switch object it will read its value from the configuration file, so this is one way that you can write generic code to get initialize all of the switches used by the assembly. However, it all seems to me to be a lot of effort.

Anyway, give this a try. Create a file (app.cs) and add the following:

using System;
using System.Diagnostics;
using System.Reflection;

class App
{
   static void Main()
   {
      Switch[] switches = GetSwitches();
      foreach(Switch sw in switches)
      {
         Console.WriteLine(sw.ToString());
      }
   }
   static Switch[] GetSwitches()
   {
      SwitchAttribute[] switchesAttr = SwitchAttribute.GetAll(Assembly.GetExecutingAssembly());
      Switch[] switches = new Switch[switchesAttr.Length];
      for (int x = 0; x < switches.Length; ++x)
      {
         ConstructorInfo ci = switchesAttr[x].SwitchType.GetConstructor(
            new Type[]{typeof(string), typeof(string)});
         switches[x] = (Switch)ci.Invoke(
            new object[] { switchesAttr[x].SwitchName, switchesAttr[x].SwitchDescription });
      }
      return switches;
   }
}

The GetSwitches method gets all of the [Switch] attributes defined in the assembly and then uses the information in the attribute to instantiate a Switch object. Note that the code gets the constructor that takes two strings and invokes it with the name and description in the attribute. If you create your own switch class you must make sure that you provide such a constructor.

Compile and run this code, you should find that it will run, but it provides no results because the assembly has no switch attributes. Now add the following attributes:

[assembly: Switch("bAssem", typeof(BooleanSwitch))]

[Switch("bClass", typeof(BooleanSwitch))]
class App

This says that the configuration will have two switches, bAssem and bClass, both of them will have Boolean values. Compile and run this code. This time you'll get the following results:

System.Diagnostics.BooleanSwitch
System.Diagnostics.BooleanSwitch

The problem with the abstract Switch class is that there is no virtual member that gives a clue as to the value of the switch. As I mentioned in the last section, the Value and SwitchSetting properties are protected. You have no option other than to get the switch value by downcasting:

Switch[] switches = GetSwitches();
foreach(Switch sw in switches)
{
   if (sw is BooleanSwitch)
   {
      Console.WriteLine("{0} is {1}",
         sw.DisplayName,
         ((BooleanSwitch)sw).Enabled);
   }
   else
   {
      Console.WriteLine(sw.ToString());
   }
}

Compile and run this code. Now you will see:

bAssem is False
bClass is False

This is to be expected because you do not have a configuration file and hence the switches are not defined. Now create a configuration file that contains switch data (app.exe.config):

<configuration>
   <system.diagnostics>
      <switches>
         <add name="bAssem" value="1"/>
         <add name="bClass" value="1"/>
      </switches>
   </system.diagnostics>
</configuration>

Run the application again. You'll now see the following:

bAssem is True
bClass is True

Change the switch values and run the application again and verify that the switches are being read.

As you can see, the SwitchAttribute class gives you access to the names and types of the switches that the application will use, but you have to do the work to actually instantiate those switches. To be honest, I don't really see much use for this mechanism, because you already know that you will use the switches, so what is the point of the [Switch] attribute?

2.6 Monitoring

Try this code (monitor.cs):

using System;
using System.Threading;
using System.Diagnostics;

class App
{
   static bool data;
   static bool running;
   static void Main()
   {
      Thread thread = new Thread(new ThreadStart(MonitorProc));
      thread.Start();
      Console.WriteLine("press a key to finish");
      running = true;
      Console.ReadKey();
      running = false;
   }
   static void MonitorProc()
   {
      BooleanSwitch bs = new BooleanSwitch("debug", "");
      data = bs.Enabled;
      Console.WriteLine("initial value {0}", data);
      do
      {
         bs = new BooleanSwitch("debug", "");
         if (bs.Enabled != data)
         {
            data = bs.Enabled;
            Console.WriteLine("changed to {0}", data);
         }
         Thread.Sleep(0);
      }
      while(running);
   }
}

This creates a new thread that creates a new BooleanSwitch to read the value of the debug switch from the configuration file and if the value has changed print the new value on the command line. At the end of each loop Thread.Sleep is called to give the main thread an opportunity to read the keyboard buffer. If a key has been pressed the running variable is set to false (to kill the worker thread) and the main thread dies. This arrangement, polling for a change in a configuration setting, is necessary because .NET does not give a mechanism to notify you when a configuration setting has changed.

Next create a configuration file with this data (monitor.exe.config):

<configuration>
   <system.diagnostics>
      <switches>
         <add name="debug" value="1"/>
      </switches>
   </system.diagnostics>
</configuration>

Compile the process. Now make sure that the configuration file is open in Notepad (notepad monitor.exe.config) and run the process. Change value to 0 and save the file. What do you see? Change value to 1 and save the file. What do you see? Here are my results:

C:\> monitor
initial value True
press a key to finish

C:\>

In other words, whenever a BooleanSwitch instance is created it always has a value of Enabled to be true. In general, once one BooleanSwitch instance has been created for a particular switch in the application domain, all other instances for this particular switch will have the same value. This means that the <switches> section in the configuration file should be treated in the same way as switches passed to the command line: they are read once and the cached value is used for subsequent reads. The only difference between the two is that command line switches are for the entire process, whereas configuration settings are for an application domain.

The reason for this behaviour is that the configuration classes will read the configuration file a section at a time (the definition of a section varies, but in this case it means the <system.diagnostics> element and its children) and the data is cached in memory. At a later point in time a value is read from the same section the cached data will be used. So, to get around this issue you need to get the configuration system to re-read the section when a switch has changed.

Change the using statements like this:

using System;
using System.Threading;
using System.IO;

Now replace the code that uses BooleanSwitch to use a new method called ReadSwitch:

static void MonitorProc()
{
   data = (ReadSwitch("debug") != "0");
   Console.WriteLine("initial value {0}", data);
   do
   {
      bool b = (ReadSwitch("debug") != "0");
      if (b != data)
      {
         data = b;
         Console.WriteLine("changed to {0}", data);
      }
      Thread.Sleep(0);
   }
   while(running);
}

The ReadSwitch method is straight forward:

static string ReadSwitch(string switchName)
{
   BooleanSwitch bs = new BooleanSwitch(switchName, "");
   return bs.Enabled.ToString();
}

There are two issues with this code. We still have not fixed the fact that once a section is read from the configuration file the values are cached in memory. Furthermore, the configuration file is polled for a change. Even if the ReadSwitch method read the configuration file data (rather than a cached value) polling like this is a performance drain. Instead, it would be better to read the file only when the file has changed. Admittedly, configuration files contain many types of information, other than switches, and if the configuration file changes then it could easily be non-switch information that has changed. The .NET framework provides the FileSystemWatcher class to detect when a file changes and this class can call your handler code.

The first thing to do is change the using statements to replace the threading namespace with the generic collection namespace:

using System;
using System.Collections.Generic;
using System.IO;

Now create a new class called SwitchMonitor, place in it the ReadSwitch method and add an instance field configFile:

class SwitchMonitor
{
   string configFile

   string ReadSwitch(string switchName)
   {
      // Rest of method
   }
}

This is initialized in the constructor, as is the FileSystemWatcher object. The design of this class will allow you to watch specific switches, and these are held in a dictionary. The constructor looks like this:

FileSystemWatcher fsw;

public SwitchMonitor()
{
   switches = new Dictionary<string, string>();
   configFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
   fsw = new FileSystemWatcher(Path.GetDirectoryName(configFile));
   fsw.Filter = Path.GetFileName(configFile);
   fsw.Changed += new FileSystemEventHandler(FileChanged);
   fsw.IncludeSubdirectories = false;
}

The code obtains the configuration file name and uses this to initialize the FileSystemWatcher object, any changes that occur to the file will trigger a call to the FileChanged method. The switches member has the name of the switches that you are watching. This makes the logic easier because this class can look for a change in one or more specific switches rather than all switches. You need to indicate the switch by calling the Add method:

public void Add(string key)
{
   if (!switches.ContainsKey(key))
   {
      string val = ReadSwitch(key);
      switches.Add(key, val);
   }
}

Note that a switch is only added to the collection if it is not there already, and when it is added to the collection the value of the switch is obtained so that you will be informed only if this value changes. The user of this class will be given the ability of determining whether the file is monitored or not:

public void Start()
{
   fsw.EnableRaisingEvents = true;
}
public void Stop()
{
   fsw.EnableRaisingEvents = false;
}

Your code will be informed when a switch changes and this is done through an event. Add the following delegate declaration at file scope:

delegate void SwitchChangedDelegate(string name, string val);

Now add the event and the handler method:

public event SwitchChangedDelegate SwitchChanged;

void FileChanged(object sender, FileSystemEventArgs args)
{
   if (args.ChangeType == WatcherChangeTypes.Changed)
   {
      Trace.Refresh();
      Dictionary<string, string> changes = null;

      foreach (KeyValuePair<string, string> pair in switches)
      {
         string val = ReadSwitch(pair.Key);
         if (val != pair.Value)
         {
            if (changes == null)
            {
               changes = new Dictionary<string, string>();
            }

            changes.Add(pair.Key, val);
            if (SwitchChanged != null)
            {
               SwitchChanged(pair.Key, val);
            }
         }
      }

      if (changes != null)
      {
         foreach (string key in changes.Keys)
         {
            switches[key] = changes[key];
         }
      }
   }
}

The handler only handles when the file has changed (ie, not when it is created or deleted). It then calls Trace.Refresh. When you create a Switch derived object the value of the switch will be read from the config file and cached. The Refresh method clears that cache so that the next time a switch is created it will be read from the config file.

The code then loops through all switches that you identified and read the value of the switch. If the value has changed then the new value is stored temporarily until the loop completes. This temporary store is necessary because you cannot change a dictionary while it is being enumerated. If a switch has changed then the event is raised to give your code the opportunity to handle it. Finally, if any switches have changed then when the loop completes the dictionary is updated.

Finally, the SwitchMonitor object is created in the Main method and a method is provided to handle the SwitchChanged event, remove the MonitorProc method and add the following:

static void Main()
{
   SwitchMonitor sw = new SwitchMonitor();
   sw.Add("debug");
   sw.SwitchChanged += new SwitchChangedDelegate(SwitchChanged);
   sw.Start();
   Console.WriteLine("press a key to finish");
   Console.ReadKey();
   sw.Stop();
}
static void SwitchChanged(string name, string val)
{
   Console.WriteLine("{0} = {1}", name, val);
}>

Compile the new code and test it out as before.

There are several things to note about this code. First, it only detects changes in the application configuration file, any changes made to the machine configuration file will not be detected. Second, the monitor code runs on a single thread, which means that you will avoid most threading issues. However, you should be aware that the FileChanged handler is called by a system thread, therefore you should make sure that the SwitchChanged handlers do not perform lengthy code. One way to mitigate this issue is to invoke the SwitchChanged event on another thread, perhaps passing the changes collection to this new thread so that there is no multi threaded access to the switches collection. All of this code is straightforward to do and is left as an exercise to the reader.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Three

This page is (c) 2007 Richard Grimes, all rights reserved