A Different Approach to Validation in ASP.NET MVC Sites

A Different Approach to Validation in ASP.NET MVC Sites

Validation support in modern ASP.NET MVC projects is phenomenal. I love how easy it is to declaratively validate my models through meta-data validation attributes. Not only does the framework easily handle everything once I validate my models, but it also cleanly leverages jQuery validation for simple and unobtrusive client-side validation. For more information on jQuery, see "6 Reasons Why jQuery Is a Great Framework" and "4 More Web Development Tips to Improve Your jQuery Coding."

In fact, I enjoy these validation mechanisms so much that I've used them to tweak the default ASP.NET MVC convention of checking whether my models are valid. Although my reasoning will require some explanation, I think my process results in a cleaner workflow.

An Exception-Driven Approach to Validation

Although it's natural for any .NET developer to freak out about excessive exception use, the reality is that an exception thrown here or there doesn't actually pose any problems. As such, the workflow that I've adopted over recent years argues that if you've got client-side validation working to address most user input problems, then anything that makes it through validation is most likely considered an exception, such as when users don't have JavaScript turned on or when users might be trying to post directly to your server. In my mind, these types of situations should be an exception. These situations aren't unheard of or something to ignore, but they are something that should be an exception to the norm.

If I have complex models whose properties have all been validated, and I run into a validation problem that prevents the combination of input from being valid, then that's something I would consider to an exception in most cases. Over the years I've deviated from the ASP.NET MVC convention of checking to see whether a model is valid through simple if checks. As a result, I've switched to a workflow that I personally think ends up being much easier to work with because it treats validation problems as an exception.

For example, Listing 1 shows a model that represents some overly-simplified data that's needed to sign up with a site where I've defined validation attributes as needed:

public class SignupViewModel : IValidated

{

    [Required(ErrorMessage = "An email address is required")]

    [Email(ErrorMessage = "Please specify a valid email address")]

    public string EmailAddress { get; set; }

 

    [Required(ErrorMessage = "A valid passphrase is required (between 6 and 20 characters long).")]

    [RegularExpression(@".{6,20}", ErrorMessage = "Passphrases must be between 6 and 20 characters long.")]

    public string Passphrase { get; set; }

 

    [Required(ErrorMessage = "Please specify your favorite animal.")]

    public string FavoriteAnimal { get; set; }

 

    public SignupViewModel() {}

}

Listing 1: Model that represents data that's needed to sign up with a site

Listing 2 shows how to validate the Listing 1 model through normal ASP.NET MVC conventions. Validation is done by checking to see whether the model is valid and handling various bits of processing if the model is valid.

[HttpPost]

public ActionResult Signup(SignupViewModel model)

{

    if (model.IsValid)

    {

        try

        {

            // save model to repository:

            this._signupRepository.SaveSignup(

                model.EmailAddress,

                model.GetHashedPassPhrase(),

                model.FavoriteAnimal

            );

 

            // send an email/welcome:

            this._notificationManager.SendWelcomeEmail(

                model.EmailAddress,

                model.FavoriteAnimal

            );

 

            // forms-auth happiness, sign the user in:

            // etc.

 

            // now redirect the user to a 'success' page:

            return RedirectToAction("SignupComplete");

        }

        catch (Exception ex)

        {

            // do something with the exception or type...

            // on a one-off basis per action:

 

            // such as:

            // log non validation errors:

            base._errorLogger.LogException(ex);

 

            TempData["Error"] = "Unexpected error: " + ex.Message;

 

            return View(model);

        }

    }

    else

    {

        TempData["Error"] =

            "Please address the validation errors specified.";

        return View(model);

    }

}

Listing 2: Validating a model through typical ASP.NET MVC conventions

Listing 3 shows the approach that I've settled on over the past few years, which looks a bit different:

[HttpPost]

public ActionResult Signup(SignupViewModel model)

{

    try

    {

        // .validate() throws exceptions on problems:

        model.Validate(this.ModelState, null);

 

        // save model to repository:

        this._signupRepository.SaveSignup(

            model.EmailAddress,

            model.GetHashedPassPhrase(),

            model.FavoriteAnimal

        );

               

        // send an email/welcome:

        this._notificationManager.SendWelcomeEmail(

            model.EmailAddress,

            model.FavoriteAnimal

        );

 

        // forms-auth happiness, sign the user in:

        // etc.

 

        // now redirect the user to a 'succes' page:

        return RedirectToAction("SignupComplete");

    }

    catch (Exception ex)

    {

        base.HandleException(ex);

    }

 

    return View(model);

}

