The service I am currently writing has some complex configuration settings. I’ve created custom ConfigurationSections to handle validation. My goal is to validate the entire configuration at startup. In the process I’ve observed some interesting behaviors about ConfigurationSection’s.
To illustrate, I begin with the definition of a simple configuration section:
class MySection : ConfigurationSection
{
[ConfigurationProperty("lions", DefaultValue="X", IsRequired=true)]
[StringValidator(MinLength=1, MaxLength=10)]
public string Lions
{
get { return (string)this["lions"]; }
set { this["lions"] = value; }
}
[ConfigurationProperty("tigers", DefaultValue="", IsRequired=true)]
[StringValidator(MinLength=0, MaxLength=10)]
public string Tigers
{
get { return (string)this["tigers"]; }
set { this["tigers"] = value; }
}
[ConfigurationProperty("bears", DefaultValue="chicago", IsRequired=true)]
[CallbackValidator(Type=typeof(MySection), CallbackMethodName="OhMy")]
public string Bears
{
get { return (string)this["bears"]; }
set { this["bears"] = value; }
}
public static void OhMy(object value)
{
string s = (string)value;
Console.WriteLine("Checking value '{0}'", s);
if (s.ToLowerInvariant().Contains("panda"))
{
Console.WriteLine("Raising \"No panda's allowed\"");
throw new ConfigurationErrorsException("No Pandas Allowed!");
}
}
}
And a program to load/access it:
class Program
{
static void TestProperty(ConfigurationSection s, string n)
{
try
{
// access to s[n] is protected so...
Console.WriteLine("Testing {0}='{1}'", n,
s.ElementInformation.Properties[n].Value);
}
catch (ConfigurationErrorsException x)
{
Console.WriteLine("Testing {0}= {1}", n,
x.GetBaseException().Message);
}
}
static void Main(string[] args)
{
try
{
Console.WriteLine("Opening configuration");
var c = ConfigurationManager.
OpenExeConfiguration(ConfigurationUserLevel.None);
Console.WriteLine("Loading 'mySection'");
var s = (MySection)c.GetSection("mySection");
Console.WriteLine("mySection loaded? {0}", null != s);
if (null != s)
{
// bypass accessors to avoid 3 try/catch blocks in sample
TestProperty(s, "lions");
TestProperty(s, "tigers");
TestProperty(s, "bears");
}
}
catch (Exception x)
{
Console.WriteLine(x.GetBaseException().Message);
}
if (Debugger.IsAttached) // make F5 work like CTRL+F5
{
Console.WriteLine("Press any key to continue . . .");
Console.ReadKey();
}
}
Running the program with the following configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
</configSections>
</configuration>
Produces:
Opening configuration
Loading 'mySection'
mySection loaded? False
Since the section isn’t defined, the expected outcome is that it is not loaded. But now it starts getting interesting because this configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="mySection" type="UnitTest.MySection,UnitTest"/>
</configSections>
</configuration>
Produces:
Opening configuration
Loading 'mySection'
Checking value 'chicago'
mySection loaded? True
Testing lions='X'
Testing tigers=''
Testing bears='chicago'
Even though there is no <mySection> tag in the configuration file, a section is created using all the default values. Further, those values are validated as evidenced by the "Checking value 'chicago'" reported by the CallbackValidator.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="mySection" type="UnitTest.MySection,UnitTest"/>
</configSections>
<mySection />
</configuration>
Produces:
Opening configuration
Loading 'mySection'
Checking value 'chicago'
Required attribute 'lions' not found.
(c:\dev\foo\UnitTest\UnitTest\bin\Debug\UnitTest.exe.Config line 6)
Here all the default values were again loaded as reported by the CallbackValidator. Then the section was checked for required values. Since none were supplied, the expected exception was raised and then trapped by line 35 of the program. This configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="mySection" type="UnitTest.MySection,UnitTest"/>
</configSections>
<mySection lions="mufasa" tigers="tigger" bears="smokey"/>
</configuration>
Produces:
Opening configuration
Loading 'mySection'
Checking value 'chicago'
Checking value 'smokey'
mySection loaded? True
Testing lions='mufasa'
Testing tigers='tigger'
Testing bears='smokey'
With a fully valid configuration, it's plain that both default values and supplied values are validated as the section gets loaded. Also note that when the bears property is read, the validation method is not called a third time.
Now the really confusing bit. This Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="mySection" type="UnitTest.MySection,UnitTest"/>
</configSections>
<mySection lions="mufasa" tigers="tigger" bears="panda"/>
</configuration>
Produces:
Opening configuration
Loading 'mySection'
Checking value 'chicago'
Checking value 'panda'
Raising "No panda's allowed"
mySection loaded? True
Testing lions='mufasa'
Testing tigers='tigger'
Testing bears= The value for the property 'bears' is not valid.
The error is: No Pandas Allowed!
(C:\dev\foo\unittest\unittest\bin\debug\UnitTest.exe.Config line 6)
Despite there being an invalid value 'panda' in the configuration file, the section loaded successfully. In contrast to the missing required value error above, no exceptions were during the load even though "Raising No Panda's Allowed" indicates that the throw line did execute. Only later when the property is accessed does the exception get raised, but where did that exception come from if the callback was not executed again? One last configuration to examine. This configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="mySection" type="UnitTest.MySection,UnitTest"/>
</configSections>
<mySection lions="mufasa_too_long" tigers="tigger" bears="smokey"/>
</configuration>
Produces:
Opening configuration
Loading 'mySection'
Checking value 'chicago'
Checking value 'smokey'
mySection loaded? True
Testing lions= The value for the property 'lions' is not valid.
The error is: The string must be no more than 10 characters long.
(C:\dev\foo\unittest\unittest\bin\debug\UnitTest.exe.Config line 6)
Testing tigers='tigger'
Testing bears='smokey'
This illustrates that this strange behavior isn't limited to the CallbackValidator. What's happening here is that failures reported by ConfigurationValidators are trapped by the framework, They get added to a collection found at section.ElementInfo.Errors. Looking at the documentation of ElementInfo another useful property is IsPresent which provides a nice way to tell if the tag existed in the file or if it was created on the fly.
And my updated program to detect error conditions before trying to use attributes:
class Program
{
static void TestProperty(ConfigurationSection s, string n)
{
try
{
// access to s[n] is protected so...
Console.WriteLine("Testing {0}='{1}'", n, s.ElementInformation.Properties[n].Value);
}
catch (ConfigurationErrorsException x)
{
Console.WriteLine("Testing {0}= {1}", n, x.GetBaseException().Message);
}
}
static void Main(string[] args)
{
try
{
Console.WriteLine("Opening configuration");
var c = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
Console.WriteLine("Loading 'mySection'");
var s = (MySection)c.GetSection("mySection");
// Make sure there is a section definition.
if (null == s)
throw new ConfigurationErrorsException(
"No section named 'mySection' in configuration file.");
// Make sure there's a matching section element.
if (!s.ElementInformation.IsPresent)
throw new ConfigurationErrorsException(
"No element 'mySection' in configuration file.");
// Make sure there were no validation errors on load.
// (Just throw the first one, though it could be nice and build a list.)
foreach(Exception x in s.ElementInformation.Errors)
throw new ConfigurationErrorsException("Validation Error", x);
// all validation tests have passed!
TestProperty(s, "lions");
TestProperty(s, "tigers");
TestProperty(s, "bears");
}
catch (Exception x)
{
Console.WriteLine(x.GetBaseException().Message);
}
if (Debugger.IsAttached) // make F5 work like CTRL+F5
{
Console.WriteLine("Press any key to continue . . .");
Console.ReadKey();
}
}
}
I can't explain the quirks in the validation process, but hopefully I've provided a reasonable workaround.