Condividi tramite


View Model versus Domain Entity Validation with MVC

I blogged way back in January about what I see as the three variants of View Model usage in ASP.NET MVC. If you’ve not read that post, check it out here and report back!

The Problem

What I’ve started to see is that many other developers like my approach of defaulting to Variant 2 (a container view model, enclosing domain/business entities), and moving to Variant 3 (separate view model and domain entity with mapping between the two) if required; i.e. mixed approaches within the same solution. This often leaves you with a domain entity (for my example, a Person entity) that has Data Annotations applied to it, so that when it is enclosed in a View Model and rendered or model-bound MVC picks up on the Annotations and handles the validation for you. Of course, you may then come across a situation where the Person entity is not well suited to being used in the View Model, so you adopt Variant 3 from my View Model post. The result is that Person is used as part of the View Model for some screens, and a separate PersonViewModel is used for other screens.

Enough waffle, let’s see some of the (slightly contrived demo) code I’m referring to;

    1:  public class Person
    2:  {
    3:      public int Id { get; set; }
    4:   
    5:      [StringLength(10)]
    6:      public string Name { get; set; }
    7:   
    8:      [Range(0, 150)]
    9:      [DrivingLicenseCheck(17)]
   10:      public int Age { get; set; }
   11:   
   12:      public bool DrivingLicense { get; set; }
   13:  }

This Person class is my domain entity. For some actions, including HomeController.Index, I just want to display all the information on Person records in my repository, so we use Variant 2 and no mapping;

    1:  public class HomeIndexViewModel
    2:  {
    3:      public IEnumerable<Person> People { get; set; }
    4:  }

However, on the Edit Person action (HomeController.Edit) I don’t want to permit Driving License to be altered or even displayed. This is a contrived example so sounds unrealistic, but trust me that this kind of pattern can arise. So in this case, we decide to go with Variant 3 and create a PersonViewModel class;

    1:  public class HomeEditViewModel
    2:  {
    3:      public PersonViewModel Person { get; set; }
    4:  }
    5:   
    6:  public class PersonViewModel
    7:  {
    8:      public int Id { get; set; }
    9:   
   10:      public string FullName { get; set; }
   11:   
   12:      [Range(0, 150)]
   13:      public int Age { get; set; }
   14:  }

You’ll notice that I have a container View Model, but also the PersonViewModel class. I then create a helper class to map between Person and PersonViewModel;

    1:  public static class PersonMapper
    2:  {
    3:      public static PersonViewModel MapToViewModel(Person person)
    4:      {
    5:          return new PersonViewModel { Id = person.Id, FullName = person.Name, Age = person.Age };
    6:      }
    7:   
    8:      public static void MapToEntity(PersonViewModel viewmodel, Person person)
    9:      {
   10:          if (viewmodel.Id != person.Id)
   11:              throw new InvalidOperationException();
   12:   
   13:          person.Name = viewmodel.FullName;
   14:          person.Age = viewmodel.Age;
   15:      }
   16:  }

The interesting question arises when you notice that not all the validation attributes on Person are reflected in PersonViewModel. For Name, I’ve just not bothered to copy it across so that you can see an example of this in action. But for the Driving License validation there’s no way to translate that into validation to appear on the PersonViewModel, as there is no DrivingLicense property. Imagine on the Edit screen I update someone’s Age from 21 down to 12 – the validation of Age > 16 if DrivingLicense is true will never be run. This is bad – we always want our domain level validation to execute, but how do we invoke that validation if it is using Data Annotations?

A Proposed Solution

Thinking this through, the ideal solution looks something like this;

  1. Form data is POSTed to an Action.
  2. Model Binding performs validation using the Data Annotations on the View Model.
  3. Map the View Model to a Domain Entity (or multiple Entities).
  4. Validate the Domain Entity using its Data Annotations.
  5. If all validation succeeded, apply the changes and redirect to a confirmation screen.
  6. If any validation failed, display the validation error messages to the user and allow them to make changes.

Performing validation of a given entity is fairly easy – the Controller class has a TryValidateModel method that takes a Data Annotations-marked entity and validates it. The problem is that this method then puts any validation failures straight into ModelState. Let’s walk through in our minds what that would mean;

  1. Model Binding validates a PersonViewModel.
  2. We map this PersonViewModel to a Person domain entity instance.
  3. We call ValidateModel on the Person domain entity.
  4. Validation of the Name and DrivingLicense properties on the Person domain entity fails.
  5. Two records are added to ModelState, one with a key of “Name” and one a key of “DrivingLicense”.
  6. The view is re-rendered using the instance of PersonViewModel.
  7. The ValidationSummary has ExcludePropertyErrors set to true, so it ignores these two property validation failure records.
  8. There are no fields being rendered called “Name” or “DrivingLicense” (there is a FullName property but not Name) so the messages are never displayed.
  9. A confused user doesn’t understand why their record won’t save, when no validation errors are displayed!

There are three options for resolving this; the first is to set ExcludePropertyErrors to false on the Validation Summary, which causes all errors to be listed. The problem is that this isn’t very nice – there are problems with both the FullName and Age fields on the form, so shouldn’t we highlight them? The second solution is to alter the TryValidateModel code to output all ModelState errors with a key of String.Empty. This elevates them from property validation errors to container object level errors, so they are displayed in the Validation Summary even when ExcludePropertyErrors is set to true. However, we still don’t get highlighting of the specific invalid fields.

