FluentValidation is “A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.” It is quite awesome. However, the provided client-side validation is limited: “any rules defined using a condition (with When/Unless), custom validators, or calls to Must will not run on the client side.” Below is some my first take on using FluentValidation on both server and client-side.
First, you need to install the nuget package. In our case, it is FluentValidation.MVC4.
We needed to make a certain property required, but only when some other property is true. To achieve this on client-side (server-side is trivial), we added these:
public class RequiredIfClientSideValidator : PropertyValidator
{
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfClientSideValidator(string errorMessage, string dependentProperty, object targetValue)
: base(errorMessage)
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
protected override bool IsValid(PropertyValidatorContext context)
{
//This is not a server side validation rule. So, should not effect at the server side.
return true;
}
}
And this
public class RequiredIfClientSideFluentPropertyValidator : FluentValidationPropertyValidator
{
private RequiredIfClientSideValidator RequiredIfClientSideValidator
{
get { return (RequiredIfClientSideValidator)Validator; }
}
public RequiredIfClientSideFluentPropertyValidator(ModelMetadata metadata,
ControllerContext controllerContext,
PropertyRule propertyDescription,
IPropertyValidator validator)
: base(metadata, controllerContext, propertyDescription, validator)
{
ShouldValidate = false;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (!ShouldGenerateClientSideRules()) yield break;
var formatter = new MessageFormatter().AppendPropertyName(Rule.GetDisplayName());
string message = formatter.BuildMessage(RequiredIfClientSideValidator.ErrorMessageSource.GetString());
var rule = new ModelClientValidationRule()
{
ValidationType = "requiredif",
ErrorMessage = message
};
string depProp = BuildDependentPropertyId(Metadata, ControllerContext as ViewContext);
// find the value on the control we depend on;
// if it's a bool, format it javascript style
// (the default is True or False!)
string targetValue = (RequiredIfClientSideValidator.TargetValue ?? "").ToString();
if (RequiredIfClientSideValidator.TargetValue.GetType() == typeof(bool))
targetValue = targetValue.ToLower();
rule.ValidationParameters.Add("dependentproperty", depProp);
rule.ValidationParameters.Add("targetvalue", targetValue);
yield return rule;
}
private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
// build the ID of the property
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(RequiredIfClientSideValidator.DependentProperty);
// unfortunately this will have the name of the current field appended to the beginning,
// because the TemplateInfo's context has had this fieldname appended to it. Instead, we
// want to get the context as though it was one level higher (i.e. outside the current property,
// which is the containing object (our Person), and hence the same level as the dependent property.
var thisField = metadata.PropertyName + "_";
if (depProp.StartsWith(thisField))
// strip it off again
depProp = depProp.Substring(thisField.Length);
return depProp;
}
}
Additionally, we introduced a validator for simple client-side validation (ClientSideValidator) where you just need to pass an error message to your JavaScript validation.
public class ClientSideValidator : PropertyValidator
{
public string JavascriptValidationAdapterName { get; set; }
public ClientSideValidator(string errorMessage, string javascriptValidationFunctionName)
: base(errorMessage)
{
JavascriptValidationAdapterName = javascriptValidationFunctionName;
}
protected override bool IsValid(PropertyValidatorContext context)
{
//This is not a server side validation rule. So, should not effect at the server side.
return true;
}
}
And an accompanying class:
public class ClientSideFluentValidator : FluentValidationPropertyValidator
{
private ClientSideValidator ClientSideValidator
{
get { return (ClientSideValidator)Validator; }
}
public ClientSideFluentValidator(ModelMetadata metadata,
ControllerContext controllerContext,
PropertyRule propertyDescription,
IPropertyValidator validator)
: base(metadata, controllerContext, propertyDescription, validator)
{
ShouldValidate = false;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (!ShouldGenerateClientSideRules()) yield break;
var formatter = new MessageFormatter().AppendPropertyName(Rule.GetDisplayName());
string message = formatter.BuildMessage(ClientSideValidator.ErrorMessageSource.GetString());
var rule = new ModelClientValidationRule()
{
ValidationType = ClientSideValidator.JavascriptValidationAdapterName,
ErrorMessage = message
};
yield return rule;
}
}
Register your custom validators in Global.asax.cs:
FluentValidationModelValidationFactory requiredIfClientSideValidationFactory =
(metadata, context, rule, validator) =>
new RequiredIfClientSideFluentPropertyValidator(metadata, context, rule, validator);
FluentValidationModelValidationFactory clientSideValidationFactory =
(metadata, context, rule, validator) =>
new ClientSideFluentValidator(metadata, context, rule, validator);
FluentValidationModelValidatorProvider.Configure(provider =>
{
provider.Add(typeof(RequiredIfClientSideValidator), requiredIfClientSideValidationFactory);
provider.Add(typeof(ClientSideValidator), clientSideValidationFactory);
});
Don’t forget to add the JavaScript part:
For requiredif validation:
$.validator.addMethod('requiredif',
function (value, element, parameters) {
var id = '#' + parameters['dependentproperty'];
// get the target value (as a string,
// as that's what actual value will be)
var targetvalue = parameters['targetvalue'];
targetvalue =
(targetvalue == null ? '' : targetvalue).toString();
// get the actual value of the target control
// note - this probably needs to cater for more
// control types, e.g. radios
var control = $(id);
var controltype = control.attr('type');
var actualvalue =
controltype === 'checkbox' ?
control.attr('checked').toString() :
control.val();
// if the condition is true, reuse the existing
// required field validator functionality
if (targetvalue.toLowerCase() === actualvalue.toLowerCase())
return $.validator.methods.required.call(
this, value, element, parameters);
return true;
}
);
$.validator.unobtrusive.adapters.add(
'requiredif',
['dependentproperty', 'targetvalue'],
function (options) {
options.rules['requiredif'] = {
dependentproperty: options.params['dependentproperty'],
targetvalue: options.params['targetvalue']
};
options.messages['requiredif'] = options.message;
});
And for propertycvalidator:
$.validator.addMethod('propertycvalidator', function (value, element, parameters) {
//put your client-side validation logic here
return true;
});
$.validator.unobtrusive.adapters.addBool('propertycvalidator');
And, now we are ready to create our model validator:
public class MyValidator()
{
//Make SomeProperty required. It will be validated on server and client side.
RuleFor(m => m.SomeProperty)
.NotEmpty().WithMessage("Some Property is required");
//SomeOtherProperty will be validated on server side - it is required when PropertyB is true.
//Client side validation is done by using RequiredIfClientSideValidator and passing dependent property
//name to unubtrusive JavaScript validation
RuleFor(m => m.SomeOtherProperty)
.NotNull()
.WithMessage("Some other property is required")
.When(model => model.PropertyB)
.SetValidator(new RequiredIfClientSideValidator(
"Some other property is required",
"PropertyB",
true));
//this will get validated on client side only
RuleFor(m => m.PropertyC)
.SetValidator(
new ClientSideValidator(
"Error message",
"propertycvalidator")); // must be lower case
}
The last part is adding the attribute to your model:
[Validator(typeof(MyValidator))]
public class YourModel
{
}