The goal of this sample is to create an implementation of data transfer (value) object validation that performs basic validation of the value of each of an object's properties. There was a requirement to validate the entire object and pass back a collection of validation messages rather than fail after the first violation.
The more practical reasoning for this sample is to perform validation on objects passed between a client and server. I wanted to enable some rudimentary validation to be transfered permitted on the value objects. Some may argue that this violated the definition of a data transfer object (or value object), but I would argue that this degree of validation can remain no less a violation than the data type declaration of the property but adds the significant benefit of reducing network calls as the client can perform this validation prior to making a network call.
Sample usage:
MyDataTransferObject dataTransferObject = new MyDataTransferObject();
try {
Validator.Validate<MyDataTransferObject>(dataTransferObject);
} catch (ValidationException validationException) {
foreach(ValidationViolation violation in
validationException.ValidationViolations) {
}
}
public class MyDataTransferObject {
const string PostalCodeViolationMessage = ;
const string PostalCodeRegexPattern = ;
const string RatingViolationMessage = ;
const int RatingMaxValue = 10;
const int RatingMinValue = 1;
string postalCode = String.Empty;
int rating;
[RegexValidation(PostalCodeViolationMessage,
PostalCodeRegexPattern, RegexOptions.None)]
public string PostalCode {
get { return postalCode; }
set { postalCode = value; }
}
[IntegerValidation(RatingViolationMessage,
RatingMaxValue, RatingMinValue)]
public int Rating {
get { return rating; }
set { rating = value; }
}
}
The Code:
public sealed class Validator {
static string ConstraintViolationHasOccurred = Resources.ObjectValidation.ValidationExceptionHasOccurred;
public static void Validate<T>(T item) {
if (item == null) throw new ArgumentNullException();
IList<ValidationViolation> constraintViolations = new List<ValidationViolation>();
foreach (PropertyInfo propertyInfo in item.GetType().GetProperties()) {
foreach (ValidationAttribute validationCondition in
propertyInfo.GetCustomAttributes(typeof(ValidationAttribute), true)) {
if (propertyInfo.CanRead && !validationCondition.IsValid(propertyInfo.GetValue(item, null))) {
constraintViolations.Add(new ValidationViolation(propertyInfo.Name, validationCondition.ViolationMessage));
}
}
}
if (constraintViolations.Count > 0) {
throw new ValidationException(ConstraintViolationHasOccurred, constraintViolations);
}
}
}
[Serializable]
public class ValidationViolation {
string property, violationMessage;
public ValidationViolation(string property, string violationMessage) {
if (String.IsNullOrEmpty(property)) throw new ArgumentNullException();
if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentNullException();
this.property = property;
this.violationMessage = violationMessage;
}
public string Property {
get { return property; }
}
public string ViolationMessage {
get { return violationMessage; }
}
}
public class ValidationException : Exception {
IList<ValidationViolation> validationViolations = new List<ValidationViolation>();
public ValidationException(string message, IList<ValidationViolation> validationViolations)
: base(message) {
if (validationViolations == null) throw new ArgumentNullException();
this.validationViolations = validationViolations;
}
public IList<ValidationViolation> ValidationViolations {
get { return validationViolations; }
}
}
This results in having to accept that the IsValid method will have an object parameter rather than a typed parameter, which would be preferable.
[AttributeUsage(AttributeTargets.Property)]
public abstract class ValidationAttribute : Attribute {
string violationMessage;
public ValidationAttribute(string violationMessage) {
if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentNullException();
this.violationMessage = violationMessage;
}
public abstract bool IsValid(object value);
public string ViolationMessage {
get { return violationMessage; }
}
}
[AttributeUsage(AttributeTargets.Property)]
public class RegexValidationAttribute : ValidationAttribute {
Regex regularExpression;
public RegexValidationAttribute(string violationMessage, string pattern)
: base(violationMessage) {
if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentException();
if (String.IsNullOrEmpty(pattern)) throw new ArgumentException();
regularExpression = new Regex(pattern);
}
public RegexValidationAttribute(string violationMessage, string pattern, RegexOptions options)
: base(violationMessage) {
if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentException();
if (String.IsNullOrEmpty(pattern)) throw new ArgumentException();
regularExpression = new Regex(pattern, options);
}
public override bool IsValid(object value) {
if (!(value is string))
throw new InvalidOperationException(
Resources.ObjectValidation.RegexValidationAttributeOnlyAppliesToStringTypes);
return regularExpression.Matches((string) value).Count == 1;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class IntegerValidationAttribute : ValidationAttribute {
int maxValue, minValue;
public IntegerValidationAttribute(string violationMessage, int maxValue, int minValue)
: base(violationMessage) {
if (minValue > maxValue)
throw new ArgumentException(Resources.ObjectValidation.MinValueMustBeLessThanOrEqualToMaxValue);
this.maxValue = maxValue;
this.minValue = minValue;
}
public override bool IsValid(object value) {
if (!(value is int))
throw new InvalidOperationException(
Resources.ObjectValidation.IntegerValidationAttributeOnlyAppliesToIntegerTypes);
return (int)value <= maxValue && (int)value >= minValue;
}
}
|