Friday, May 27, 2011

Fluent Validation remote validation

Recently I was looking for a way to do remote validation with FluentValidation, for an asp.net mvc 3 application. But looking at this discussion I think there is no official way to do this right now. Although Jeremy does seem to have plan for this.

http://fluentvalidation.codeplex.com/discussions/258348


So, I had to create my own remote validator extension to FluentValidation. For those who cannot wait for the official remote validation for fluent validation. Here's how I did it.

//Create a validator, that does nothing on the server side.
public class RemoteValidator : PropertyValidator
{
  public string Url { get; private set; }
  public string HttpMethod { get; private set; }
  public string AdditionalFields { get; private set; }

  public RemoteValidator(string errorMessage, 
                         string action, 
                         string controller, 
                         HttpVerbs HttpVerb = HttpVerbs.Get, 
                         string additionalFields = "")
        : base(errorMessage)
  {
    var httpContext = HttpContext.Current;

    if (httpContext == null)
    {
      var request = new HttpRequest("/", "http://ubasolutions.com", "");
      var response = new HttpResponse(new StringWriter());
      httpContext = new HttpContext(request, response);
    }

    var httpContextBase = new HttpContextWrapper(httpContext);
    var routeData = new RouteData();
    var requestContext = new RequestContext(httpContextBase, routeData);

    var helper = new UrlHelper(requestContext);

    Url = helper.Action(action, controller);
    HttpMethod = HttpVerb.ToString();
    AdditionalFields = additionalFields;
  }

  protected override bool IsValid(PropertyValidatorContext context)
  {
    //This is not a server side validation rule. So, should not effect at the server side.
    return true;
  }
}

//Create a MVC wrapper for the validator.
public class RemoteFluentValidationPropertyValidator : 
                 FluentValidationPropertyValidator
{
  private RemoteValidator RemoteValidator
  {
    get { return (RemoteValidator)Validator; }
  }

  public RemoteFluentValidationPropertyValidator(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.PropertyDescription);
    string message = formatter.BuildMessage(RemoteValidator.ErrorMessageSource.GetString());

    //This is the rule that asp.net mvc 3, uses for Remote attribute. 
    yield return new ModelClientValidationRemoteRule(message, RemoteValidator.Url, RemoteValidator.HttpMethod, RemoteValidator.AdditionalFields);
  }
}

Thats all. Now you'll have to register the Validator inside the applicaition_start in the global.asax file.

           //Note that I'm using NinjectValidatorFactory for injecting Fluent validation into the modelbinders. So, this configuration may differ a little on every system.
            FluentValidationModelValidationFactory validationFactory = (metadata, context, rule, validator) => new RemoteFluentValidationPropertyValidator(metadata, context, rule, validator);

            FluentValidationModelValidatorProvider.Configure(x => x.Add(typeof(RemoteValidator), validationFactory));

            NinjectValidatorFactory ninjectValidatorFactory = new NinjectValidatorFactory(kernel);
            var validationProvider = new FluentValidationModelValidatorProvider(ninjectValidatorFactory);
            validationProvider.Add(typeof(RemoteValidator), validationFactory);
            ModelValidatorProviders.Providers.Add(validationProvider);
            
            DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
            kernel.Load<FluentValidatorModule>();



How to apply the validation Rule?

Ok, now we will be setting rules for validation.
RuleFor(x => x.UserName).NotEmpty().SetValidator(new RemoteValidator("user name already exists", "ValidateUserName", "Account", System.Web.Mvc.HttpVerbs.Post));

Well, that was quite simple wasn't it?

Here is the code for ValidateUserName Action inside Account controller
        public ActionResult ValidateUserName(string username)
        {
            var user = UserRepository.FindSingleByUsername(username);
            if (user == null)
                return Json(true, JsonRequestBehavior.AllowGet);
            return Json("Username already exists", JsonRequestBehavior.AllowGet);
        }

Thats it folks. Your remote validation is ready to go.

N.B.
You can see that this is just a wrapper around Asp.net mvc's Remote Attribute, and works more or less on same way the Remote attribute would work.

Asp.net mvc 3 unobtrusive validation fires validation on every keyup event. So, I had to change jquery.validate.unobtrusive.js to fire validation only on blur events. I still could not find a reliable way to do this only on the elements with remote validation, since they(microsoft) are doing validation at the form level instead of element level. To be able to set individual settings, a massive rewrite of jquery.validate.unobtrusive.js may be required.

Update May 11, 2012

For all those looking for the change in the way unobtrusive validator works, I will point you to this stack overflow question: http://stackoverflow.com/questions/4798855/mvc-3-specifying-validation-trigger-for-remote-validation

I used the technique mentioned in this answer: http://stackoverflow.com/a/4836653

2 comments:

  1. What did you change in the unobstrusive.js to fire on blur events instead of keyup event?

    ReplyDelete
  2. Sorry, for late reply. As you can see, I haven't been active on my blog since almost a year. Instead of posting code here, I'll point you to the code that I used.

    http://stackoverflow.com/questions/4798855/mvc-3-specifying-validation-trigger-for-remote-validation

    Thanks,
    Nripendra

    ReplyDelete