Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where do you put your validation in asp.net mvc 3?

One common recommended practice in asp.net mvc is that you should not send your business models to your views.. instead you should create viewmodels specific to each view.

When that is done and you call the ModelState.IsValid method in your controller you are effectively checking the validity of the viewmodel but not the business object.

What is the conventional approach to dealing with this?

public class Person
{
public int ID {get; set;};

[Required]
public string Name {get; set;}

[Required]
public string LastName {get; set;}

public virtual ICollection<Exam> Exams {get; set;}

}

public class PersonFormViewModel
{

public int ID {get; set;};    


[Required]
public string Name {get; set;}

[Required]
public string LastName {get; set;}

}

This is exactly what I have right now but Im not sure if the [Required] attribute should appear on both models or just the ViewModel or just the Business Model.

Any tips on this issue are appreciatedd.

More links to support my claim that it is a common good practice to always use view models.

How to add validation to my POCO(template) classes

http://blogs.msdn.com/b/simonince/archive/2010/01/26/view-models-in-asp-net-mvc.aspx

like image 837
nacho10f Avatar asked Jul 01 '11 21:07

nacho10f


2 Answers

My preference is to do input validation on the view models, and business validation on the domain models.

In other words, any data annotations such as required fields, length validation, regex, etc should be done on your view models, and added to the model state when error occurs.

And you'll probably have business/domain rules that rely on more than just a "form", so you should do that either in the domain models (execute the validation after they're mapped back), or with a service layer.

All our models have a method called "Validate", which we call in the services prior to persisting. They throw custom exceptions if they fail business validation, which gets caught by the controller and also added to the model state.

May not be everyone's cup of tea, but it's consistent.

Example of business validation, as requested:

Here's an example of a domain model we have, which represents a generic "Post" (question, photo, video, etc):

public abstract class Post
{
   // .. fields, properties, domain logic, etc

   public void Validate()
   {
      if (!this.GeospatialIdentity.IsValidForThisTypeOfPost())
         throw new DomainException(this, BusinessException.PostNotValidForThisSpatial.);
   }
}

You see there, I am checking against business rules, and throwing custom exceptions. DomainException is our base, and we have many derived implementations. We have an enum called BusinessException, which contains values for all our exceptions. We use extension methods on the enum to provide the resource-based error message.

This is not simply a field on the model I'm checking, e.g "All posts must have a subject", because although that is part of the domain, it's input validation first and foremost, and thus is handled via the data annotations on the view model.

Now, the controller:

[HttpPost]
public ActionResult Create(QuestionViewModel viewModel)
{
   if (!ModelState.IsValid)
     return View(viewModel);

   try
   {
      // Map to ViewModel
      var model = Mapper.Map<QuestionViewModel,Question>(viewModel);

      // Save.
      postService.Save(model); // generic Save method, constraint: "where TPost: Post, new()".

      // Commit.
      unitOfWork.Commit();

      // P-R-G
      return RedirectToAction("Index", new { id = model.PostId });
   }
   catch (Exception exc) 
   {
      var typedExc = exc as DomainException;

      if (typedExc != null)
      {
         // Internationalised, user-friendly domain exception, so we can show
         ModelState.AddModelError("Error", typedExc.BusinessError.ToDescription());
      }
      else
      { 
         // Could be anything, e.g database exception - so show generic msg.
         ModelState.AddModelError("Error", "Sorry, an error occured saving the Post. Support has been notified. Please try again later.");
      }
   }

   return View(viewModel);
}

So, by the time we get to the "Save" method on the service, the model has passed input validation. Then the Save method calls post.Validate(), invoking business rules.

If an exception is raised, the controller catches it and displays the message. If it gets pass the Save method and another error occurs (90% of the time, it's Entity Framework, for example), we show a generic error message.

As I said, not for everyone, but this works well for our team. We have a clear separation of presentation and domain validation, and a consistent flow of control from the raw HTTP POST, to the redirect after success.

like image 60
RPM1984 Avatar answered Nov 01 '22 16:11

RPM1984


The MetaData "buddy" class is exactly what this is for. The validation is created once but can be used on both the model and the viewmodel classes:

public class PersonMetaData
{
  [Required] 
  public string Name {get; set;}  

  [Required] 
  public string LastName {get; set;} 
}

[MetadataType(typeof(PersonMetaData))]
public class Person
{
  public string Name {get; set;}  
  public string LastName {get; set;} 
}

[MetadataType(typeof(PersonMetaData))]
public class PersonFormViewModel 
{
  public string Name {get; set;}  
  public string LastName {get; set;} 
}
like image 25
HitLikeAHammer Avatar answered Nov 01 '22 17:11

HitLikeAHammer