Demonstrating Custom Configuration for .NET (Part 3): Validation
Validating Values
Introduction
Previous Parts of This Series.
Parts 1 and 2 added structure and data, but this is often not enough, one needs valid data—the domain of the data for your configuration being narrower than the domain of the underlying type of the parsed values. Hence the need to extend the custom types representing the structure with more types and attributes to limit the ranges of those values.
Value Validation
Checking single values is the easiest part, albeit needing two custom types to be defined for each distinct kind of validation, but at least each case can be parameterised (i.e. if you need to check a date range, you only need two types, not four).
First: The Validator
The first type is derived from System.Configuration.ConfigurationValidatorBase
.
This will actually do the validation in two steps:
-
Overriding
CanValidate
to confirm that the type can be validated. Returntrue
to indicate you can, otherwise validation will fail. This override is usually used to confirm the type of the value matches the expectations of the next method. -
Override
Validate
to perform the validation. To indicate failure throw an exception (which will be wrapped by the configuration runtime, into aConfigurationErrorsException
.
NB. the validation needs to handle the default (before the configuration
content is parsed) value. E.g. a DateTime
value will be initially
validated with a value of DateTime.MinValue
before being called
again with the value read from the configuration file. (For types which can
be used with code attributes—see §17.1.3 of the C# specification,
the default value is an optional parameter for the ConfigurastionProperty
attribute. For other types, the code attribute based declaration of configuration
values can be replaced with a more programmatic one, which I should cover later
in this series.)
For instance to check that a DateTime
value has a minimum year the
following validation code will work (note the check to allow MinValue
):
Second: The Attribute
The second type is derived from ConfigurationValidatorAttribute
(which itself derives from System.Attribute
). This is used to
(1) annotate the configuration property in the ConfigurationElement
(or
ConfigurationSection
) type, and (2) be an object factory for the first,
validation, type. Additional parameters can be passed from this attribute to
the validator.
The key override is ValidatorInstance
which needs to return an
initialised instance of the validator class.
For example, to support the DateTimeValidator
type, the following:
Using Validation
The only change is to annotate the configuration property with the just defined attribute, passing any necessary additional parameters, e.g.:
Note the use of an explicit lower limit for StartDate
but not for
EndDate
. (DateTime
is not a type with literals
available for attribute parameters, hence just using the year here.)
What Happens on Validation Failure?
Validation is performed when the configuration section is read, i.e. when
Configuration.GetSection(‹name›)
is
called. Importantly this means that if configuration sections are not used,
validation will not be performed, and no errors will be reported, however many
invalid, missing or extra values there are. Also note that extra attributes
and elements will be reported as errors, this behaviour can be modified by
overriding ConfigurationElement
’s
OnDeserializeUnrecognizedAttribute
or
OnDeserializeUnrecognizedElement
.
When an error, validation or otherwise (including malformed XML) occurs a
ConfigurationErrorsException
is thrown. If this is based
on another exception being thrown internally (e.g. on malformed XML, a
XmlException
) then that original exception may be the
ConfigurationErrorsException
’s InnerException
(sometimes this seems to be the base, other times not—I don’t see
much consistency).
Any reporting of this to the user or logging is up to the application, this is not trivial as while the text of the message does include key information (like where the error was in the configuration file) it is not exactly in a user friendly format. But then configuration files are not targeted at (typical) end user direct editing.
Checking Values Together
There is no direct support for further validation after
each individual value has been loaded and (given suitable attributes)
validated. But ConfigurationElement
does have the
PostDeserialize
method, which is called at the right time.
But the error message is not ideal, starting with the text: “An error occurred creating the configuration section handler for ‹section-name›”. But it does work.
See the code for AppConfigSection.cs for an example