Be Aware of DefaultModelBinder Conventions

December 1st 2011 ASP.NET MVC

DefaultModelBinder is an essential piece of ASP.NET MVC framework which makes writing strongly typed actions really simple. In spite of its strengths (or maybe because of them) it can still introduce hard to solve problems in your code. Take a look at the following example, a simplification of the problem I was confronted with today:

public class DocumentVersion
{
    public int Id { get; set; }
    public int Version { get; set; }
    public string Name { get; set; }
}

public class DocumentController : Controller
{
    public ActionResult New()
    {
        return View();
    }

    public ActionResult Save(DocumentVersion version)
    {
        if (ModelState.IsValid)
        {
            // save data
            return View("Confirm");
        }
        return View("New");
    }
}

Assuming all DocumentVersion properties are submitted and valid Save action should return Confirm view, right? Wrong! Try it out and you'll get a validation error on Version property. Taking a closer look it turns out ModelState["Version"].Errors[0].Exception contains an InvalidOperationException:

The parameter conversion from type 'System.String' to type 'MvcApplication1.Models.DocumentVersion' failed because no type converter can convert between these types.

Of course there's no String to DocumentVersion converter. Though, Version property is an int. Why does it want to convert it to a DocumentVersion?

I soon started running out of ideas and fortunately enough I quickly decided to enable .NET Framework source stepping. A few moments later I reached the following piece of code in DefaultModelBinder and suddenly it became obvious what was happening:

if (!String.IsNullOrEmpty(bindingContext.ModelName)
        && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
    // We couldn't find any entry that began with the prefix. 
    // If this is the top-level element, fall back to the empty prefix.
    if (bindingContext.FallbackToEmptyPrefix) { 
        bindingContext = new ModelBindingContext() {
            ModelMetadata = bindingContext.ModelMetadata, 
            ModelState = bindingContext.ModelState, 
            PropertyFilter = bindingContext.PropertyFilter,
            ValueProvider = bindingContext.ValueProvider 
        };
        performedFallback = true;
    }
    else { 
        return null;
    } 
} 

// Simple model = int, string, etc.; determined by calling 
// TypeConverter.CanConvertFrom(typeof(string)) 
// or by seeing if a value in the request exactly matches 
// the name of the model we're binding.
// Complex type = everything else.
if (!performedFallback) {
    bool performRequestValidation = 
        ShouldPerformRequestValidation(controllerContext, bindingContext); 
    ValueProviderResult vpResult = 
        bindingContext.UnvalidatedValueProvider
                      .GetValue(bindingContext.ModelName, 
                          skipValidation: !performRequestValidation);
    if (vpResult != null) { 
        return BindSimpleModel(controllerContext, bindingContext, vpResult); 
    }
}

Notice the call to bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName) at the top and read the comment above the bottom block of the code. It turns out that in my sample ModelName was version just like one of the DocumentVersion properties therefore DefaultModelBinder decided to use simple model binding which failed because of a missing converter as it was also clearly stated in the exception. You might be wondering where ModelName came from. It's the name of the action method parameter. Fixing the code was simple now – rename the parameter and the code starts working as expected:

public ActionResult Save(DocumentVersion documentVersion)
{
    if (ModelState.IsValid)
    {
        // save data
        return View("Confirm");
    }
    return View("New");
}

Lesson of the day? Be aware of conventions and make sure parameter names don't match any of the property names if you are using complex models.

Copyright
Creative Commons License