|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionOne of the wonderful features of .NET has been its XML configuration features. In the days of .NET 1.x, common application settings, database connection strings, ASP.NET web server configuration, and basic custom configuration data could be stored in a .config file. Custom configuration sections could use a few basic but custom structures, allowing a small variety of information to be stored in a .config file. More complex configuration, however, was most often accomplished with custom XML structures and custom parsing code. Such code could become quite complex, though, and there are a variety of ways of differing performance to accomplish the same thing. With .NET 2.0, the days of writing your own (probably complicated, poorly-performing, and tedious) code to manage custom XML configuration structures are over. The custom configuration capabilities of the built-in XML configuration subsystem in .NET 2.0 have been greatly revamped, boasting some extremely useful and time saving features. With relative ease, just about any XML configuration structure you might need is possible with a relatively minimal amount of work. In addition, deserialization of the XML in a .config file can always be overridden. This allows any XML structure necessary to be supported without losing the other advanced features of .NET 2.0 configuration. The Continuing MysteryIn the first installment of the Mysteries of .NET 2.0 Configuration, I covered some of the basics of the configuration framework. Creation of custom configuration sections, section groups, elements, and collections encompass the core features of the configuration framework, but are only the beginning. In this article, we will begin decoding some of the more interesting pieces of the .NET 2.0 configuration puzzle such as type conversion and validation. This article will also cover some of the performance and usability concerns of the .NET 2.0 Configuration framework, and how to improve or overcome them. This article is a continuation of the Mysteries of .NET 2.0 Configuration series. The first article, "Unraveling the Mysteries of .NET 2.0 Configuration", can be found here. If you are unfamiliar with what the .NET 2.0 Configuration framework has to offer, and do not know how to write and use custom Topics of Configuration
Validating Configuration DataAs we learned in "Unraveling the Mysteries", writing custom configuration sections is quite simple and straightforward. The benefits of writing a .NET 2.0 Validating the correctness of data in your custom configuration is very straightforward. The .NET 2.0 Configuration framework actually includes several premade validators that should suffice for most validation needs. In the event that one of the premade validators is not sufficient, there are two additional methods of validating your configuration data. It is important to note that only one validator at a time can be applied to a configuration property. Sometimes you may find yourself needing to use two validators, and the simplest solution to this is to create your own wrapper validator, using the two premade solutions within your custom validator class. Using Premade ValidatorsUsing premade validators in your custom configuration classes is a simple matter of applying them to the properties you wish to be validated. There are two ways of applying validators, one imperative and one declarative. If you choose to explicitly create your configuration properties and override the The Validation Types
For the most part, these validators are self-explanatory. Validating <configuration>
<configSections>
<section name="example"
type="Examples.Configuration.ValidatedExampleSection, Examples" />
</configSections>
<example myTimeSpan="8:15:00" myInt="10" myLong="6018427387904" />
</configuration>
We can write a validated configuration section that ensures the data typed into each of the three properties is indeed valid public class ValidatedExampleSection: ConfigurationSection
{
#region Constructor
static ValidatedExampleSection()
{
s_propMyTimeSpan = new ConfigurationProperty(
"myTimeSpan",
typeof(TimeSpan),
TimeSpan.Zero,
null,
new TimeSpanValidator(TimeSpan.Zero, TimeSpan.FromHours(24)),
ConfigurationPropertyOptions.IsRequired
);
s_propMyInt = new ConfigurationProperty(
"myInt",
typeof(int),
0,
null,
new IntegerValidator(-10, 10),
ConfigurationPropertyOptions.IsRequired
);
s_propMyLong = new ConfigurationProperty(
"myLong",
typeof(long),
0,
null,
new LongValidator(Int64.MinValue, Int64.MaxValue),
ConfigurationPropertyOptions.IsRequired
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propMyTimeSpan);
s_properties.Add(s_propMyInt);
s_properties.Add(s_propMyLong);
}
#endregion
#region Fields
private static ConfigurationPropertyCollection s_properties;
private static ConfigurationProperty s_propMyTimeSpan;
private static ConfigurationProperty s_propMyInt;
private static ConfigurationProperty s_propMyLong;
#endregion
#region Properties
[ConfigurationProperty("myTimeSpan", DefaultValue=TimeSpan.Zero,
IsRequired=true)]
[TimeSpanValidator(MinValueString="0:0:0", MaxValueString="24:0:0")]
public TimeSpan MyTimeSpan
{
get { return (TimeSpan)base[s_propMyTimeSpan]; }
}
[ConfigurationProperty("myInt", DefaultValue=0, IsRequired=true)]
[IntegerValidator(-10, 10)]
public int MyInt
{
get { return (int)base[s_propMyInt]; }
}
[ConfigurationProperty("myLong", DefaultValue=0, IsRequired=true)]
[LongValidator(Int64.MinValue, Int64.MaxValue)]
public int MyInt
{
get { return (int)base[s_propMyLong]; }
}
#endregion
}
In the event that any of the configuration data was outside of the specified ranges (i.e., The other premade validators may not be quite as straightforward as the ones just discussed. The The last premade validator we will discuss in this section is the The Callback ValidatorMost often, when writing any piece of code, a simple solution is all that's initially needed. In the event you find yourself writing a validated configuration section and one of the premade validators is not sufficient, but your needs are not particularly complex, the Let's assume we have a configuration section that expects an integer value to be the modulus of 10, between -100 and 100. The public class ValidatedExampleSection: ConfigurationSection
{
#region Constructor
static ValidatedExampleSection()
{
s_propMyInt = new ConfigurationProperty(
"myInt",
typeof(int),
0,
null,
new CallbackValidator(new
ValidatorCallback(ModRangeValidatorCallback)),
ConfigurationPropertyOptions.IsRequired
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propMyInt);
}
#endregion
#region Fields
private static ConfigurationPropertyCollection s_properties;
private static ConfigurationProperty s_propMyInt;
#endregion
#region Properties
[ConfigurationProperty("myInt", DefaultValue=0, IsRequired=true)]
[CallbackValidator("Examples.Configuration.
ValidatedExampleSection.ModRangeValidatorCallback")]
public int MyInt
{
get { return (int)base[s_propMyInt]; }
}
#endregion
#region Helpers
private void ModRangeValidatorCallback(object value)
{
int intVal = (int)value;
if (intVal >= -100 && intVal <= 100)
{
if (intVal % 10 != 0)
throw new ArgumentException("The integer " +
"value is not a multiple of 10.");
}
else
{
throw new ArgumentException("The integer value is not" +
" within the range -100 to 100");
}
}
#endregion
}
It is possible to reuse a callback for multiple properties in a Writing Your Own ValidatorWriting a custom validator is a simple task, and only requires the class derives from
Validators require no special constructors, so you are free to create as many as you like with whatever parameters are necessary. It is interesting to note that configuration validators can be created and used in your own code, and do not necessarily need to be applied to a Below is an example of a custom validator. This validator is also a wrapper for both the public class RegexStringWrapperValidator: ConfigurationValidatorBase
{
#region Constructors
public RegexStringWrapperValidator(string regex) :
this (regex, 0, 0x7fffffff)
{
}
public RegexStringWrapperValidator(string regex, int minLength) :
this(regex, minLength, 0x7fffffff)
{
}
public RegexStringWrapperValidator(string regex, int minLength,
int maxLength)
{
m_regexValidator = new RegexStringValidator(regex);
m_stringValidator = new StringValidator(minLength, maxLength);
}
#endregion
#region Fields
private RegexStringValidator m_regexValidator;
private StringValidator m_stringValidator;
#endregion
#region Overrides
public override bool CanValidate(Type type)
{
return (type == typeof(string));
}
public override void Validate(object value)
{
m_stringValidator.Validate(value);
m_regexValidator.Validate(value);
}
#endregion
}
As you can see, writing a custom validator is extremely simple. However, we are not done quite yet. We still need to write a custom attribute so we can declaratively apply our validator to a public sealed class RegexStringWrapperValidatorAttribute:
ConfigurationValidatorAttribute
{
#region Constructors
public RegexStringWrapperValidatorAttribute(string regex)
{
m_regex = regex;
m_minLength = 0;
m_maxLength = 0x7fffffff;
}
#endregion
#region Fields
private string m_regex;
private int m_minLength;
private int m_maxLength;
#endregion
#region Properties
public string Regex
{
get { return m_regex; }
}
public int MinLength
{
get
{
return m_minLength;
}
set
{
if (m_maxLength < value)
{
throw new ArgumentOutOfRangeException("value",
"The upper range limit value must be greater" +
" than the lower range limit value.");
}
m_minLength = value;
}
}
public int MaxLength
{
get
{
return m_maxLength;
}
set
{
if (m_minLength > value)
{
throw new ArgumentOutOfRangeException("value",
"The upper range limit value must be greater " +
"than the lower range limit value.");
}
m_maxLength = value;
}
}
public override ConfigurationValidatorBase ValidatorInstance
{
return new RegexStringWrapperValidator(m_regex, m_minLength,
m_maxLength);
}
#endregion
}
Maintaining Type SafetyA reigning beauty of the .NET platform is its strict type safety. This feature helps in writing safe, secure code. The most vulnerable components of any application are those that access external resources, such as configuration. Thankfully, the .NET 2.0 Configuration framework includes some facilities to help you ensure your configuration data is type safe. Initially, all configuration data is read as a string, and must be converted to the proper type during deserialization to be used. The .NET 2.0 Configuration framework does this automatically for the most part, but there are times when the default conversion is insufficient. Configuration type converters can be used to handle custom conversion, and when used in conjunction with validators, can ensure your configuration data is type safe and secure. Using Premade ConvertersWhen writing a custom configuration section, type conversion is generally something that is done automatically by the .NET 2.0 Configuration framework. There are some special instances where automatic type conversion is not sufficient, such as when an integer value needs to be any The Converter Types
Again, these are all self-explanatory and need little explanation, but for posterity's sake, I'll provide an example. One thing to note is that enumerations are handled automatically and quite well by the framework through the use of the public class TypeSafeExampleSection: ConfigurationSection
{
#region Constructor
static TypeSafeExampleSection()
{
s_propMyInt = new ConfigurationProperty(
"myInt",
typeof(int),
"Infinite",
new InfiniteIntConverter(),
new IntegerValidator(-10, 10),
ConfigurationPropertyOptions.IsRequired
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propMyInt);
}
#endregion
#region Fields
private static ConfigurationPropertyCollection s_properties;
private static ConfigurationProperty s_propMyInt;
#endregion
#region Properties
[ConfigurationProperty("myInt", DefaultValue="Infinite", IsRequired=true)]
[IntegerValidator(-10, 10)]
[TypeConverter(typeof(InfiniteIntConverter)]
public int MyInt
{
get { return (int)base[s_propMyInt]; }
}
#endregion
}
Writing Your Own ConvertersThe true value of type converters shows when you run into a scenario where there is no direct correlation between an XML/text representation of data and the .NET classes and structures that store that data. Consider this simple structure: struct MyStruct
{
public int Length;
public int Width;
public int Height;
public double X;
public double Y;
public double Z;
public override string ToString()
{
return "L: " + Length + ", W: " + Width + ", H: " + Height +
", X: " + X + ", Y: " + Y + ", Z: " + Z;
}
}
Using such a structure in a configuration file is not as straightforward as using an L: 20, W: 30, H: 10, X: 1.5, Y: 1.0, Z: 7.3
Let's also assume we don't care what order each pair is in, nor what capitalization or spacing there is between each component. Let's also assume that all six components are required, and that none of them can be left out. We can now write a type converter to convert between a string representation and our public sealed class MyStructConverter: ConfigurationConverterBase
{
public MyStructConverter() { }
/// <summary>Converts a string to a MyStruct.</summary>
/// <returns>A new MyStruct value.</returns>
/// The <see
/// cref="T:System.ComponentModel.ITypeDescriptorContext">
/// </see> object used for type conversions.
/// The <see
/// cref="T:System.Globalization.CultureInfo">
/// </see> object used during conversion.
/// The <see cref="T:System.String">
/// </see> object to convert.
public override object ConvertFrom(ITypeDescriptorContext ctx,
CultureInfo ci, object data)
{
string dataStr = ((string)data).ToLower();
string[] values = dataStr.Split(',');
if (values.Length == 6)
{
try
{
MyStruct myStruct = new MyStruct();
foreach (string value in values)
{
string[] varval = value.Split(':');
switch (varval[0])
{
case "l":
myStruct.Length = Convert.ToInt32(varval[1]); break;
case "w":
myStruct.Width = Convert.ToInt32(varval[1]); break;
case "h":
myStruct.Height = Convert.ToInt32(varval[1]); break;
case "x":
myStruct.X = Convert.ToDouble(varval[1]); break;
case "y":
myStruct.Y = Convert.ToDouble(varval[1]); break;
case "z":
myStruct.Z = Convert.ToDouble(varval[1]); break;
}
}
return myStruct;
}
catch
{
throw new ArgumentException("The string contained invalid data.");
}
}
throw new ArgumentException("The string did not contain all six, " +
"or contained more than six, values.");
}
/// <summary>Converts a MyStruct to a string value.</summary>
/// <returns>The string representing the value
/// parameter.</returns>
/// The <see
/// cref="T:System.ComponentModel.ITypeDescriptorContext">
/// </see> object used for type conversions.
/// The <see
/// cref="T:System.Globalization.CultureInfo">
/// </see> object used during conversion.
/// The <see cref="T:System.String">
/// </see> object to convert.
/// The type to convert to.
public override object ConvertTo(ITypeDescriptorContext ctx,
CultureInfo ci, object value, Type type)
{
return value.ToString();
}
}
Concering PerformanceSlow performance is probably one of the top killers of any application, and often the hardest aspect of an application to tune. What runs like a dream in a developer's environment can quickly become overwhelmed in high-throughput, high-use production environments. Optimization is often a long running and ongoing task for many applications. Thankfully, optimizing your custom configuration classes is a very simple matter, and requires hardly any additional work over not optimizing them. Throughout this article and its predecessor, I've written example configuration classes using two methods: declaratively through attributes, and explicitly using static variables and a static constructor. The explicit method is the optimized method, but why it is optimized is not apparent unless you dig into the source code that drives the .NET 2.0 Configuration framework. The quickest and simplest way of writing a configuration class is to use attributes, applying /// <summary>Gets the collection of properties.</summary>
/// <returns>The <see
/// cref="T:System.Configuration.ConfigurationPropertyCollection">
/// </see> collection of properties for the element.</returns>
protected virtual ConfigurationPropertyCollection Properties
{
get
{
ConfigurationPropertyCollection collection1 = null;
if (ConfigurationElement.PropertiesFromType(base.GetType(),
out collection1))
{
ConfigurationElement.ApplyInstanceAttributes(this);
ConfigurationElement.ApplyValidatorsRecursive(this);
}
return collection1;
}
}
This is the default private static bool PropertiesFromType(Type type,
out ConfigurationPropertyCollection result)
{
ConfigurationPropertyCollection collection1 =
(ConfigurationPropertyCollection)
ConfigurationElement.s_propertyBags[type];
result = null;
bool flag1 = false;
if (collection1 == null)
{
lock (ConfigurationElement.s_propertyBags.SyncRoot)
{
collection1 = (ConfigurationPropertyCollection)
ConfigurationElement.s_propertyBags[type];
if (collection1 == null)
{
collection1 =
ConfigurationElement.CreatePropertyBagFromType(type);
ConfigurationElement.s_propertyBags[type] =
collection1;
flag1 = true;
}
}
}
result = collection1;
return flag1;
}
It is also important to note that the Once your public override ConfigurationPropertyCollection Properties
{
get
{
return s_properties;
}
}
In the overridden property, we are completely bypassing the base code and returning our prepared properties collection. Our properties collection incurs no reflection costs due to the fact that we explicitly defined each and every Best Configuration PracticesMaking the best use of the .NET 2.0 Configuration framework generally means following the same best practices you would when writing any piece of code, or when using XML for any other purpose. Best practices are best practices, so don't skimp out just because its configuration you're dealing with. Some of the best practices I use when writing configuration for my .NET apps follow.
Article History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||