10
Vote

Dictionary<> model binder throws exception when no values present

description

This was working fine in MVC 3, I'm not sure if this is an intended change or not.

In MVC4, the model binder cannot properly bind a Dictionary<> if the form was submitted with no values. Consider the following controller action:

[HttpPost]
public void Index(Dictionary<int, bool> values, FormCollection form)
{
}

In MVC3 if this action was called with no values, the dictionary would simply contain 0 entries. In MVC4 if this action is called with no values, it throws an exception: "Specified cast is not valid."

Changing the controller action to the following produces the expected result as it was in MVC3:

[HttpPost]
public void Index(FormCollection form)
{
var values = new Dictionary<int,bool>();
this.UpdateModel(values, "values");
}

I have attached a solution which demonstrates the problem.

file attachments

comments

chaoaretasty wrote Sep 27, 2012 at 4:05 PM

Some more information.

I have just spent 2 hours hunting down this bug. It occurs anytime you bind a dictionary directly in the signature of your action method and no values are passed to bind to it (when you would expect to get a null dictionary).

If you're dictionary is anything other than string or object it will throw an error (see stacktrace below). If it is, for example, <string, string> it does something even more insidious, it appears to bind routevalues to the dictionary! You won't get an exception but you'll get bad data.

I have a method that takes several parameters including a Dictionary<DateTime, Decimal> which can be null that was throwing this exception, I changed it to <string,string>, stepped through and the Immediate Window told me that my dictionary was:

HandHygiene
Count = 2
[0]: {[action, EditICBox]}
[1]: {[controller, InfectionControl]}
I had problems setting up the symbol server settings to step through the source but I've looked through the MVC 3 and 4 sources and I think the problem is in DefaultModelBinder.cs in UpdateDictionary() at line 751. Previously if a dictionary couldn't be bound by looking for indexes this method would return null. Now there is an extra attempt to bind the dictionary if it failed on indexes (see "Extra Dictionary Binding Attempt" below).

I haven't been able to test this but I think this is causing the RouteValues to bind to the temporary dictionary, causing either erroneous entries or InvalidCastException the original poster mentioned.

The invalid casting is annoying but can be worked around, but as this could introduce extra data I feel the priority should be increased from low.

Extra Dictionary Binding Attempt:

// Let's try another method
        if (modelList.Count == 0)
        {
            IEnumerableValueProvider enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
            if (enumerableValueProvider != null)
            {
                IDictionary<string, string> keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
                foreach (var thisKey in keys)
                {
                    modelList.Add(CreateEntryForModel(controllerContext, bindingContext, valueType, valueBinder, thisKey.Value, thisKey.Key));
                }
            }
        }
Stacktrace:

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.InvalidCastException: Specified cast is not valid.
at System.Web.Mvc.DefaultModelBinder.CollectionHelpers.ReplaceDictionaryImpl[TKey,TValue](IDictionary2 dictionary, IEnumerable1 newContents)
--- End of inner exception stack trace ---
at System.RuntimeMethodHandle._InvokeMethodFast(IRuntimeMethodInfo method, Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeType typeOwner)
at System.RuntimeMethodHandle.InvokeMethodFast(IRuntimeMethodInfo method, Object target, Object[] arguments, Signature sig, MethodAttributes methodAttributes, RuntimeType typeOwner)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
at System.Web.Mvc.DefaultModelBinder.CollectionHelpers.ReplaceDictionary(Type keyType, Type valueType, Object dictionary, Object newContents)
at System.Web.Mvc.DefaultModelBinder.UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType)
at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)

zihotki wrote Oct 1, 2012 at 3:12 PM

I also think that the priority should be changed from low, if I need in router data, I'll use other ways to get it.

jaysqrd wrote Nov 12, 2012 at 8:39 PM

I think this should be made a higher priority. It has a lot of implications for current MVC3 applications transitioning to MVC4.

tommck wrote Nov 26, 2012 at 3:59 PM

I too believe this should be a high priority fix. I just ran into this myself. Minimally, this should be listed as a "breaking change" in the release notes (with possible workarounds server-side.

Oh, this also happens with IDictionary<> as well as Dictionary<>

korggy wrote Dec 10, 2012 at 1:04 PM

This just bit me again in another project... <sigh>

mattwendels wrote Dec 18, 2012 at 3:11 PM

This has caused us numerous problems and will require a lot of changes to get things working again. Priority needs changing to high!

theorygeek wrote Jan 2 at 9:40 PM

I got this error message when I had a Dictionary<int, string> as a property in my model - not as a direct parameter to the action. The form submitted an index and key for the dictionary entry, but not a value.

richardprior wrote Jan 11 at 1:16 PM

Temporary fix, replace the dictionary binder. I've attached an example of a modified default binder that fixes the issue.

To use, register in App_Start:
ModelBinder.Binders.DefaultBinder = new DefaultDictionaryBinder();

Ptaylor wrote Feb 21 at 11:16 AM

The following accepted pull request fixes this issue. Pull Request

korggy wrote Feb 21 at 1:18 PM

@Ptaylor Awesome, thanks for the fix! This issue keep biting me when I least expect it...

phenning wrote Jun 6 at 5:10 PM

This was integrated into master with commit: 26d31700413459c319414f87f6146b197df49059

kirthik wrote Today at 6:11 AM

I am still seeing "Specified cast is not valid " error for MVC5 project too. I used 617.0 preview refresh build.