0

I'm trying to map a JSON to a c# ViewModel on a Controller Save action. Basically, the JSON has this structure:

{
   "Id": "",
   "TypeId": "37",
   "FormId": "",
   "ExtraData":{
      "title": "Some random title",
      "contribute": "author",
      "location": {
          "url": "/Files/ed5cf2ea-c920.jpeg",
          "size": "100",
          "format": "application/json"
      }
   }
}

Some fields are "know" and some other aren't. The 'ExtraData' is a container for all these unknown fields and they can vary a lot.

I've created a c# VIewModel to map this:

public class ContentViewModel
{
    public Guid Id { get; set; }
    public int TypeId{ get; set; }
    public int FormId{ get; set; }
    public Dictionary<string, object> ExtraData { get; set; }
}

When I make a request, with a JSON like the above, only the 'root' elements of the ExtraData are mapped. The "location" object is mapped to an object.

I've tried to change the Dictionary to a dynamic, still doesn't work. I also tried to create a JsonConverter, but it never got executed. My guess it doesn't work with ASP.NET MVC app out of the box?

I've tried a lot of things, searched and I'm feeling I got to a dead end.

EDIT: The ExtraData will be stored as is in MongoDB. It can be very simple or complex(with multiple nodes and nested objects).

jpgrassi
  • 5,482
  • 2
  • 36
  • 55
  • MVC is using JavaScriptSerilizer, not Json.NET, so using JsonConverter won't work. Try using ApiController from Web Api 2. – jahav Sep 24 '15 at 12:24
  • Not 100% sure with out testing, but I would bet your problem is the 'ExtraData' don't know exactly if that will bind like that. Can you turn ExtraData into a class of its own? Then Title, Contribute, and Location can be properties in their own class. – Botonomous Sep 24 '15 at 12:25
  • You mean, create a class with only the Dictionary? I can do that. – jpgrassi Sep 24 '15 at 12:27
  • @jpgrassi Create a class that has properties for Title, Contribute Location. Then that type goes in ContentViewModel as ExtraData. – Botonomous Sep 24 '15 at 12:31
  • I think you didn't get it. The ExtraData can vary. The fields title, contribute are only examples.. basically any thing can come in this ExtraData they are not "fixed" data. – jpgrassi Sep 24 '15 at 12:36
  • what are you going to do with `ExtraData`? If it is unknown, then its better to send pair. So your location object can be sent as `"location": "{\"url\": \"/Files/ed5cf2ea-c920.jpeg\", \"size\": \"100\", \"format\": \"application/json\" }"` – Dandy Sep 24 '15 at 12:57
  • The ExtraData will be stored as is in MongoDB. It can't be key string because some objects can be "nested" in any level and I can't lose this hierarchy – jpgrassi Sep 24 '15 at 12:59

2 Answers2

3

If you are OK to go with 'dynamic' object parsing to your requirement (since you mentioned, you already tried changing Dictionary to dynamic), you may try with following approach.

Please note, though i am quite not sure if the approach fit your requirement but i hope it might help you in some way to start with.

I took the custom model binder(for specific property Type) approach since that's another extensibility point to deal with complex model/property binding in ASP.NET MVC

Custom Model Binder

 public class ComplexObjectModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var contentType = controllerContext.HttpContext.Request.ContentType;
            if (!contentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
                return (null);

            string jsonString;

            using (var stream = controllerContext.HttpContext.Request.InputStream)
            {
                stream.Seek(0, SeekOrigin.Begin);
                using (var reader = new StreamReader(stream))
                    jsonString = reader.ReadToEnd();
            }

            if (string.IsNullOrEmpty(jsonString)) return (null);

            DynamicComplexObject ExtraData = new DynamicComplexObject();

            ExtraData.Details = new JavaScriptSerializer().Deserialize<dynamic>(jsonString);

            return (ExtraData);
        }
    }

Register the custom binder in Global.asax:

public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            //Custom model binder registration
            ModelBinders.Binders.Add(typeof(Models.DynamicComplexObject), new ComplexObjectModelBinder());

            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }

View Models:

public class ContentViewModel
{
    public Guid Id { get; set; }
    public int TypeId { get; set; }
    public int FormId { get; set; }
    public DynamicComplexObject ExtraData { get; set; }
}
//You may try to improve the following model and update the custom model binder logic as appropriate.
public class DynamicComplexObject
{
    public dynamic Details { get; set; }
}

In following JSON i added one more level nesting for testing

var data = {
        "Id": "",
        "TypeId": "37",
        "FormId": "",
        "ExtraData": {
            "title": "Some random title",
            "contribute": "author",
            "location": {
                "url": "/Files/ed5cf2ea-c920.jpeg",
                "size": "100",
                "format": "application/json",
                "onemorelocation": { //Added one more nested object to test
                    "a": 1,
                    "b": 2
                }
            }
        }
    };
$("#btnSubmit").on("click", function () {
        $.ajax({
            "datatype": "json",
            "contentType": "application/json; charset=utf-8",
            "type": "POST",
            "url": '@Url.Action("SaveData", "Home")',
            "data": JSON.stringify(data),
            success: function (d) {
               //Do stuff here
            }
        });
    });

From debugging i can see the binding happening as shown in image. enter image description here

Siva Gopal
  • 3,474
  • 1
  • 25
  • 22
  • This also seems to duplicate some fields inside the ExtraData elements, right? Tried now your approach just to see.. – jpgrassi Sep 25 '15 at 03:01
0

Siva's answer might work but I did not want to mess with the default Model Binder. After reading this I could understand why my dynamic/Dictionary was not binding correctly.

So, I've created a custom ValueProvider and that replaced the default JsonValueProviderFactory which does not work with dynamic neither nested dictionaries.

JsonNetValueProviderFactory

public sealed class JsonNetValueProviderFactory : ValueProviderFactory
{
    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
            throw new ArgumentNullException("controllerContext");

        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
            return null;

        var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        var bodyText = reader.ReadToEnd();

        return String.IsNullOrEmpty(bodyText) ? null : new DictionaryValueProvider<object>(JsonConvert.DeserializeObject<ExpandoObject>(bodyText, new ExpandoObjectConverter()), CultureInfo.CurrentCulture);
    }
}

Global.asax.cs

public static void RegisterFactory()
{
        ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
        ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory());
        JsonConvert.DefaultSettings = () => new JsonSerializerSettings
        {
            TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple,
            TypeNameHandling = TypeNameHandling.All
        };

 }

ViewModel

public class ContentViewModel
{
   public Guid Id { get; set; }
   public int TypeId{ get; set; }
   public int FormId{ get; set; }
   public dynamic ExtraData { get; set; }
}

Again, Siva's answer might also work but for my case, I think it was better to create a custom ValueProvider instead of messing with the model binder.

Here is working: enter image description here

Also, a list of very helpful links: ASP.Net MVC3 JSON.Net Custom ValueProviderFactory ASP.NET MVC 3 – Improved JsonValueProviderFactory using Json.Net

Community
  • 1
  • 1
jpgrassi
  • 5,482
  • 2
  • 36
  • 55