Print

Splitting up Validation Application Block configuration into multiple files

This article describes how to build an IConfigurationSource implementation that allows reading multiple configuration files that each contain part of the total Enterprise Library Validation Application Block configuration.

Five months ago I wrote about extracting the Enterprise Library Validation Application Block (VAB for short) configuration to its own file. This is useful in situations where the configuration file gets big or, as I explained in the article, when you want to have unit tests supporting those validations. While you can use the same technique to put the different Enterprise Library blocks in their own configuration file, I want to take this one step further and allow the single VAB configuration to be split up into multiple configuration files. While I haven't tested this, I expect this technique to also work with the other application blocks.

Separating your VAB configuration into multiple files is useful in scenario’s where the configuration gets very big or when dealing with Software as a Service applications. With SaaS you’d usually have multiple customers using the same web service or web application or perhaps even have multiple physical deployments (one per customer) for the same application / code base. Splitting up the configuration is useful when you have different validation requirements among your customers. The idea is to have one base configuration that contains the validations that hold for all customers and multiple specific configurations; one per customer.

It shouldn’t come as an surprise that this scenario isn’t supported out of the box. Despite the fact that the solution will not be a one-liner, it’s great that the extensibility of VAB allows us to actually do this. The trick is to create an implementation of the IConfigurationSource interface that allows loading multiple configuration files and can merge them to one ValidationSettings element which the VAB infrastructure is able to process.

In the code snippet below you can see the usage of the class I called ValidationConfigurationSourceCombiner. It implements IConfigurationSource and therefore allows it to be supplied as input to the validation process. It allows multiple IConfigurationSource instances to be supplied through its constructor. During its creation it will combine the supplied configurations together to one big configuration, by iterating and comparing all elements and sub elements of these configurations.

IConfigurationSource configurationSource =
new ValidationConfigurationSourceCombiner(
new FileConfigurationSource("validation_base.config"),
new FileConfigurationSource("validation_cust_13.config"),
new FileConfigurationSource("validation_cust_56.config")
);

Although the example uses a static list of configuration files, you can also load them dynamically for instance by searching the filesystem for all validation_*.config files, as shown here:

var appDir = AppDomain.CurrentDomain.BaseDirectory;
var pattern = "validation_*.config";

IConfigurationSource configurationSource =
new ValidationConfigurationSourceCombiner(
from fileName in Directory.GetFiles(appDir, pattern)
select new FileConfigurationSource(fileName)
as IConfigurationSource
);

When you create a single combiner that holds all validations for all customers, you will have to differentiate by using customer specific rulesets and inform the validator that only the default and cusomerX rulesets have to be validated. Specifying customer specific rulesets however, can be error prone. Your other option would be to create a combiner per customer. You can store them in a dictionary with the customer id as key and supply the customer’s specific combiner to the VAB validator.

Because the class takes IConfigurationSource instances and implements IConfigurationSource itself, it can by itself be used again as input to yet another combiner. This allows you to do infinitely stacking of instances :-).

Below the code for the ValidationConfigurationSourceCombiner.

public class ValidationConfigurationSourceCombiner
: IConfigurationSource
{
private readonly ValidationSettings combinedSettings;

public ValidationConfigurationSourceCombiner(
params IConfigurationSource[] sources)
: this((IEnumerable<IConfigurationSource>)sources)
{
}

public ValidationConfigurationSourceCombiner(
IEnumerable<IConfigurationSource> sources)
{
string sectionName = ValidationSettings.SectionName;

var settings =
from source in sources
select source.GetSection(sectionName)
as ValidationSettings;

this.combinedSettings =
settings.Aggregate(CombineSettings);
}

public ConfigurationSection GetSection(string sectionName)
{
if (sectionName == ValidationSettings.SectionName)
{
return this.combinedSettings;
}

return null;
}

#region IConfigurationSource Members

// Rest of the IConfigurationSource members left out.
// Just implement them by throwing an exception from
// their bodies; they are not used.

#endregion

private static ValidationSettings CombineSettings(
ValidationSettings left, ValidationSettings right)
{
var valCopy = Copier.MakeCopy(left);

new ValidationSettingsMerger(right).MergeInto(valCopy);

return valCopy;
}
}

