ASP.NET MVC Custom Model Binder – Safe Updates for Unspecified Fields

Model Binders are one of the ASP.NET MVC framework’s celebrated features.

The typical way web apps work with a form POST is that the forms key/value pairs are iterated through and processed. In MVC, this works in the Action method’s FormCollection.

        [HttpPost]
        public ActionResult Edit(int id, FormCollection collection)

You create your data object and have a line per field.

            dataObject.First_name = collection["first_name"].ToString();
            dataObject.Age = (int)collection["age"];

This gets a little tedious, especially when you have to check values for null or other invalid values.

MVC Model Binders do some “magic” to handle the details of mapping your HTTP POST to an object. You specify the typed parameter in the ActionResult method signature…

        [HttpPost]
        public ActionResult Edit(int id, MyCompany.POCO.MyModel model)

… and the framework handles the mapping to the object for you.

The good part: you just saved a lot of code, which is good for efficiency and for supporting/debugging.

The bad part: what happens when we edit/update an object and the form does not include all the fields? We just overwrote the value to the default .NET value and saved to the db.

For example, if the model record had a property called [phone_number], and this MVC form did not have it. Maybe the form had to hide some values from update, or else the data model changed and added a field. In an Edit/update, the steps would be:

  1. creates the object from the class,
  2. copy the values from the form
  3. save/update to the db

… we never actually grab the current values of [phone_number], and we just set it to the .NET default value for the string type. Lost some real data. Not good.

ActionResult method and Model Binder steps

What’s actually happening:

  • framework looks at the parameter type and executes the registered IModelBinder for it. If there is none, it uses DefaultModelBinder

DefaultModelBinder will do the following: (source here)

  • create a new instance of the model – default values , i.e. default(MyModel)
  • read the form POST collection from HttpRequestBase
  • copy all the matching fields from the Request collection to the model properties
  • run it thru the MVC Validator, if any
  • return it to the controller ActionResult method for further action

Writing code in the Action method to fix the problem

My first step to deal with the issue was to fall back to the FormCollection model binder and hand-code the fix. It looks something like this:

        [HttpPost]
        public ActionResult Edit(int id, MyCompany.POCO.MyModel model, FormCollection collection)
        {
            // update
            if (!ModelState.IsValid)
            {
                return View("Edit", model);
            }

            var poco = modelRepository.GetByID(id);

            // map form collection to POCO
            // * IMPORTANT - we only want to update entity properties which have been 
            // passed in on the Form POST. 
            // Otherwise, we could be setting fields = default when they have real data in db.
            foreach (string key in collection)
            {
                // key = "Id", "Name", etc.
                // use reflection to set the POCO property from the FormCollection
                System.Reflection.PropertyInfo propertyInfo = poco.GetType().GetProperty(key);
                if (propertyInfo != null)
                {
                    // poco has the form field as a property
                    // convert from string to actual type
                    propertyInfo.SetValue(poco, Convert.ChangeType(collection[key], propertyInfo.PropertyType), null);
                    // InvalidCastException if failed.
                }

            }

            modelRepository.Save(poco);

            return RedirectToAction("Index");
        }

In this example, modelRepository could be using NHibernate, EF, or stored procs under the hood, but it could be any data source. We loop thru each form post key and try to find a matching property on the model (using reflection). If it matches, convert the string value from the form collection and set it as the value for that propery (also using reflection).

This works and is good, until you realize you have to insert it into every Action method. We could also go traditional, and just stick it in a function call. But we want to leverage the MVC convention-over-configuration philosophy. So now we’re going to try wrapping it in a custom model binder class.

Creating a Custom Model Binder to fix the problem

To avoid the “unspecified field” problem, we want a model binder to actually do the following on Edit:

  • Get() the model from the repository by id to create a new instance of the model
  • Update the fields of the persisted model which match from the FormCollection
  • run it thru the MVC Validator, if any
  • return it to the controller ActionResult method for further action (like Save() )

I am going to define a generic class which is good for any of my POCO types, and inherit from DefaultModelBinder:

    public class PocoModelBinder<TPoco> : DefaultModelBinder
    {
        MyCompany.Repository.IPocoRepository<TPoco> ModelRepository;

        public PocoModelBinder(MyCompany.Repository.IPocoRepository<TPoco> modelRepository)
        {
            this.ModelRepository = modelRepository;
        }

Note, i also inject my Repository (i use IoC), so that i can retrieve the object before update.

DefaultModelBinder has the methods CreateModel() and BindModel(), and we’re going to go with that.

        public object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            // http://stackoverflow.com/questions/752/get-a-new-object-instance-from-a-type-in-c
            TPoco poco = (TPoco)typeof(TPoco).GetConstructor(new Type[] { }).Invoke(new object[] { });

            // this is from the Route url: ~/{controller}/{action}/{id}
            if (controllerContext.RouteData.Values["action"].ToString() == "Edit")
            {
                // for Edit(), get from Repository/database
                string id = controllerContext.RouteData.Values["id"].ToString();
                poco = this.ModelRepository.GetByID(Int32.Parse(id));
            }
            else
            {
                // call default CreateModel() -- for the Create method
                poco = (TPoco)base.CreateModel(controllerContext, bindingContext, poco.GetType());
            }

            return poco;
        }

As you can see, with CreateModel(), if it is an Edit call, we retrieve the model object by the id specified in the URL. This is already parsed out in the RouteData collection. If it is not an Edit, we just call the base class CreateModel(). For example, a Create() call may also use the same ModelBinder.

Now, in the BindModel() method, this is where we move our logic to iterate thru the Form key/value pairs and update the POCO. But in this version, we only update fields in the form, and leave other properties alone:

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            object model = this.CreateModel(controllerContext, bindingContext);

            // map form collection to POCO
            // * IMPORTANT - we only want to update entity properties which have been 
            // passed in on the Form POST. 
            // Otherwise, we could be setting fields = default when they have real data in db.
            foreach (string key in controllerContext.HttpContext.Request.Form.Keys )
            {
                // key = "Pub_id", "Name", etc.
                // use reflection to set the POCO property from the FormCollection
                // http://stackoverflow.com/questions/531025/dynamically-getting-setting-a-property-of-an-object-in-c-2005
                // poco.GetType().GetProperty(key).SetValue(poco, collection[key], null);

                System.Reflection.PropertyInfo propertyInfo = model.GetType().GetProperty(key);
                if (propertyInfo != null)
                {
                    // poco has the form field as a property
                    // convert from string to actual type
                    // http://stackoverflow.com/questions/1089123/c-setting-a-property-by-reflection-with-a-string-value

                    propertyInfo.SetValue(model, Convert.ChangeType(controllerContext.HttpContext.Request.Form[key], propertyInfo.PropertyType), null);

                    // InvalidCastException if failed.

                }

            }

            return model;
        }

Great. Now that we have our ModelBinder, we have to tell our MvcApplication to use it. We add it the following line to Application_Start():

            // Custom Model Binders
            System.Web.Mvc.ModelBinders.Binders.Add(
                typeof(MyCompany.POCO.MyModel)
                , new MyMvcApplication.ModelBinders.PocoModelBinder<MyCompany.POCO.MyModel>(
                    WindsorContainer.Resolve<MyCompany.BLL.Repository.IPocoRepository<MyCompany.POCO.MyModel>>()
                    )
                );

In english, we are saying: Add to the ModelBinder collection… when you have to Model Bind a MyCompany.POCO.MyModel, use the PocoModelBinder<> (and pass it an IPocoRepository so it can access the data store).

Now we’re able to run our app, and can do safe, smart updates the “MVC-way”, keeping our methods clean.

I’ve use the Castle Windor IoC container and any NHibernate-backed Repository in this case, but the same technique can be used in any ASP.NET MVC app using any data access backend, and with or without any IoC container.

For more on Model Binders, see Mehdi Golchin’s Dive Deep Into MVC – IModelBinder Part 1.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>