Using FluentValidation with ASP.NET MVC Opinionated Input Builders

I've been following Eric Hexter's series on using opinionated input builders with ASP.NET MVC and wanted to see whether it would be possible to use his InputBuilder code together with FluentValidation.

In Eric's example, he has a class called SampleModel whose properties are decorated by several attributes:

public class SampleModel
{
  public Guid Guid { get; set; }
 
  [Required]
   public string Name { get; set; }
 
  [Required]
  [Label("Number of Types")]
  [PartialView("RadioButtons")]
  public NumberOfTypeEnum EnumAsRadioButton { get; set; }
 
  public NumberOfTypeEnum Enum { get; set; }
 
  [Range(3, 15)]
  public int IntegerRangeValue { get; set; }
 
  [Example("mm/dd/yyyy hh:mm PM")]
  public DateTime TimeStamp { get; set; }
 
  [DataType(DataType.MultilineText)]
  public string Html { get; set; }
 
  public bool IsNeeded { get; set; }
}

These properties on this object are then rendered in the view by using the strongly-typed Input extension:

<%=Html.Input(c => c.Name)%>
<%=Html.Input(c => c.TimeStamp)%>
<%=Html.Input(c => c.Html)%>
...etc...

The attributes that decorate the properties drive the UI, so for example decorating the Html property with [DataType(DataType.MultilineText)] would cause a multi-line textbox to be rendered.

Rather than using attributes, my aim was to configure the InputBuilders by storing metadata alongside validation information in a Validator class.

Out of the box, FluentValidation supports several validator types. Some of these map nicely to the DataAnnotations attributes used by the SampleModel. For example, the [Required] attribute maps nicely to the NotNull and NotEmpty validators, and the [Range] attribute works nicely with the GreaterThan/LessThan validators:

public class SampleModelValidator : AbstractValidator<SampleModel> {
  public SampleModelValidator() {
    RuleFor(x => x.Name).NotEmpty();
 
    RuleFor(x => x.IntegerRangeValue)
      .GreaterThanOrEqualTo(3)
      .LessThanOrEqualTo(15);
  }
}

However, there are no equivalents for the metadata attributes such as [PartialView], [Example] or [DataType], but these can easily be built by constructing some 'fake' validators (these validators won't actually perform any validation - they will simply hold metadata).

For example, to build an equivalent for the DataTypeAttribute, we can create a property validator like this:

public interface IDataTypeMetaData {
	DataType DataType { get; }
}
 
public class DataTypeValidator<T, TProperty> : IPropertyValidator<T, TProperty>, IDataTypeMetaData {
	private DataType datatype;
 
	public DataTypeValidator(DataType datatype) {
		this.datatype = datatype;
	}
 
	public PropertyValidatorResult Validate(PropertyValidatorContext<T, TProperty> context) {
		return PropertyValidatorResult.Success();
	}
 
	public DataType DataType {
		get { return datatype; }
	}
}

Note that we declare both a validator to hold the DataType information and also a non-generic interface (which we'll use later to obtain the metadata). The Validate method is a no-op, always reporting success (as we don't actually want to perform any validation).

Next, we can declare an extension method that allows this to be used from within our SampleModelValidator:

public static class ValidatorExtensions {
	public static IRuleBuilderOptions<T, TProperty> DataType<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, DataType dataType) {
		return ruleBuilder.SetValidator(new DataTypeValidator<T, TProperty>(dataType));
	}
}

After using this approach to create equivalents for all the other metadata attributes, our SampleModelValidator now looks like this:

public class SampleModelValidator : AbstractValidator<SampleModel> {
	public SampleModelValidator() {
		RuleFor(x => x.Name).NotEmpty();
 
		RuleFor(x => x.EnumAsRadioButton)
			.NotNull()
			.RenderUsing("RadioButtons")
			.WithName("Number of Types");
 
		RuleFor(x => x.IntegerRangeValue)
			.GreaterThanOrEqualTo(3)
			.LessThanOrEqualTo(15);
 
		RuleFor(x => x.TimeStamp).Example("mm/dd/yyyy hh:mm PM");
 
		RuleFor(x => x.Html).DataType(DataType.MultilineText);
	}
}

The next stage is to create a custom ValidatorDescriptor. The default ValidatorDescriptor doesn't do very much so we'll need to inherit from it and create some additional methods to retrieve the information needed by the InputBuilder:

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentValidation;
using FluentValidation.Internal;
using FluentValidation.Validators;
 
public interface ICustomValidatorDescriptor {
	string GetExample(PropertyInfo prop);
	string GetLabel(PropertyInfo prop);
	string GetPartialName(PropertyInfo prop);
	bool GetIsRequired(PropertyInfo prop);
}
 
public class CustomValidatorDescriptor<T> : ValidatorDescriptor<T>, ICustomValidatorDescriptor {
	public CustomValidatorDescriptor(IEnumerable<IValidationRule<T>> ruleBuilders) : base(ruleBuilders) {
	}
 
 
	public string GetExample(PropertyInfo prop) {
		return Rules.OfType<ISimplePropertyRule<T>>()
		       	.Where(x => x.Member == prop)
		       	.Select(x => x.Validator)
		       	.OfType<IExampleMetaData>()
		       	.Select(x => x.Example).FirstOrDefault() ?? string.Empty;
	}
 
	public string GetLabel(PropertyInfo prop) {
		return Rules.OfType<IPropertyRule<T>>()
				.Where(x => x.Member == prop)
				.Select(x => x.PropertyDescription).FirstOrDefault();
	}
 
	public string GetPartialName(PropertyInfo prop) {
		return Rules.OfType<ISimplePropertyRule<T>>()
				.Where(x => x.Member == prop)
				.Select(x => x.Validator)
				.OfType<IRenderUsingMetaData>()
				.Select(x => x.ViewName).FirstOrDefault();
	}
 
	public bool GetIsRequired(PropertyInfo prop) {
		return Rules.OfType<ISimplePropertyRule<T>>()
			.Where(x => x.Member == prop)
			.Select(x => x.Validator)
			.Where(x => x is INotNullValidator || x is INotEmptyValidator)
			.Any();
	}
}

The ValidatorDescriptor accepts in its constructor a collection of IValidationRule objects that represent the rules configured in the SampleModelValidator. Using some Linq magic we can retrieve the metadata information that we stored inside the validation rules. We also have to override the CreateDescriptor method in our SampleModelValidator to return an instance of our custom ValidatorDescriptor, rather than the default:

public override IValidatorDescriptor<SampleModel> CreateDescriptor() {
	return new CustomValidatorDescriptor<SampleModel>(this);
}

The next stage is to override the InputBuilder's default conventions so that it will use our SampleModelValidator to obtain its metadata. To do this, we create a FluentValidationConventions class that accepts a ValidatorFactory in its constructor and contains methods that delegate to our CustomValidatorDescriptor:

public class FluentValidationConventions {
	private IValidatorFactory validatorFactory;
 
	public FluentValidationConventions(IValidatorFactory factory) {
		this.validatorFactory = factory;
	}
 
	private ICustomValidatorDescriptor GetDescriptor(Type type) {
		var validator = validatorFactory.GetValidator(type);
		return (ICustomValidatorDescriptor) validator.CreateDescriptor();
	}
 
	public string ExampleConvention(PropertyInfo prop) {
		return GetDescriptor(prop.ReflectedType).GetExample(prop);
	}
 
	public string LabelConvention(PropertyInfo prop) {
		return GetDescriptor(prop.ReflectedType).GetLabel(prop) ?? DefaultConventions.LabelForProperty(prop);
	}
 
	public string PartialNameConvention(PropertyInfo prop) {
		return GetDescriptor(prop.ReflectedType).GetPartialName(prop) ?? DefaultConventions.PartialName(prop);
	}
 
	public bool RequiredConvention(PropertyInfo prop) {
		return GetDescriptor(prop.ReflectedType).GetIsRequired(prop);
	}
}

Note that I used a very simple implementation of ValidatorFactory for this example that wraps a Dictionary. A real ValidatorFactory would probably wrap an IoC container or a Service Locator:

public class SampleValidatorFactory : IValidatorFactory {
	static Dictionary<Type, IValidator> validators = new Dictionary<Type, IValidator>() {
		{ typeof(SampleModel), new SampleModelValidator() }
	};
 
	public IValidator<T> GetValidator<T>() {
		return (IValidator<T>) GetValidator(typeof (T));
	}
 
	public IValidator GetValidator(Type type) {
		IValidator validator;
		if (validators.TryGetValue(type, out validator)) {
			return validator;
		}
		return null;
	}
}

In our global.asax, we can then replace the InputBuilder's default conventions with our custom conventions:

protected void Application_Start()
{
	RegisterRoutes(RouteTable.Routes);
	InputBuilder.InputBuilder.BootStrap();
	SetupFluentValidationConventions();
}
 
private void SetupFluentValidationConventions() {
	var conventions = new FluentValidationConventions(new SampleValidatorFactory());
 
	ModelPropertyFactory.ExampleForPropertyConvention = conventions.ExampleConvention;
	ModelPropertyFactory.LabelForPropertyConvention = conventions.LabelConvention;
	ModelPropertyFactory.PartialNameConvention = conventions.PartialNameConvention; 
	ModelPropertyFactory.PropertyIsRequiredConvention = conventions.RequiredConvention; 
}

...and that's it. The InputBuilder will now get all its metadata from our Validator class rather than from attributes. If you want to play with the source code for this example it's available in my svn repository at https://www.jeremyskinner.co.uk/svn/public/trunk/FluentValidationInputBuilders (username 'guest') on GitHub and can be downloaded from here.

Written on June 14, 2009