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:

  1. Overriding CanValidate to confirm that the type can be validated. Return true 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.
  2. Override Validate to perform the validation. To indicate failure throw an exception (which will be wrapped by the configuration runtime, into a ConfigurationErrorsException.

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):

public class DateTimeValidator : ConfigurationValidatorBase {
   private DateTime lowerLimit;
   internal DateTimeValidator(DateTime lowerLimit) {
     this.lowerLimit = lowerLimit;
   }

   public override bool CanValidate(Type type) {
     return type.Equals(typeof(DateTime));
   }

   public override void Validate(object value) {
     Trace.Assert(value.GetType().Equals(typeof(DateTime)));
     DateTime val = (DateTime)value;
     // MinValue is used as a placeholder before the actual value is set
     if (val < lowerLimit && val != DateTime.MinValue) {
       throw new ArgumentException("Invalid DateTime value in configuration");
     }
   }
 }

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:

[AttributeUsage(AttributeTargets.Property)]
public class DateTimeValidatorAttribute : ConfigurationValidatorAttribute {
  public DateTimeValidatorAttribute() {
  }

  public override ConfigurationValidatorBase ValidatorInstance {
    get {
      return new DateTimeValidator(new DateTime(minYear, 1, 1));
    }
  }

  private int minYear = 1900;
  public int MinimumYear {
    get { return minYear; }
    set { minYear = value; }
  }
}

Using Validation

The only change is to annotate the configuration property with the just defined attribute, passing any necessary additional parameters, e.g.:

[ConfigurationProperty("startDate", IsRequired=true),
 DateTimeValidator(MinimumYear=2000)]
public DateTime StartDate {
  get { return (DateTime)this["startDate"]; }
  set { this["startDate"] = value; }
}

[ConfigurationProperty("endDate", IsRequired=true),
 DateTimeValidator()]
public DateTime EndDate {
  get { return (DateTime)this["endDate"]; }
  set { this["endDate"] = value; }
}

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

The Complete Solution

DateTimeValidator.cs

namespace ConfigurationDemoPart3 {
  public class DateTimeValidator : ConfigurationValidatorBase {
    private DateTime lowerLimit;

    internal DateTimeValidator(DateTime lowerLimit) {
      this.lowerLimit = lowerLimit;
    }

    public override bool CanValidate(Type type) {
      return type.Equals(typeof(DateTime));
    }

    public override void Validate(object value) {
      Trace.Assert(value.GetType().Equals(typeof(DateTime)));
      DateTime val = (DateTime)value;

      // MinValue is used as a placeholder before the actual value is set
      if (val < lowerLimit && val != DateTime.MinValue) {
        throw new ArgumentException("Invalid DateTime value in configuration");
      }
    }
  }

  [AttributeUsage(AttributeTargets.Property)]
  public class DateTimeValidatorAttribute : ConfigurationValidatorAttribute {
    public DateTimeValidatorAttribute() {
    }

    public override ConfigurationValidatorBase ValidatorInstance {
      get {
        return new DateTimeValidator(new DateTime(minYear, 1, 1));
      }
    }

    private int minYear = 1900;
    public int MinimumYear {
      get { return minYear; }
      set { minYear = value; }
    }
  }
}

AppConfigSection.cs

namespace ConfigurationDemoPart3 {
  public class AppConfigSection : ConfigurationSection {

    [ConfigurationProperty("startDate", IsRequired=true),
     DateTimeValidator(MinimumYear=2000)]
    public DateTime StartDate {
      get { return (DateTime)this["startDate"]; }
      set { this["startDate"] = value; }
    }

    [ConfigurationProperty("endDate", IsRequired=true),
     DateTimeValidator()]
    public DateTime EndDate {
      get { return (DateTime)this["endDate"]; }
      set { this["endDate"] = value; }
    }

    protected override void PostDeserialize() {
      if (StartDate > EndDate) {
        throw new ArgumentException("EndDate must be after StartDate");
      }
      base.PostDeserialize();
    }
  }
}

Program.cs

namespace ConfigurationDemoPart3 {
  class Program {
    static void Main(string[] args) {
      var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
      ShowSection(config, "custom");
      ShowSection(config, "custom2");
    }

    private static void ShowSection(Configuration config, string secName) {
      try {
        var customSection = config.GetSection(secName) as AppConfigSection;

        Console.WriteLine("Config section \"{2}\": start = {0:d}, end = {1:d}", customSection.StartDate, customSection.EndDate, secName);

      } catch (ConfigurationErrorsException e) {
        var inner = e.InnerException;
        Console.WriteLine("Loading config section \"{0}\" failed: {1} (inner type {2})",
          secName, e.Message, inner == null ? "<None>" : inner.GetType().Name);
      }
    }
  }
}

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
  <section name="custom"
       type="ConfigurationDemoPart3.AppConfigSection, ConfigurationDemoPart3" />
  <section name="custom2"
       type="ConfigurationDemoPart3.AppConfigSection, ConfigurationDemoPart3" />
  </configSections>

  <custom startDate="2000-05-20" endDate="2010-05-22" />
  <custom2 startDate="2010-05-20" endDate="2009-05-22" />
</configuration>

Previous Part of This Series on .NET Custom Configuration

  1. Demonstrating Custom Configuration Sections for .NET (Part 1)
  2. Demonstrating Custom Configuration for .NET (Part 2): Custom Elements