As you can see this class doesn’t really do much. The constructor processes the supplied IConfigurationSource instances, by aggregating them down to a single ValidationSettings object and the GetSection method returns that instance. The Enumerable.Aggregate method uses the CombineSettings method, which takes two ValidationSettings instances and produces a new ValidationSettings that contains settings from both the instances. This is done by making a (deep) copy of the left and adding all settings from the right to that copy.

This adding (or merging as I call it in the code) is done by the ValidationSettingsMerger class, which simply iterates over all the items in the settings object. For each item it checks if the target object already contains the item. If the target item is missing, a deep copy of the source item is made and that copy is added to the target’s item collection. When the item already exists, the source item is merged with the target item. This merging is done by executing the operation described in this paragraphs for the item’s sub items.

To complete this story, here is the rest of the implementation.

As always: happy validating!

internal interface IMerger
{
string TypeName { get; }

string Name { get; }

IMerger Parent { get; }
}

internal class ValidationSettingsMerger
{
private readonly ValidationSettings source;

public ValidationSettingsMerger(ValidationSettings source)
{
this.source = source;
}

public void MergeInto(ValidationSettings target)
{
foreach (var sourceType in this.source.Types)
{
var targetType = target.Types.Get(sourceType.Name);

bool typeAlreadyInTarget = targetType != null;

if (typeAlreadyInTarget)
{
var merger = new TypeMerger(this.source, sourceType);
merger.MergeInto(targetType);
}
else
{
target.Types.Add(Copier.MakeCopy(sourceType));
}
}
}
}

internal abstract class ValidationMerger<T> : IMerger
where T : NamedConfigurationElement
{
protected ValidationMerger(ValidationSettings settings,
T sourceElement, IMerger parent)
{
this.SourceSettings = settings;
this.Source = sourceElement;
this.Parent = parent;
}

public abstract string TypeName { get; }

public IMerger Parent { get; private set; }

public string Name { get { return this.Source.Name; } }

protected T Source { get; private set; }

protected ValidationSettings SourceSettings
{
get;
private set;
}
}

internal class TypeMerger
: ValidationMerger<ValidatedTypeReference>
{
public TypeMerger(ValidationSettings sourceSettings,
ValidatedTypeReference sourceType)
: base(sourceSettings, sourceType, null)
{
}

public override string TypeName { get { return "type"; } }

public void MergeInto(ValidatedTypeReference target)
{
this.SetDefaultRuleset(target);

foreach (var sourceRules in this.Source.Rulesets)
{
var targetRules =
target.Rulesets.Get(sourceRules.Name);

if (targetRules != null)
{
this.CreateRulesetMerger(sourceRules)
.MergeInto(targetRules);
}
else
{
targetRules = Copier.MakeCopy(sourceRules);
target.Rulesets.Add(targetRules);
}
}
}

private void SetDefaultRuleset(ValidatedTypeReference target)
{
var sourceRuleset = this.Source.DefaultRuleset;
var targetRuleset = target.DefaultRuleset;

if (String.IsNullOrEmpty(targetRuleset))
{
target.DefaultRuleset = sourceRuleset;
}
else if (String.IsNullOrEmpty(sourceRuleset))
{
// Don't override the target ruleset.
}
else if (sourceRuleset != targetRuleset)
{
this.ThrowDefaultRulesetsDifferException(target);
}
}

private void ThrowDefaultRulesetsDifferException(
ValidatedTypeReference target)
{
// Note: because the merging we loose the Element-
// Information of the target, so we can not use it
// the exception message.
throw new ConfigurationErrorsException(
string.Format(CultureInfo.InvariantCulture,
"The configuration file {0} contains a type " +
"'{1}' that has a DefaultRuleset that differs " +
"from the DefaultRuleset in the type of the " +
"other configuration files. DefaultRuleset " +
"'{2}' was expected but '{3}' was found.",
this.Source.ElementInformation.Source,
target.Name, this.Source.DefaultRuleset,
target.DefaultRuleset));
}

private RulesetMerger CreateRulesetMerger(
ValidationRulesetData sourceRuleset)
{
return new RulesetMerger(this.SourceSettings,
sourceRuleset, this);
}
}