The third solution is to perform a mapping from Person domain entity property validation errors to property names that match those on the PersonViewModel. So if Name validation fails, we add a record to ModelState with a key of “FullName”, and if the DrivingLicense validation fails we add a record to ModelState with a key of “Age” (notice this isn’t a direct property name mapping too – validation of DrivingLicense failed but the field that caused the issue on our screen was Age).

My Code

To achieve this I came up with an extension method for Controller that replicates the functionality in TryValidateModel, but also performs a simple mapping between property names. Let’s see the class and then discuss it;

    1:  public static bool TryValidateAndTranslate(
    2:      this Controller controller, 
    3:      object model, 
    4:      string prefix, 
    5:      object propertyMap)
    6:  {
    7:      return TryValidateAndTranslate(
    8:          controller, 
    9:          model, 
   10:          prefix, 
   11:          new RouteValueDictionary(propertyMap));
   12:  }
   13:   
   14:  public static bool TryValidateAndTranslate(
   15:      this Controller controller, 
   16:      object model, 
   17:      string prefix, 
   18:      RouteValueDictionary propertyMap)
   19:  {
   20:      ModelMetadata metadata = ModelMetadataProviders
   21:          .Current
   22:          .GetMetadataForType(() => model, model.GetType());
   23:   
   24:      foreach (ModelValidationResult validationResult in ModelValidator
   25:          .GetModelValidator(metadata, controller.ControllerContext)
   26:          .Validate(null))
   27:      {
   28:          var propertyName = 
   29:              CreatePropertyName(validationResult.MemberName, prefix, propertyMap);
   30:          controller.ModelState.AddModelError(propertyName, validationResult.Message);
   31:      }
   32:   
   33:      return controller.ModelState.IsValid;
   34:  }

This code simply performs validation, but just before adding an error to ModelState (line 28) it calculates the mapped property name, using a property map passed in to the call. If no property name matching the error is found, it uses String.Empty to display the message as a non-property validation message in the Validation Summary. The result is that I can call the method like this;

    1:  this.TryValidateAndTranslate(
    2:      person, 
    3:      "Person", 
    4:      new { Name = "FullName", Age = "Age" })

The second argument is the Prefix used to render the fields for the entity we’re validating. So if your View Model (perhaps HomeEditViewModel) has a “Person” property which is of type PersonViewModel, the prefix is likely to be “Person” (matching the name of the property on HomeEditViewModel). This means MVC emits fields like this;

    1:  <input type="text" name="Person.Name" value="Simon" />

Using TryValidateAndTranslate

To use this extension method is really simple, and follows the logical steps I proposed above;

    1:  [HttpPost]
    2:  public ActionResult Edit(HomeEditViewModel personvm)
    3:  {
    4:      var person = PersonService.Data.GetById(personvm.Person.Id);
    5:      PersonMapper.MapToEntity(personvm.Person, person);
    6:   
    7:      if (!this.TryValidateAndTranslate(
    8:              person, 
    9:              "Person", 
   10:              PersonMapper.EntityToViewModelPropertyMappings))
   11:          return View(personvm);
   12:   
   13:      PersonService.Data.Update(person);
   14:      return RedirectToAction("Index");
   15:  }

Note that I’ve stored the property map for Person to PersonViewModel as a field on my PersonMapper helper class. I’ve also got a very simple repository pattern in place for retrieving and updating Person records – the calls to PersonService are where you would probably have calls to some slightly more meaty business logic.

Also, note that I don’t check ModelState.IsValid before I do the mapping and use TryValidateAndTranslate. I could do this, for example (don’t use this code!);

    1:  [HttpPost]
    2:  public ActionResult Edit(HomeEditViewModel personvm)
    3:  {
    4:      if (ModelState.IsValid)
    5:          return View(personvm);
    6:   
    7:      var person = PersonService.Data.GetById(personvm.Person.Id);
    8:      // rest of code removed .....
    9:  }

But that would have undesirable behaviour. The validation on my View Model would run, and display errors. The user would then correct these mistakes and resubmit the form, at which point the validation on my Domain Entity would run and return errors, redisplaying a different set of problems! This is not a good User Experience, and therefore we run all our validation before checking ModelState.IsValid implicitly on line 7 of the correct approach above (because TryValidateAndTranslate returns the status of ModelState.IsValid).

If you’d like to see this in action, download the attached code sample. Usual disclaimers apply – and your mileage may vary!

Further Work

Two things strike me as likely to need to be enhanced here. Firstly, I’ve only handled very simple mappings between properties; there is probably scope to do more complex things and to handle hierarchies of entities. Secondly, I’d like to see if I could integrate the mapping functionality with AutoMapper. If you have done this, please comment below!

Lastly, the error messages displayed against fields can be misleading – for example it could say “The field Name must be 10 characters or less” adjacent to the “FullName” field. This means you have to think about error messages that are portable. I have wondered about digging a little deeper and replicating the logic that actually calls Data Annotations attributes’ GetValidationResult method, as this would allow the chance to alter the name of the field under validation. This is a potential minefield though!

I hope this has helped – let me know how you get on with it, or if you have any thoughts.

 

ManualValidation.zip

 Original post by Simon Ince on 7th December 2010, here: https://blogs.msdn.com/b/simonince/archive/2010/12/07/view-model-versus-domain-entity-validation-with-mvc.aspx

Comments

  • Anonymous
    January 06, 2012
    Thanks so much for this. This is awesome post I ever seen on internet. This is rare to find that’s why difficult to understand. Anyway, you are definitely someone that has something to say that people need to hear. Keep up the good work. Keep on inspiring the people. For more information regards: <a href= http://asianrunway.com/ title=" Modelling Agencies London"> Modelling Agencies London</a>