David Wick
David Wick

Reputation: 7095

ModelBinder deserialize arbitrary json

I'm having trouble getting the default model binder to work as expected for the following data structure:

json:

{
    template: "path/to/template",
    slides: [{
        index: 0,
        context: {
            foo: "bar"
        }
    }, {
        index: 1,
        context: {
            foo: 'bar!',
            table: [
                ['Price', 'Revenue', 'Profit'],
                [$5', 100, 20],
                ['$10', 320, 4],
                ['$7', 50, 2]
            ]
        }

    }]
}

IPresentationData model:

public interface IPresentationData
{
    public string Template { get; set; }

    public ICollection<SlideData> Slides { get; set; }
}

ISlideData model:

public interface ISlideData
{
    public int Index { get; set; }

    public IContext Context { get; set; }
}

IContext model:

public interface IContext : IDictionary<string, dynamic>
{
}

The default model binder works fine with the exception of SlideData.Context, which can be an arbitrarily large/deep object.

For example, json.context.table is deserialized into:

{
  'table[0][0]': 'Price',
  'table[0][1]': 'Revenue',
  ...
}

Instead of:

{
  'table': [
    [...]
  ]
}

I'm thinking I need to override System.Web.Mvc.DefaultModelBinder#BindModel for just the SlideData.Context property, but I'm not really sure where to start.

Any help here would be much appreciated.

Upvotes: 2

Views: 1330

Answers (1)

David Wick
David Wick

Reputation: 7095

It turns out what was needed was an IModelBinder that supports ExpandoObject.

Here's the code:

In HttpApplication#Application_Start:

ModelBinders.Binders.Add(typeof(Context), new DynamicDictionaryModelBinder());
ModelBinders.Binders.Add(typeof(ExpandoObject), new DynamicDictionaryModelBinder());

Custom binder:

public class DynamicDictionaryModelBinder : DefaultModelBinder
{
    public override object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        var model = bindingContext.Model;
        var modelType = bindingContext.ModelType;

        if (model == null)
        {
            model = this.CreateModel(controllerContext, bindingContext, modelType);
        }

        var dictionaryBindingContext = new ModelBindingContext()
                                        {
                                            ModelMetadata =
                                                ModelMetadataProviders.Current
                                                .GetMetadataForType(() => model, modelType),
                                            ModelName = bindingContext.ModelName,
                                            ModelState = bindingContext.ModelState,
                                            PropertyFilter = bindingContext.PropertyFilter,
                                            ValueProvider = bindingContext.ValueProvider
                                        };

        return this.UpdateDynamicDictionary(controllerContext, dictionaryBindingContext);
    }

    private static KeyValuePair<string, object> CreateEntryForModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext,
        Type valueType,
        IModelBinder valueBinder,
        string modelName,
        string modelKey)
    {
        var valueBindingContext = new ModelBindingContext()
        {
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
            ModelName = modelName,
            ModelState = bindingContext.ModelState,
            PropertyFilter = bindingContext.PropertyFilter,
            ValueProvider = bindingContext.ValueProvider
        };

        var thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);

        return new KeyValuePair<string, object>(modelKey, thisValue);
    }

    private object UpdateDynamicDictionary(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        var modelList = new List<KeyValuePair<string, object>>();

        var enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
        if (enumerableValueProvider != null)
        {
            var keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
            var groups = keys.GroupBy((k) => k.Key.Split('[')[0]);

            foreach (var group in groups)
            {
                if (group.Count() > 1)
                {
                    var valueType = typeof(ICollection<ExpandoObject>);

                    modelList.Add(
                        CreateEntryForModel(
                            controllerContext,
                            bindingContext,
                            valueType,
                            Binders.GetBinder(valueType),
                            bindingContext.ModelName + '.' + group.Key,
                            group.Key));
                }
                else
                {
                    var item = group.Single();
                    var value = bindingContext.ValueProvider.GetValue(item.Value);
                    var valueType = value != null && value.RawValue != null ?
                            typeof(object) : typeof(ExpandoObject);

                    modelList.Add(
                        CreateEntryForModel(
                            controllerContext,
                            bindingContext,
                            valueType,
                            Binders.GetBinder(valueType),
                            item.Value,
                            item.Key));
                }
            }

        }

        var dictionary = (IDictionary<string, object>)bindingContext.Model;

        foreach (var kvp in modelList)
        {
            dictionary[kvp.Key] = kvp.Value;
        }

        return dictionary;
    }
}

The json was already being serialized properly by the built in IValueProvider stack, it was just a matter of providing the proper types to Binder#GetBinder(Type) as well as the proper accessor. For example ICollection<ExpandoObject> + revenue vs object + revenue[0][0].

Upvotes: 4

Related Questions