Thursday, March 23, 2017

MVC and EF Validation with added ValidationContext item

Leave a Comment

I have a scenario where I'd like to add an item to the ValidationContext and check for it in the EF triggered entity validation. I'm doing this in a wizard so I can only validate certain things on specific steps. (If there's a good pattern for that please do share it).

The problem is that the validation is triggered, twice actually, before the controller action is even hit. I wish I understood why. I'm not sure how to get the item in ValidationContext before that happens, so I can't tell the validation what step I'm on.

Furthermore, if I only do the custom validation when save changes is triggered by checking for the item as I have in my code below, then I get no automatic model validation errors displayed when the page refreshes.

In my custom context:

public WizardStep Step { get; set; }  protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) {     items.Add("ValidationStep", Step);     return base.ValidateEntity(entityEntry, items); } 

Service that sets the entity:

public void SaveChanges(WizardStep step) {     _context.Step = step;     _context.SaveChanges(); } 

In my entity

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {     // Step will only be present when called from save changes.  Calls from model state validation won't have it     if (validationContext.Items.ContainsKey("ValidationStep"))     {         var validationStep = (WizardStep)validationContext.Items["ValidationStep"];         if (validationStep == WizardStep.Introduction)         {             if (criteria)             {                 yield return new ValidationResult($"Error message  ", new[] { "field" });             }         }     } } 

Controller:

public ActionResult MyAction(HomeViewModel vm) {     try     {         _incidentService.AddOrUpdate(vm.Enttiy);         _incidentService.SaveChanges(WizardStep.Introduction);     }     catch (Exception ex)     {         return View(vm);     }     return RedirectToAction("Index"); } 

4 Answers

Answers 1

The first validation is on the MVC created model that is passed to the controller. MVC uses a ModelBinder class to construct, populate and validate the client http form data into the model. Any failed validation will be returned to the client. A valid model may then be changed by the controller, so a second validation is done by EF when saved. I believe when saved, EF validation is only triggered if the property is new or has different data the original value.

Theoretically it should be possible to have a custom MVC ModelValidator and intercept the Validate method to set ValidationContext items. However, I could NOT figure out how to do that. I did however find a slightly different solution that works for me. Perhaps it can be adapted to fit your needs.

In my case, I wanted the EF DbContext (In my code its named CmsEntities) available to the Validation methods so I can querying the database (and do rich complex business logic validation). The controller has the DbContext, but the model validation is called by the ModelBinder before passing it to the controller’s action.

My solution is to:

1) Add a DbContext property to my Entity (Using partial class, or in Base Entity that all entities inherit from)

2) Create a Custom ModelBinder that will get the DbContext from the controller and populate it to the model

3) Register the Custom ModelBinder in the Application_Start()

Now, inside any validation method, the model will have a populated DbContext. 

Custom ModelBinder

public class CmsModelBinder : DefaultModelBinder {     protected override bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)     {         // Copy CmsEntities from Controller to the Model (Before we update and validate the model)         var modelPropertyInfo = bindingContext.Model.GetType().GetProperty("CmsEntities");         if (modelPropertyInfo != null)         {             var controllerPropertyInfo = controllerContext.Controller.GetType().GetProperty("CmsEntities");             if (controllerPropertyInfo != null)             {                 CmsEntities cmsEntities = controllerPropertyInfo.GetValue(controllerContext.Controller) as CmsEntities;                 modelPropertyInfo.SetValue(bindingContext.Model, cmsEntities);             }         }                     return base.OnModelUpdating(controllerContext, bindingContext);     } 

Global.asax.cs

    protected void Application_Start()     {         ...         ModelBinders.Binders.DefaultBinder = new CmsModelBinder();     } 

Answers 2

First of all you should consider if WizardStep does belong to context or the object which is modified during separate steps? Other thing is why not to use ie. Strategy to handle validation logic on separate steps?

About validation, I see you are mixing 2 things.

One is validation on context, where you should handle validation logic for every entity type you have in context.

The other one is implementation of IValidatableObject.Validate which should be called automatically for entity on SaveChanges.

I would decide and choose one way to go and from info you gave us I think having only IValidatableObject.Validate makes more sense, but then you would have to either put step into entity that's being validated or somehow inject that step other way only for validation.

Answers 3

You can do this in this way:

try {   //write code  }  catch (System.Data.Entity.Validation.DbEntityValidationException ex)             {                 var outputLines = new List<string>();                 foreach (var eve in ex.EntityValidationErrors)                 {                     outputLines.Add(string.Format(                         "{0}: Entity of type \"{1}\" in state \"{2}\" has the following validation errors:",                         DateTime.Now, eve.Entry.Entity.GetType().Name, eve.Entry.State));                     foreach (var ve in eve.ValidationErrors)                     {                         outputLines.Add(string.Format(                             "- Property: \"{0}\", Error: \"{1}\"",                             ve.PropertyName, ve.ErrorMessage));                     }                 }                 System.IO.File.AppendAllLines(@"c:\temp\errors.txt", outputLines);             } 

Answers 4

Just share my solution for mvc validate:

public class TestController:Controller {     public ActionResult Action1(MyModel data)     {         try         {             if (!ModelState.IsValid)             {                 var errors = ModelState.Values.Where(c => c.Errors.Count > 0).SelectMany(c => c.Errors.Select(o => o.ErrorMessage));                 var errorMsg = String.Join("<br/>", errors);                 return Json(new                 {                     IsSuccess = false,                     Message = errorMsg                 });             }             //deal business             return Json(new { IsSuccess = true, Message = "success" });         }         catch (Exception ex)         {             return Json(new { IsSuccess = false, Message = "fail" });         }     } } public class MyModel : IValidatableObject {     [Required(ErrorMessage = "{0} is required")]     public decimal TotalPrice { get; set; }     [Required(ErrorMessage = "{0} is required")]     public decimal TotalPriceWithoutCoupon { get; set; }     public ContactStruct Contact { get; set; }     public bool Condition{ get; set; }      public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)     {         var instance = validationContext.ObjectInstance as MyModel;         if (instance == null)         {             yield break;         }         if (instance.Condition)         {             if (instance.Contact == null)             {                 yield return new ValidationResult("contact is required", new string[] { "Contact" });             }             else             {                 if (string.IsNullOrEmpty(instance.Contact.phone))                 {                     yield return new ValidationResult("the phone of contact is required", new string[] { "Contact.phone" });                 }             }         }     } } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment