Let's start this installment of the ETElevate development blog by talking about validation error messages.

Right now, our validator implementations return a simple true/false value indicating success or failure of the validation test. An example for max length validation can be seen below:

public class MaxLengthValidator : IValidator
{
	public int MaxCharacterCount { get; private set; }

	public MaxLengthValidator(int maxCharacterCount)
	{
		MaxCharacterCount = maxCharacterCount;
	}

	public bool Check(string value)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return true;
		}

		return value.Length <= MaxCharacterCount;
	}
}

This tells us whether or not the value is valid, but it doesn't give us any meanginful information to report to the end user or to write to an output report. We want to produce error messages that are intelligible and enable troubleshooting, and we want to make those error messages available to whomever is consuming the output of our process. In the absense of good error messages, the user is forced to locate the failed value and attempt to manually derive the cause for failure.

Knowing that we want to produce error messages, the question we need to answer is, "What component has the right information for generating the error message." At this point, the validator itself has the most information about the failure. It knows what the valid parameters are and what the current value is. Therefore, we will update our Check method to produce a more complex result which contains both the validity status and any error message that is produced. The simplest possible implementation of this type of result object would look something like this:

public class ValidationResult
{
	public string ErrorMessage { get; set; }
	public bool IsValid { get { return string.IsNullOrEmpty(ErrorMessage); } }

	public ValidationResult()
	{
	}

	public ValidationResult(string errorMessage)
	{
		if (string.IsNullOrEmpty(errorMessage))
		{
			throw new ArgumentException("errorMessage cannot be null or empty.");
		}

		ErrorMessage = errorMessage;
	}
}

Note: this class only provides the capability for a single error message. We will see if multiple error messages become necessary later and modify our code accordingly.

Our MaxLengthValidator class now implements the following Check method:

public ValidationResult Check(string value)
{
	if (string.IsNullOrWhiteSpace(value))
	{
		return new ValidationResult();
	}

	return value.Length <= MaxCharacterCount
		? new ValidationResult()
		: new ValidationResult($"Character length of {value.Length} exceeds maximum length of {MaxCharacterCount}");
}

Remember, when we construct ValidationResult instances without an error message, it is considered a valid result. The interface for IValidator also needs to change so that our Check method can return a ValidationResult:

public interface IValidator
{
	ValidationResult Check(string value);
}

One shortcoming of this method is that it is not very flexible in terms of IValidator implementation. Since we can only return a single ValidationResult instance, we are prevented from doing things like implementing composite validators that group multiple validators and produce multiple validation results. This could be solved by either adding more functionality to the ValidationResult class or by passing an object interface like IValidationResultBuilder to the Check() method. We could then allow Check() to report as many ValidationResult instances as necessary by calling back to the IValidatorResultBuilder. There are other choices too, like making the result an IList instead of a single ValidationResult.

All of these options are valid to consider here, but we don't yet have the requirements to help us decide on what is the best option, so we will leave that discussion for another time when we have requirements that force us to reevaluate the current implementation.

Unfortunately, this is a pretty nasty refactoring because we have to udpate all of our IValidator implementations and we need to update all the tests related to those implementations. Most of this is simple search and replace and it won't take too long, but it's a bit tedious. At least we will have a nice test suite which can validate that all of our implementation changes were performed correctly when we're finished with refactoring the tests.

Lastly, we will add some tests to validate the behavior of the ValidationResult object:

[TestFixture]
public class ValidationResultTests
{
	private const string testErrorMessage = "Test error message";

	[Test]
	public void WhenNoErrorMessage_IsValidIsTrue()
	{
		var result = new ValidationResult();
		Assert.IsTrue(result.IsValid);
	}

	[Test]
	public void WhenErrorMessage_IsValidIsFalse()
	{
		var result = new ValidationResult(testErrorMessage);
		Assert.IsFalse(result.IsValid);
	}

	[Test]
	public void WhenErrorMessage_ErrorMessageIsSet()
	{
		var result = new ValidationResult(testErrorMessage);
		Assert.AreEqual(testErrorMessage, result.ErrorMessage);            
	}

