Monday, August 24, 2015

Custom Config Sections

Sometimes when you're developing a small application (or I suppose it could be a large application) you'll find that you wish the app.config/web.config had some additional options.  Coincidentally, that exact scenario arose for me recently.  I'm writing a small console application to combine reports for three different types of code coverage.  One of the reporting tools I'm using is OpenCover, which allows you to specify - through the use of command line arguments - which assemblies to check when running.  I wanted to set something up in my config file to allow me to specify this information, so I needed some custom configuration nodes.

I started by reading through this blog, which was very informative, but didn't totally do what I needed it to do.  I extended that example to meet my needs, and this is what I ended up with:
   1:  <configSections>
   2:      <section name="AssemblySettings" type="UnitTestCoverage.ConfigurationExtensions.AssemblySettings, UnitTestCoverage.ConfigurationExtensions"/>
   3:      <section name="FilterSettings" type="UnitTestCoverage.ConfigurationExtensions.FilterSettings, UnitTestCoverage.ConfigurationExtensions"/>
   4:  </configSections>
   5:  <appSettings>...snipped...</appSettings>
   6:  <AssemblySettings>
   7:      <Assemblies>
   8:          <clear />
   9:          <add key="SomeProject.Test" value="C:\Dev\SomeProject.Test\bin\Debug\SomeProject.Test.dll" outputDirectory="C:\Output\SomeProject.Test\" outputFile="SomeProject.Test.xml">
  10:              <filters>
  11:                  <clear />
  12:                  <add key="FirstName" value="[FirstNamespace]SomeProject.FirstNamespace.*" type="Include" />
  13:                  <add key="SecondName" value="[SecondNamespace]SomeProject.SecondNamespace.*" type="Include" />
  14:                  <add key="Test" value="[SomeProject.Test]" type="Ignore" />
  15:              </filters>
  16:          </add>
  17:          <add key="SomeOtherProject.Test" value="C:\Dev\SomeOtherProject.Test\bin\Debug\SomeOtherProject.Test.dll" outputDirectory="C:\Output\SomeOtherProject.Test\" outputFile="SomeOtherProject.Test.xml">
  18:              <filters>
  19:                  <clear />
  20:                  <add key="SomeOtherProject" value="[SomeOtherNamespace]*" type="Include" />
  21:                  <add key="SomeOtherProjectTest" value="[SomeOtherNamespace.Test]*" type="Ignore" />
  22:              </filters>
  23:          </add>
  24:      </Assemblies>
  25:  </AssemblySettings>

So how did I do that?  I'd love to show you.  To get what I have above, I had to create five new objects in my solution.  I decided to keep this part separate from my business logic by creating a ConfigurationExtensions project to hold everything.  You don't have to do that, of course, but I found it to be easiest.  The first class I created was the AssemblySettings class, which inherits from System.Configuration.ConfigurationSection and corresponds to
<AssemblySettings>
up there.
   1:  public class AssemblySettings : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Assemblies", IsDefaultCollection = false)]
   4:      [ConfigurationCollection(typeof(AssemblyCollection), AddItemName = "add", RemoveItemName = "remove",
   5:          ClearItemsName = "clear")]
   6:      public AssemblyCollection Assemblies
   7:      {
   8:          get
   9:          {
  10:              AssemblyCollection assemblyCollection = (AssemblyCollection)base["Assemblies"];
  11:              return assemblyCollection;
  12:          }
  13:      }
  14:  }

The next class is AssemblyCollection (which you may have noticed is referenced on line 4 of AssemblySettings). This class inherits from ConfigurationElementCollection and it doesn't have a direct partner in the web.config. However, this is the class that allows me to have multiple Assembly items in the web.config so it is very important.
   1:  public class AssemblyCollection : ConfigurationElementCollection
   2:  {
   3:      public override ConfigurationElementCollectionType CollectionType
   4:      {
   5:          get { return ConfigurationElementCollectionType.AddRemoveClearMap; }
   6:      }
   7:   
   8:      protected override ConfigurationElement CreateNewElement()
   9:      {
  10:          return new AssemblyElement();
  11:      }
  12:   
  13:      protected override object GetElementKey(ConfigurationElement element)
  14:      {
  15:          return ((AssemblyElement)element).Key;
  16:      }
  17:   
  18:      public AssemblyElement this[int index]
  19:      {
  20:          get { return (AssemblyElement)BaseGet(index); }
  21:          set
  22:          {
  23:              if (BaseGet(index) != null)
  24:              {
  25:                  BaseRemoveAt(index);
  26:              }
  27:              BaseAdd(index, value);
  28:          }
  29:      }
  30:   
  31:      new public AssemblyElement this[string Name]
  32:      {
  33:          get { return (AssemblyElement)BaseGet(Name); }
  34:      }
  35:   
  36:      public int IndexOf(AssemblyElement assembly)
  37:      {
  38:          return BaseIndexOf(assembly);
  39:      }
  40:   
  41:      public void Add(AssemblyElement assembly)
  42:      {
  43:          BaseAdd(assembly);
  44:      }
  45:   
  46:      protected override void BaseAdd(ConfigurationElement element)
  47:      {
  48:          BaseAdd(element, false);
  49:      }
  50:   
  51:      public void Remove(AssemblyElement assembly)
  52:      {
  53:          if (BaseIndexOf(assembly) >= 0)
  54:          {
  55:              BaseRemove(assembly.Key);
  56:          }
  57:      }
  58:   
  59:      public void RemoveAt(int index)
  60:      {
  61:          BaseRemoveAt(index);
  62:      }
  63:   
  64:      public void Remove(string name)
  65:      {
  66:          BaseRemove(name);
  67:      }
  68:   
  69:      public void Clear()
  70:      {
  71:          BaseClear();
  72:      }
  73:  }