internal class RulesetMerger
: ValidationMerger<ValidationRulesetData>
{
public RulesetMerger(ValidationSettings settings,
ValidationRulesetData sourceRuleset, IMerger parent)
: base(settings, sourceRuleset, parent)
{
}

public override string TypeName { get { return "ruleset"; } }

public void MergeInto(ValidationRulesetData target)
{
var source = this.Source;

this.MergeCollection(source.Fields, target.Fields);
this.MergeCollection(source.Methods, target.Methods);
this.MergeCollection(source.Properties, target.Properties);
this.MergeValidators(source.Validators, target.Validators);
}

private void MergeCollection<TMember>(
NamedElementCollection<TMember> sourceCollection,
NamedElementCollection<TMember> targetCollection)
where TMember : ValidatedMemberReference, new()
{
foreach (var sourceMember in sourceCollection)
{
var targetMember =
targetCollection.Get(sourceMember.Name);

if (targetMember != null)
{
this.CreateMemberMerger(sourceMember)
.MergeInto(targetMember);
}
else
{
targetMember = Copier.MakeCopy(sourceMember);
targetCollection.Add(targetMember);
}
}
}

private void MergeValidators(
ValidatorDataCollection sourceValidators,
ValidatorDataCollection targetValidators)
{
var merger = this.CreateValidatorMerger();
merger.MergeValidatorsInto(
sourceValidators, targetValidators);
}

private MemberMerger<TMemberReference>
CreateMemberMerger<TMemberReference>(
TMemberReference sourceElement)
where TMemberReference : ValidatedMemberReference, new()
{
return new MemberMerger<TMemberReference>(
this.SourceSettings, sourceElement, this);
}

private ValidatorMerger CreateValidatorMerger()
{
return new ValidatorMerger(this.SourceSettings, this);
}
}

internal class MemberMerger<TMember>
: ValidationMerger<ValidatedMemberReference>
where TMember : ValidatedMemberReference, new()
{
public MemberMerger(ValidationSettings sourceSettings,
TMember sourceMember, IMerger parent)
: base(sourceSettings, sourceMember, parent)
{
}

[SuppressMessage("Microsoft.Globalization",
"CA1308:NormalizeStringsToUppercase")]
public override string TypeName
{
get
{
// Allow returning the actual type without
// subclassing the MemberMerger<TMember>.
return typeof(TMember).Name
.Replace("Validated", string.Empty)
.Replace("Reference", string.Empty)
.ToLowerInvariant();
}
}

public void MergeInto(TMember target)
{
var merger = this.CreateValidatorMerger();

merger.MergeValidatorsInto(this.Source.Validators,
target.Validators);
}

private ValidatorMerger CreateValidatorMerger()
{
return new ValidatorMerger(this.SourceSettings, this);
}
}

internal class ValidatorMerger
{
private readonly IMerger parent;
private readonly ValidationSettings settings;

public ValidatorMerger(ValidationSettings settings, IMerger parent)
{
this.parent = parent;
this.settings = settings;
}

public void MergeValidatorsInto(
ValidatorDataCollection sourceValidators,
ValidatorDataCollection targetValidators)
{
foreach (var sourceValidator in sourceValidators)
{
var targetValidator =
targetValidators.Get(sourceValidator.Name);

if (targetValidator != null)
{
this.CreateValidatorMerger().MergeInto(targetValidator);
}
else
{
targetValidator = Copier.MakeCopy(sourceValidator);
targetValidators.Add(targetValidator);
}
}
}

public void MergeInto(ValidatorData target)
{
string parentsInformation =
this.GetValidatorParentsInformation();

throw new ConfigurationErrorsException(String.Format(
CultureInfo.InvariantCulture,
"The configuration file {0} contains a {1} " +
"with name '{2}' that already is defined in " +
"configuration {3}. {4}",
this.settings.ElementInformation.Source,
target.Type.Name, target.Name,
target.ElementInformation.Source,
parentsInformation));
}

private string GetValidatorParentsInformation()
{
var parentsDescription =
from parent in this.GetParents()
select string.Format(CultureInfo.InvariantCulture,
"{0} '{1}'", parent.TypeName, parent.Name);

return string.Format(CultureInfo.InvariantCulture,
"The validator is defined in {0}.",
string.Join(", ", parentsDescription.ToArray()));
}

private IEnumerable<IMerger> GetParents()
{
var parent = this.parent;

while (parent != null)
{
yield return parent;
parent = parent.Parent;
}
}

private ValidatorMerger CreateValidatorMerger()
{
return new ValidatorMerger(this.settings, this.parent);
}
}