	[Test]
	public void WhenConstructedWithNullOrEmptyErrorMessage_ArgumentExceptionIsThrown()
	{
		Assert.Throws(() => new ValidationResult(null));
		Assert.Throws(() => new ValidationResult(string.Empty));
	}
}

Next, let's talk about how we can open up our validator code so that we don't have to change existing code every time we create a new type of validator. This idea of being able to extend the system capabilities without modifying existing code is called the Open-Closed Principle. You may recall from earlier posts that we have a static switch statement which is responsible for instantiating validators inside our ValidatorFactory object:

switch (validatorSpec.Type)
{
	case ValidatorType.Required:
		return new RequiredValidator();

	case ValidatorType.MinLength:
		var minCharacterCount = GetIntParameter(validatorSpec, "MinCharacterCount");
		return new MinLengthValidator(minCharacterCount);

	case ValidatorType.MaxLength:
		int maxCharacterCount = GetIntParameter(validatorSpec, "MaxCharacterCount");
		return new MaxLengthValidator(maxCharacterCount);

	case ValidatorType.Format:
		string formatRegexPattern = GetStringParameter(validatorSpec, "FormatRegexPattern");
		return new FormatValidator(formatRegexPattern);

	case ValidatorType.Code:
		var codeListParam = GetStringParameter(validatorSpec, "CodeList");
		var codeList = codeListParam.Split(",").ToList();
		return new ValidCodeContentValidator(codeList);

	case ValidatorType.Date:
		var dateFormat = GetStringParameter(validatorSpec, "DateFormat");
		var culture = new CultureInfo(GetStringParameter(validatorSpec, "CultureName"));
		var minDate = GetDateTimeParameter(validatorSpec, "MinDate");
		var maxDate = GetDateTimeParameter(validatorSpec, "MaxDate");
		return new ValidDateContentValidator(dateFormat, culture, minDate, maxDate);

	default:
		throw new ArgumentException($"Unable to create validator instance for type: {validatorSpec.Type}");
}

This code knows about all types of validators and contains the logic to transform their configuration spec into a runnable validator object. This is helpful since it separates the creation of our validators from the execution of them and gives us flexibility for changing how they are instantiated without impacting their consumption. However, its static implementation has the severe limitation that it requires a code update every time a new validator is implemented. Furthermore, it requires an update in a block of code that impacts every other type of validator. This code, which could create impacts to large portions of the system with small changes, is something that we want to avoid. We also want to provide our end users the ability to implement and use their own validator types, so we need to find a way to support that extensibility feature.

Our solution will be to transform ValidatorFactory from a static factory class into a dynamic registry. We will convert the switch case from a code structure into a data structure and that will give us the flexibility to add any type of validator in the future.

The first thing to recognize is that each case statement has a standard format:

  1. There is a constant value which is used to choose the logic for constructing the validator.
  2. Each block is completely isolated, but each block does use common utility functions to make the initialization code easier to write and maintain.
  3. Each block takes a validatorSpec as an input parameter.
  4. Each block of validator construction logic returns a result which implements IValidator.

Given these aspects of our current logic, we can define a new interface which represents the construction logic for a specific validator type:

interface IValidatorFactory
{
	IValidator CreateValidator(ValidatorSpec validatorSpec);
}

An implementation of this interface for our MaxLengthValidator may look like this:

public class MaxLengthValidatorFactory : IValidatorFactory
{
	public IValidator CreateValidator(ValidatorSpec validatorSpec)
	{
		var validatorSpecReader = new ValidatorSpecReader();
		int maxCharacterCount = validatorSpecReader.GetIntParameter(validatorSpec, "MaxCharacterCount");
		return new MaxLengthValidator(maxCharacterCount);
	}
}

This class implements the responsibility of taking a validatorSpec and converting it into a runnable IValidator instance. Note that we have moved the utility functions like GetIntParameter into a common reusable class called ValidatorSpecReader.

The next step is to implement these IValidatorFactory objects for each type of validator, and then add them to a registry which will replace our current statically implemented ValidatorFactory. The registry class will allow us to associate a string constant with the type of factory to use. This enables us to dynamically link the validator string constant with the logic required to instantiate the validator.