The last Assembly related class is AssemblyElement, which inherits from ConfigurationElement.
   1:  public class AssemblyElement : ConfigurationElement
   2:  {
   3:      public AssemblyElement(string key, string value, string outputDirectory, string outputFile)
   4:      {
   5:          Key = key;
   6:          Value = value;
   7:          OutputDirectory = outputDirectory;
   8:          OutputFile = outputFile;
   9:      }
  10:   
  11:      public AssemblyElement()
  12:          : this("", @"SomeValue.dll", @"C:\Output\", "results.xml")
  13:      {
  14:      }
  15:   
  16:      [ConfigurationProperty("key", DefaultValue = "", IsRequired = true, IsKey = true)]
  17:      public string Key
  18:      {
  19:          get { return (string)this["key"]; }
  20:          set { this["key"] = value; }
  21:      }
  22:   
  23:      [ConfigurationProperty("value", DefaultValue = "", IsRequired = true)]
  24:      public string Value
  25:      {
  26:          get { return (string)this["value"]; }
  27:          set { this["value"] = value; }
  28:      }
  29:   
  30:      [ConfigurationProperty("noshadow", DefaultValue = "true", IsRequired = false)]
  31:      public bool NoShadow
  32:      {
  33:          get { return (bool) this["noshadow"]; }
  34:          set { this["noshadow"] = value; }
  35:      }
  36:   
  37:      [ConfigurationProperty("outputFile", DefaultValue = @"results.xml", IsRequired = true)]
  38:      public string OutputFile
  39:      {
  40:          get { return (string)this["outputFile"]; }
  41:          set { this["outputFile"] = value; }
  42:      }
  43:   
  44:      [ConfigurationProperty("outputDirectory", DefaultValue = @"C:\Output\", IsRequired = true)]
  45:      public string OutputDirectory
  46:      {
  47:          get { return (string)this["outputDirectory"]; }
  48:          set { this["outputDirectory"] = value; }
  49:      }
  50:   
  51:      [ConfigurationProperty("filters")]
  52:      public FilterCollection Filters
  53:      {
  54:          get { return (FilterCollection)this["filters"]; }
  55:          set { this["filters"] = value; }
  56:      }
  57:  }

You can see in the config file (and in AssemblyElement) that we have a nested configuration for filters, that uses a new class called FilterCollection. FilterCollection inherits from ConfigurationElementCollection.
   1:  public class FilterCollection : ConfigurationElementCollection
   2:  {
   3:      public override ConfigurationElementCollectionType CollectionType
   4:      {
   5:          get { return ConfigurationElementCollectionType.AddRemoveClearMap; }
   6:      }
   7:   
   8:      protected override ConfigurationElement CreateNewElement()
   9:      {
  10:          return new FilterElement();
  11:      }
  12:   
  13:      protected override object GetElementKey(ConfigurationElement element)
  14:      {
  15:          return ((FilterElement)element).Key;
  16:      }
  17:   
  18:      public FilterElement this[int index]
  19:      {
  20:          get { return (FilterElement)BaseGet(index); }
  21:          set
  22:          {
  23:              if (BaseGet(index) != null)
  24:              {
  25:                  BaseRemoveAt(index);
  26:              }
  27:              BaseAdd(index, value);
  28:          }
  29:      }
  30:   
  31:      new public FilterElement this[string Name]
  32:      {
  33:          get { return (FilterElement)BaseGet(Name); }
  34:      }
  35:   
  36:      public int IndexOf(FilterElement filter)
  37:      {
  38:          return BaseIndexOf(filter);
  39:      }
  40:   
  41:      public void Add(FilterElement filter)
  42:      {
  43:          BaseAdd(filter);
  44:      }
  45:   
  46:      protected override void BaseAdd(ConfigurationElement element)
  47:      {
  48:          BaseAdd(element, false);
  49:      }
  50:   
  51:      public void Remove(FilterElement filter)
  52:      {
  53:          if (BaseIndexOf(filter) >= 0)
  54:          {
  55:              BaseRemove(filter.Key);
  56:          }
  57:      }
  58:   
  59:      public void RemoveAt(int index)
  60:      {
  61:          BaseRemoveAt(index);
  62:      }
  63:   
  64:      public void Remove(string name)
  65:      {
  66:          BaseRemove(name);
  67:      }
  68:   
  69:      public void Clear()
  70:      {
  71:          BaseClear();
  72:      }
  73:  }

Since a FilterCollection is made up of individual filters, we'll have to create those as well. The FilterElement class inherits from ConfigurationElement.
   1:  public class FilterElement : ConfigurationElement
   2:  {
   3:      public FilterElement(string key, string type, string value)
   4:      {
   5:          Type = type;
   6:          Value = value;
   7:          Key = key;
   8:      }
   9:   
  10:      public FilterElement()
  11:          : this("Namespace", "Add", "[*]*")
  12:      {
  13:      }
  14:   
  15:      [ConfigurationProperty("key", DefaultValue = "", IsRequired = true, IsKey = true)]
  16:      public string Key
  17:      {
  18:          get { return (string)this["key"]; }
  19:          set { this["key"] = value; }
  20:      }
  21:   
  22:      [ConfigurationProperty("type", DefaultValue = "Add", IsRequired = true)]
  23:      public string Type
  24:      {
  25:          get { return (string)this["type"]; }
  26:          set { this["type"] = value; }
  27:      }
  28:   
  29:      [ConfigurationProperty("value", DefaultValue = "[*]*")]
  30:      public string Value
  31:      {
  32:          get { return (string)this["value"]; }
  33:          set { this["value"] = value; }
  34:      }
  35:  }

No comments:

Post a Comment