.NET 2.0 introduces significant changes to the System.Configuration namespace. There are plenty of articles out there that already deal with new ways of using the configuration classes. This article will not try to steal their thunder by essentially copying their content here. Instead, this article will describe a utility that I have written which helps bridge a gap in the new configuration model: providing intellisense and validation of the configuration file whilst actually editing it.
Prerequisites
It is assumed that you are already familiar with the .NET 2.0 System.Configuration namespace
. If you aren't, then take a look at this article by Luis Angel R.C.
Contents
Throughout this article, I will refer to a sample project which contains a custom configuration section. The project is a simple FileWatcher
application, which watches a number of files / folders based on the configuration settings. The settings are handled by a custom configuration section that has an XSD already generated for it. You can download the project from one of the links above.
One of the best improvements to developer productivity ever (in my opinion) has to be Intellisense. Microsoft added Intellisense features to the XML editor in Visual Studio 2002. The editor prompts you with automatic completion of elements and attributes, so long as the XML has an XML Schema supporting it that was known to Visual Studio. Now with Visual Studio 2005 and the .NET 2.0 configuration framework, we have a powerful configuration namespace built into the framework. The trouble with the new configuration namespace is that the configuration is based upon ConfigurationSection
classes and not XML Schemas. So, the XML editor can't help us complete the configuration elements.
This was the precise reason why I wrote the XSDExtractor
application. My other pet project makes extensive use of configuration files and I wanted to make sure that people could configure the application with minimal fuss. Maintaining lots of XML configuration files isn't fun when you don't get any assistance from the IDE. I also wanted to allow editing of configuration files remotely, but still with validation of the file. This meant either sending copies of the ConfigurationSection
classes to the remote client or sending them an XML Schema instead. I think you can guess which one I chose.
The XSDExtractor
program (source code available above) attached to this article creates an XML Schema file from a compiled ConfigurationSection
type by using reflection. The resultant XSD file can then be used when writing the configuration files to give the editor Intellisense.
The XSDExtractor
application is built up of parsers. Parsers in the XSDExtractor
sense are able to read the metadata contained in a compiled assembly and then deal with it appropriately. The System.Configuration namespace
contains support for writing your own elements, collections and attributes. So, I copied this model with a TypeParser
inheritance model. The base class is shown below:
The TypeParser abstract class
serves as the base class for all the TypeParser
s in the application. In addition, the type parsers are instantiated by calling a static
method on the TypeParserFactory
class. The responsibility for creating the correct parser is delegated to the factory class. All that the caller knows is that the return value from the factory class inherits from TypeParser
.
Other significant code within the application is within the Validator
model. The System.Configuration namespace
contains attributes that can restrict the type of data that is stored in a configuration field. Luckily, the XML Schema specification is powerful enough to also allow restriction of data in fields. So, the Validator
model's responsibility is to convert the validator attributes into XML Schema Simple Types, with facets that limit the values available to the field. The SimpleType
shown below has been created from an enumeration in the ConfigurationSection
class.
In a similar fashion to the TypeParser
model, the Validator
model also uses a Factory
class to instantiate the validator. The factory
object returns an instance of the ValidatorAttributeParser
. Well, a subclass of it actually:
This hierarchy of classes handles the ValidatorAttribute
family of attributes. These can be placed on any properties of the ConfigurationSection
and are used to restrict the data that is viewed as acceptable to the configuration. For example, you can restrict the values of an integer to be between a certain range of values. If the value entered in the configuration file is outside the values, an exception is thrown. Each subclass of the ValidatorAttributeParser
class deals with a particular attribute in the System.Configuration namespace
. See the limitations section below for further details on these classes.
When I first set out to write the XSDExtractor
application, I thought that I would have to write my own classes to mimic the capabilities of the XML Schema language, such as ComplexTypes
, SimpleTypes
, Elements
, etc. As it turns out, .NET ships with a System.Xml.Schema namespace that contains all of the classes I required. This enabled me to build the output XML Schema using a rich collection of objects.
It took me a little while to understand how to say that an element was of a particular complex type or that an attribute was of a type of simple type. For anyone who's interested, this is the simple rule that I followed:
To set an object's type to an existing global type -- either known in the document or via an imported namespace -- I set the SchemaTypeName
equal to an XmlQualifiedName
object, like so:
<object>.SchemaTypeName = new XmlQualifiedName("xs:string")
For an object whose type was to be consumed only by that object, I would set the SchemaType
property equal to either a new XmlSchemaComplexType
or XmlSchemaSimpleType
, like so:
<object>.SchemaType = new XmlSchemaSimpleType();
Once I got over these differences, the Xml.Schema namespace
was nice and easy to use.
First of all, extract the utility into its own folder. XSDExtractor
is a command window project and so has to be passed parameters on the command line. If you run XSDExtractor
without any parameters, then it will ask you if it's ok to convert all ConfigurationSection
subclasses in all assemblies discovered in the current directory and any subdirectories. This is a recommended usage of the utility if you have lots of ConfigurationSection
classes that you would like converted. The XSDs will be saved in the same directory as the assembly that contains the ConfigurationSection
class. It will be named the same as the ConfigutationSection
class name in order to prevent filename collisions. Here's a list of command-line parameters that the utility accepts:
Switch | Detail | Example |
/a | Specifies the assembly that should be inspected | /a C:\myassembly.dll |
/c | Class name that should be inspected | /c MyNamespace.MyClass |
/r | Root namespace element. Name of the element which will be the first element in the Xsd. If omitted, then the ConfigurationSection class name is used instead. | /r MyRoot |
/s | Silence. No prompting for overwriting or any decisions. | /s true |
Visual Studio 2005 ships with a powerful XML editor. To get Intellisense for your configuration section, all you have to do is add the xmlns
attribute to the root element. This should make the job of writing the configuration section much easier. Visual Studio 2003 did a similar job, but it assumed that you were writing a document from the root. Unfortunately, when you're writing configuration sections, this is never the case. As the following diagram shows, adding an xmlns
attribute to your configuration root element allows Visual Studio 2005 to provide Intellisense support.
Adding the xmlns
attribute does cause an unfortunate side effect: your configuration will not load! The reason why it won't load is because the configuration loader detects the attribute and then tells you that it contains an unknown attribute 'xmlns
'. It's actually a very useful feature because if you mistype any attributes, you'll know about it straight away. The solution is to add an optional string
property to your configuration like this:
[ConfigurationProperty("xmlns", IsRequired = false)]
public string Xmlns {
get { return (string)base["xmlns"];}
}
I find that it's easier to add this property to a base class that you inherit all your ConfigurationSection
subclasses from, so that you get the property without thinking about it.
Using the source code from the MultiFileWatcher
example application, you'll see that the ConfigurationSection
class is relatively simple.
public class MultiWatcherConfigurationSection : ConfigurationSection {
[ConfigurationProperty("xmlns", IsRequired = false)]
public string Xmlns {
get { return (string)base["xmlns"]; }
}
[ConfigurationProperty("files",
IsDefaultCollection=true, IsKey=false, IsRequired=true)]
public FilesCollection Files {
get { return (FilesCollection)base["files"]; }
}
}
It contains two properties, the xmlns
property I mentioned in the previous section and a collection which allows multiple elements to appear in the configuration.
XSDExtractor
works with the types and attributes contained in an assembly to build the XML Schema. In the above example, the first thing that XSDExtractor
would have done is detect that the MultiWatcherConfigurationSection
class is actually a subclass of the ConfigurationSection
class. Then, it would examine all the public
properties of the type and check which ones were decorated with the [ConfigurationProperty]
attribute. These properties are the expected -- although sometimes optional -- elements of the configuration file.
The relationship between which properties are attributes and which are elements is simple. If the property returns either a built-in type -- such as int
, string
, bool
, etc. -- then it's classed as an attribute of its containing element. If the property returns a type of ConfigurationElement
, then it's classed as a child element of its containing element. Finally, if the property returns a type of ConfigurationElementCollection
, then it's also a child element of its containing element, but has special rules applied to the particle element.
So, using the above rule, the configuration will be expected to have an attribute of type string
in the root configuration element and a child element (repeatable) of the FilesCollection
. Of course, the FileCollection
also contains further information on the child element. Here's a snippet of FileCollection
class:
[ConfigurationCollection(typeof(FileElement),
AddItemName="add",
CollectionType
= ConfigurationElementCollectionType.BasicMap)]
public class FilesCollection : ConfigurationElementCollection {
.
.
The bit that we are interested in is the attribute [ConfigurationCollection]
. This tells us that the collection will consist of ConfigurationElement
's of the FileElement
type and that the element name given to each of these FileElement
s will actually be "add
." The CollectionType
property tells us that there isn't a remove or clear option with this collection; you may simply add to the collection and that's all. Here's an example of what the collection would look like:
The <files>
element is determined by the following code fragment in the MultiWatcherConfigurationSection
class:
[ConfigurationProperty("files",
IsDefaultCollection=true, IsKey=false, IsRequired=true)]
The <add>
element represents the FileElement
type determined by the following code fragment in the FilesCollection
class.
[ConfigurationCollection(typeof(FileElement),
AddItemName="add",
CollectionType
= ConfigurationElementCollectionType.BasicMap)]
XSDExtractor
uses this meta data and the expected XML structure to build an XML Schema that matches the XML structure. It also does it using a recursive algorithm which allows it to build XML Schemas that match ConfigurationSection
classes with many nested and sub-nested types.
There are two ways to create a custom ConfigurationSection
class. The first way is the declarative model where each of the elements are decorated with ConfigurationProperty
and ConfigurationCollection
attributes. The second way is the programmatic model where the individual elements are added manually in code. Due to the way that XSDExtractor
works, it will only create an XML Schema for a ConfigurationSection
that uses the first model, the declarative model. If you mix and match the two models, then XSDExtractor
will still work, but the XML Schema produced may not be accurate.
One more limitation is that the ValidatorAttributeParser
class hierarchy only supports parsing of the standard validators that ship with .NET 2.0. If your ConfigurationSection
contains a bespoke validation attribute, then XSDExtractor
will not understand how to deal with it. The default behaviour under those circumstances is to return an unrestricted XmlSchemaSimpleType
.
I hope that you find this utility useful and that it helps you write configuration files faster. If you want to see how I'm using the features of the XSDExtractor
application within another application, then take a look at my JFDI project (a .NET 2.0 Job Framework) over at Sourceforge. Any feedback that you have is more than welcome.
- 17th May, 2007
- 24th September, 2006
- Version 1.1.1 released
Fixes: Enhancements:
- Issue where a collection used more than once in a class would be incorrectly added to the schema more than once. Thanks to Idael Cardoso for correcting this issue and providing a class that demonstrated the issue. (Unit test added to recreate the bug / prove the fix works)
- Nant compatible build script added to the project
- 10th August, 2006
- Version 1.1 released
Fixes: Enhancements:
- The /R switch now works correctly
StringValidatorAttribute
's that have a blank list of invalid characters no longer cause a blank pattern element to be generated (which caused the XSD to be invalid) xmlns
properties are now ignored - Documentation is now added to the XSD. Standard information on the field type, the default value and whether the field is required or not is now displayed in a tooltip in the VS2005 XML editor. If the property is decorated with a
System.ComponentModel.DescriptionAttribute
, then the description is appended to the default information also. - Correctly handles default collections such as the
HttpModules
configuration section - Useful information is added as a comment to the XSD so that it is possible to see how the XSD was generated, who by and when
- 1st August, 2006
The utility and source code are free to use and are released under the GNU Lesser General Public License. They are both released with the usual yada-yada about limitations of responsibility when using it, etc. If you find the utility / source code useful, then all I ask is that you give this article a mention somewhere in your app / blog / whatever!