public class ValidatorRegistry
{
	private Dictionary validatorFactories = new Dictionary();
	
	public void Register(string validatorTypeName, IValidatorFactory validatorFactory)
	{
		if (validatorFactories.ContainsKey(validatorTypeName))
		{
			throw new ArgumentException($"A registration already exists for validatorTypeName {validatorTypeName}.");
		}

		validatorFactories[validatorTypeName] = validatorFactory;
	}

	public IValidator CreateValidator(ValidatorSpec validatorSpec)
	{
		if (!validatorFactories.ContainsKey(validatorSpec.Type))
		{
			throw new ArgumentException($"A registration validatorTypeName {validatorSpec.Type} does not exist.");
		}

		return validatorFactories[validatorSpec.Type].CreateValidator(validatorSpec);
	}
}

Our registry object allows us to register an IValidatorFactory by type name and then use CreateValidator to instantiate any validator type that has been registered.

Since we need to offer an interface which end users can call to register their own validator types, we will remove the dependency on the ValidatorType enum. Instead, we will use string constants. This enables end users to expand the valid constants without modifying our enum code.

public static class ValidatorType
{
	public const string None = "None";
	public const string Required = "Required";
	public const string MinLength = "MinLength";
	public const string MaxLength = "MaxLength";
	public const string Format = "Format";
	public const string Content = "Content";
	public const string Code = "Code";
	public const string Date = "Date";
}

A few quick tests of the registry functionality demonstrate how it will be used in practice:

[Test]
public void CanRegisterAndCreateValidatorWithoutSpecParameters()
{
	var registry = new ValidatorRegistry();
	registry.Register(ValidatorType.Required, new RequiredValidatorFactory());

	var spec = new ValidatorSpec
	{
		Type = ValidatorType.Required
	};

	var validator = registry.CreateValidator(spec);

	Assert.NotNull(validator);
	Assert.IsTrue(validator is RequiredValidator);
}

[Test]
public void CanRegisterAndCreateValidatorWithSpecParameters()
{
	var registry = new ValidatorRegistry();
	registry.Register(ValidatorType.MaxLength, new MaxLengthValidatorFactory());

	var spec = new ValidatorSpec
	{
		Type = ValidatorType.MaxLength,
		Parameters =
		{
			new ValidatorSpecParameter{ Name = "MaxCharacterCount", Value = "5" }
		}
	};

	var validator = registry.CreateValidator(spec);

	Assert.NotNull(validator);
	Assert.IsTrue(validator is MaxLengthValidator);
	Assert.IsFalse(validator.Check("123456").IsValid);
}

In our first test, we register the simplest possible type of validator factory which requires no parameters to instantiate a validator. In the second test, we demonstrate that a more complex validator which requires a parameter in its configuration can also be registered and instantiated.

Finally, we will need a default configuration for our ValidatorRegistry. This provides a method for us to initialize ValidatorRegistry with all the validators that are standard in ETElevate. This is the final piece that turns our switch case statement from code into data:

public class DefaultValidatorRegistryInitializer
{
	public void Initialize(ValidatorRegistry registry)
	{
		registry.Register(ValidatorType.Required, new RequiredValidatorFactory());
		registry.Register(ValidatorType.MinLength, new MinLengthValidatorFactory());
		registry.Register(ValidatorType.MaxLength, new MaxLengthValidatorFactory());
		registry.Register(ValidatorType.Format, new FormatValidatorFactory());
		registry.Register(ValidatorType.Content, new CodeValidatorFactory());
		registry.Register(ValidatorType.Date, new DateValidatorFactory());
	}
}

Now we can register any type code (that is not in use already) with any validator factory, which means that we can register validators that are created by end users of ETElevate.

We can remove the ValidatorFactory static implementation at this point. We have replaced the functionality in that class with a set of smaller classes and the dynamically configurable registry.

The next major validation enhancement that is required is to implement multi-field validators which can evaluate multiple fields in comparison with each other. We will implement that in the next post.

Browse the GitHub Repository at this point in its commit history

ETElevate GitHub Repository Home

Thank you for reading!