internal static class Copier
{
public static ValidationSettings MakeCopy(
ValidationSettings source)
{
var copy = new ValidationSettings();

foreach (var sourceType in source.Types)
{
copy.Types.Add(Copier.MakeCopy(sourceType));
}

return copy;
}

public static ValidatedTypeReference MakeCopy(
ValidatedTypeReference source)
{
var target = new ValidatedTypeReference()
{
AssemblyName = source.AssemblyName,
DefaultRuleset = source.DefaultRuleset,
Name = source.Name,
};

foreach (var sourceRuleset in source.Rulesets)
{
target.Rulesets.Add(Copier.MakeCopy(sourceRuleset));
}

return target;
}

public static ValidationRulesetData MakeCopy(
ValidationRulesetData source)
{
var target = new ValidationRulesetData(source.Name);

Copier.CopyCollection(source.Fields, target.Fields);
Copier.CopyCollection(source.Methods, target.Methods);
Copier.CopyCollection(source.Properties, target.Properties);
Copier.CopyValidators(source.Validators, target.Validators);

return target;
}

public static TMember MakeCopy<TMember>(TMember sourceMember)
where TMember : ValidatedMemberReference, new()
{
var target = new TMember();

target.Name = sourceMember.Name;

foreach (var sourceValidator in sourceMember.Validators)
{
target.Validators.Add(Copier.MakeCopy(sourceValidator));
}

return target;
}

public static ValidatorData MakeCopy(ValidatorData source)
{
// A validator is considered readonly,
// we can simply use the reference.
return source;
}

private static void CopyCollection<TMember>(
NamedElementCollection<TMember> sourceCollection,
NamedElementCollection<TMember> targetCollection)
where TMember : ValidatedMemberReference, new()
{
foreach (var sourceElement in sourceCollection)
{
targetCollection.Add(Copier.MakeCopy(sourceElement));
}
}

private static void CopyValidators(
ValidatorDataCollection sourceValidators,
ValidatorDataCollection targetValidators)
{
foreach (var sourceValidator in sourceValidators)
{
targetValidators.Add(Copier.MakeCopy(sourceValidator));
}
}
}

- .NET General, C#, Enterprise Library, Validation Application Block - three comments / No trackbacks - §

The code samples on my weblog are colorized using javascript, but you disabled javascript (for my website) on your browser. If you're interested in viewing the posted code snippets in color, please enable javascript.

three comments:

Nice post, Steven!

Just noticed that there is small error with the wildcard code:

ValidationConfigurationSourceCombiner constructor takes an array of IConfigurationSource but the LINQ statement is returning an IEnumerable.
Tuzo - 30 03 11 - 23:22

Good catch. I stripped out the wrong constructor. I guess it's time to start writing unit tests for my blog post code :-).
Steven (URL) - 31 03 11 - 00:22

Welcome! Nike Air Max Shoes Pas Cher .FR,Store Online, "Health & Lifestyle" . Sale All kinds of Nike Air Max shoes, with nike air max 90, nike air max 95,nike air max 2012,nike air max 2011,nike air max pas cher etc. http://www.nikeairmaxot.com.
Nike Air Max Shoes (URL) - 18 04 12 - 09:52


No trackbacks:

Trackback link:

Please enable javascript to generate a trackback url


  
Remember personal info?

/

Before sending a comment, you have to answer correctly a simple question everyone knows the answer to. This completely baffles automated spam bots.
 

  (Register your username / Log in)

Notify:
Hide email:

Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.