Listing 3: Alternative method for validation in ASP.NET MVC

As you can see in Listing 3, my workflow assumes that everything will work normally unless there's an exception, which will be thrown when I validate my object. In my case, this workflow is supported by the fact that I have a generic exception handler for all of my controller actions, which I put into a SiteController that I use as the base class for all of my controllers. Listing 4 shows the code for this exception handler:

protected void HandleException(Exception ex)

{

    if (ex is ValidationException)

    {

        TempData["Error"] = "Please address the validation errors specified.";

        return;

    }

 

    // log non validation errors:

    this._errorLogger.LogException(ex);

 

    if (ex is PersistenceException)

    {

        TempData["Error"] = "Sorry. Your changes were not saved. Error: " + ex.Message;

        return;

    }

 

    // if we're still here, unhandled exception:

    TempData["Error"] = "Unexpected error: " + ex.Message;

}

Listing 4: Generic exception handler

With this generic exception handler, validation exceptions are simply flagged for end users by spitting out a message that tells them about validation problems, while bigger or uglier problems are logged and handled as needed. The key point is that if any exceptions do occur, then they will be logged or handled as needed and execution of my action methods drop to the bottom where any data that's passed in by the client is sent back out by means of redisplaying the view that they've been working with. If things proceed as expected, then users will be redirected to a confirmation page.

Creating My Own Validation Interface

Of course, if you look at the exception-driven workflow that I've outlined in Listing 3, then you'll see that I'm calling a method called .validate(). This might seem a bit obtuse since ASP.NET MVC 3.0 and later versions expose the IValidatableObject interface. But I prefer my approach because it's pretty easy to implement and provides an extra bit of logic that causes my .validate() method to throw an exception when the model isn't valid. Listing 5 shows an example of how I'm validating my sign up model:

public void Validate(ModelStateDictionary modelState, object validationContext)

{

    // simple validation example:

    if(this.FavoriteAnimal.Equals("Giraffe", StringComparison.InvariantCultureIgnoreCase))

        modelState.AddModelError("FavoriteAnimal", "Sorry. Giraffes aren't allowed.");

 

    if (!modelState.IsValid)

        throw new ValidationException();

 

    this.IsValid = true;

}

Listing 5: Validating the sign up model

Note that I pass an optional object called validationContext that can load with anything I like. In addition the object has the ability to be cast as needed in my validation routine.

A Note about Coupling

An obvious concern with the workflow I've outlined is that it requires tight coupling of my models to ASP.NET MVC logic that's due to the requirement of being able to handle interactions with a ModelStateDictionary. But because I've personally and almost entirely devolved to only using ViewModels (and mapping data back and forth between them to my underlying business models and objects as needed), this works pretty well for most of my projects. However, I also have a similar workflow in which I use a different IValidated interface that expects a List instead, which removes the ASP.NET MVC dependency in favor of a System.ComponentModel.DataAnnotation dependency. This alternative workflow is something that you would need if you're defining validation attributes on your base models or objects. Listing 6 shows this approach with actual validation logic for the new version of .validate():

public void Validate(List<ValidationResult> errors, object validationContext)

{

    // simple validation example:

    if (this.FavoriteAnimal.Equals("Giraffe", StringComparison.InvariantCultureIgnoreCase))

        errors.Add(new ValidationResult("Sorry. Giraffes aren't allowed.", new string[] { "FavoriteAnimal" }));

 

    if (errors.Count > 0)

        throw new ValidationException(errors);

 

    this.IsValid = true;

}

Listing 6: Validation logic for the new version of .validate()

The following line of code shows how to pass in the collection of ValidationResult objects from my controllers where I simply copy out any existing validation errors from ASP.NET MVC's model:

model.Validate(this.ModelState.GetValidationErrors(), null); 

The problem is that I have to reverse copy-out any new errors that have been added through my .validate() method, but that's pretty simple to handle. In fact, I've merely extended ASP.NET MVC's ModelState object a tiny bit with the use of some extension methods that handle both needs.

Download the Sample Code

I've used this approach for several years now. It's not perfect, but nothing ever is. However it's met my needs very well and has made it much easier for me to standardize controller actions and makes my code easier to read. Consequently, visit my website if you'd like a copy of the sample code.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish