UpdateModel/TryUpdateModel gotchas with models created through reflection
Explicit model binding
The Model Binding feature takes away most of the burden from developers by taking the responsibility of model instantiation from the information available in the request. Sometimes we meet cases where we need to trigger the model binding process explicitly inside a controller. MVC provides two methods for rescue: UpdateModel and TryUpdateModel.
Both these methods perform the same operation, that is they update the model from the value providers. The difference between them is the UpdateModel throws an exception if the model state is not valid while TryUpdateModel not. Though both these methods are generic and we don't need to explicitly specify the generic parameter.
public ActionResult Save(FormCollection form) { var emp = new Employee(); if(TryUpdateModel(emp, form)) { _empRepo.Save(emp); return RedirectToAction("Index"); } return View(); }
Both these methods have overloads that accepts an IValueProvider. When you don't pass a particular value provider the controller uses all the available value providers to fill the created model.
There is a peculiar problem with these two methods when we try to bind a model that is instantiated through reflection. In this article we are going to see about that interesting issue and how we can solve that.
The problem
Let's take the same example but in this time the Employee class is instantiated through reflection.
public ActionResult Save(FormCollection form) { var empType = Type.GetType("Example.Models.Employee"); var emp = Activator.CreateInstance(empType); if(TryUpdateModel(emp, form)) { _empRepo.Save(emp); return RedirectToAction("Index"); } return View(); }
When you run the above code you will find that the properties of emp object will never get filled even though those values are available in the form.
Why this happens?
The way the controller gets the type of the passed model is causing the problem. All the overloaded versions of UpdateModel/TryUpdateModel calls finally a version of TryUpdateModel and that looks as below,
protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class { ... IModelBinder binder = Binders.GetBinder(typeof(TModel)); ModelBindingContext bindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel)), ModelName = prefix, ModelState = ModelState, PropertyFilter = propertyFilter, ValueProvider = valueProvider }; binder.BindModel(ControllerContext, bindingContext); return ModelState.IsValid; }
The problem is the that method gets the type of the passed model from the generic parameter instead from the passed object i.e. typeof(TModel) instead of model.GetType(). It uses that way in couple of places, to get the binder for the type and when instantiating the ModelMetaData.
The problem is when you get the type of the passed object from the generic parameter it returns the type correctly if the object is instantiated directly but if the object is instantiated through reflection then type(TModel) will return the type as System.Object instead of the true type.
How can we fix this?
We can't fix this issue but I hope the MVC team will take care of that in the coming versions. But still that time we can achieve the model binding for entities created through reflection by doing that work manually forgetting the built-in methods.
public ActionResult Save(FormCollection form) { var empType = Type.GetType("Example.Models.Employee"); var emp = Activator.CreateInstance(empType); var binder = Binders.GetBinder(empType); var bindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => emp, empType), ModelState = ModelState, ValueProvider = form }; binder.BindModel(ControllerContext, bindingContext); if (ModelState.IsValid) { _empRepo.Save(emp); return RedirectToAction("Index"); } return View(); }
In the above code we just duplicated the code from the TryUpdateModel ripping off some of the things and most importantly replacing the typeof(TModel) into model.GetType().
Hope this helps someone who is running into this issue.