Tuesday, July 13, 2010

ValidationAttribute verses the contextual validation

This last week I got tasked with building some validation for the customer object in our new system here at work. Since we are using MVC2 I can use DataAnnotations to do this. Which in most situations would just mean I throw on some attributes like [Required(ErrorMessage="No soup for you")] But not where I work we can’t have one set of required fields no. Each country has a different set of required fields. This means that I not only need to know what the data I am validating is but what context it is validate in.

This means I can’t use

public bool IsValid(object value)

since it has no idea of context. However there is a second overload of IsValid that will work. It took me a bit to understand how this works but it is very nice.

protected ValidationResult IsValid(object value, ValidationContext validationContext)

the ValidationContext gives you access to the object that’s data member you are validating. By using the ObjectType and ObjectInstances of the property you can get access to all the data members of the current object.

Using the above functions It is easy to create a validator that checks to see if a country should or shouldn’t be validate for a given data member and then validate it if I need to.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class RequiredForCountry : ValidationAttribute
{
public string CountryIds { get; set; }
public int TermId { get; set; }

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (string.IsNullOrEmpty(this.CountryIds))
{
throw new System.ApplicationException(string.Format(
"The property {0} is not set up correctly in class {1}",
validationContext.MemberName,
validationContext.ObjectType.FullName));
}
var countryId = GetCountryID(validationContext);
if (CountryIds.Contains(countryId.ToString()))
{
if (value == null)
return new ValidationResult(TermId.ToString());
var strCheck = value as string;
if (strCheck != null && strCheck == string.Empty)
return new ValidationResult(TermId.ToString());
}
return ValidationResult.Success;
}

private int GetCountryID(ValidationContext context)
{
switch (context.ObjectType.Name)
{
case "Customer":
return (context.ObjectInstance as ICustomer).MainAddress.Country.Id;
case "Order":
return (context.ObjectInstance as IOrder).Country.Id;
default:
return -1; // eventually this will throw an error
}
}
}

While the above code isn’t the prettiest code I have ever written it gets the job done and allows me to not have to write custom validation for every country.

No comments: