Skip to content

Instantly share code, notes, and snippets.

@DalSoft
Created January 10, 2012 12:33
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save DalSoft/1588818 to your computer and use it in GitHub Desktop.
Save DalSoft/1588818 to your computer and use it in GitHub Desktop.
ASP.NET MVC 3 - Improved JsonValueProviderFactory using Json.Net
//Example of a model that won't work with the current JsonValueProviderFactory but will work with JsonDotNetValueProviderFactory
public class CmsViewModel
{
public bool IsVisible { get; set; }
public string Content { get; set; }
public DateTime Modified { get; set; }
public DateTime Created { get; set; }
//This property will not work with the current JsonValueProviderFactory
public dynamic UserDefined { get; set; }
}
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
namespace JsonDotNetValueProviderFactoryTestHarness
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "TestJsonValueProviderFactory", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
//Remove and JsonValueProviderFactory and add JsonDotNetValueProviderFactory
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new JsonDotNetValueProviderFactory());
}
}
}
using System.Dynamic;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace System.Web.Mvc
{
public sealed class JsonDotNetValueProviderFactory : 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);
}
}
}
@eugenet8k
Copy link

@Umar-Mukhtar The problem is that MVC has updated implementation of the original JsonValueProviderFactory. So now the result of GetValueProvider() is expected in a different format. This may be used as an example code for overriding of default JSON parsing by JSON.NET one with the recent version of MVC:

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

            var jsonData = GetDeserializedObject(controllerContext);
            if (jsonData == null)
            {
                return null;
            }

            var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            AddToBackingStore(backingStore, String.Empty, jsonData);
            return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
        }

        private static object GetDeserializedObject(ControllerContext controllerContext)
        {
            if (!controllerContext.HttpContext.Request.ContentType.StartsWith(
                "application/json", 
                StringComparison.OrdinalIgnoreCase))
            {
                // not JSON request
                return null;
            }

            var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
            var bodyText = reader.ReadToEnd();
            if (String.IsNullOrEmpty(bodyText))
            {
                // no JSON data
                return null;
            }

            var jsonData = JsonConvert.DeserializeObject<ExpandoObject>(
                bodyText, new ExpandoObjectConverter());
            return jsonData;
        }

        private static void AddToBackingStore(IDictionary<string, object> backingStore, string prefix, object value)
        {
            var d = value as IDictionary<string, object>;
            if (d != null)
            {
                foreach (var entry in d)
                {
                    AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
                }
                return;
            }

            var l = value as IList;
            if (l != null)
            {
                for (var i = 0; i < l.Count; i++)
                {
                    AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
                }
                return;
            }

            // primitive
            backingStore.Add(prefix, value);
        }

        private static string MakeArrayKey(string prefix, int index)
        {
            return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
        }

        private static string MakePropertyKey(string prefix, string propertyName)
        {
            return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
        }
    }

@eugenet8k
Copy link

Also watch out the code of registering a new factory. It's wrong. It adds our custom JsonDotNetValueProviderFactory factory in the end of the ValueProviderFactories.Factories list. So it gets the lowest priority during model binding and this may break the values of deserialized objects if other factories have the parameters with the same name. The recommended registering code is:

public static void RegisterFactory()
        {
            var defaultJsonFactory = ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault();
            var index = ValueProviderFactories.Factories.IndexOf(defaultJsonFactory);
            ValueProviderFactories.Factories.Remove(defaultJsonFactory);
            ValueProviderFactories.Factories.Insert(index, new JsonDotNetValueProviderFactory());
        }

The idea is to insert our instance at the same index where default JsonValueProviderFactory was.

@Ixonal
Copy link

Ixonal commented Aug 15, 2014

@ievgen, I was having trouble with the original version of the JsonDotNetValueProvider not wanting to cooperate with nested classes, but that reworked version seems to work for me, except for the case of a json request containing a list at the base level. I came upon a solution, and it is, thankfully, very simple. Just check whether the body text begins with "[" and ends with "]", then deserialize to an IList of ExpandoObject.

Here's my reworked GetDeserializedObject method:

private static object GetDeserializedObject(ControllerContext controllerContext) {
  if(!controllerContext.HttpContext.Request.IsAjaxRequest()) return null;

  StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
  string bodyText = reader.ReadToEnd();
  if(string.IsNullOrEmpty(bodyText)) {
    // no JSON data
    return null;
  }

  object retVal;

  if(bodyText.StartsWith("[") && bodyText.EndsWith("]")) {
    retVal = JsonConvert.DeserializeObject<IList<ExpandoObject>>(bodyText, new ExpandoObjectConverter());
  } else {
    retVal = JsonConvert.DeserializeObject<ExpandoObject>(bodyText, new ExpandoObjectConverter());
  }

  return retVal;
}

Edit
I noted that my previous code would bork if the base level array contained primitive types, instead of objects, because they could not be cast to ExpandoObject. Below is the updated method that will take this into account (as far as I've been able to tell).

    private static object GetDeserializedObject(ControllerContext controllerContext) {
      if(!controllerContext.HttpContext.Request.IsAjaxRequest()) return null;

      StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
      string bodyText = reader.ReadToEnd().Trim();
      if(string.IsNullOrEmpty(bodyText)) {
        // no JSON data
        return null;
      }

      object retVal;

      if(bodyText.StartsWith("[") && bodyText.EndsWith("]")) {
        //base element is an array
        IList<object> obj = JsonConvert.DeserializeObject<IList<object>>(bodyText, new ExpandoObjectConverter());

        //the above conversion will leave nested objects as JContainers, so we need to locate them 
        //and convert them to ExpandoObject instances, but leave everything else alone
        for(int c = 0; c < obj.Count; c++) {
          if(obj[c] is JContainer) {
            //convert the JContainer to an expando object
            obj[c] = ((JContainer)obj[c]).ToObject<ExpandoObject>();
          }
        }

        retVal = obj;
      } else if(bodyText.StartsWith("{") && bodyText.EndsWith("}")) {
        //base element is an object
        retVal = JsonConvert.DeserializeObject<ExpandoObject>(bodyText, new ExpandoObjectConverter());
      } else {
        //base element is a primitive type... technically not correct, but let's not ignore it
        retVal = JsonConvert.DeserializeObject<object>(bodyText);
      }

      return retVal;
    }

Edit 2
In thinking about it, this could also probably be accomplished by altering the AddToBackingStore method to look for JContainer objects and handling them accordingly...

@huan086
Copy link

huan086 commented Mar 12, 2015

There's a better solution available in the link. It does not use ReadToEnd, thus saving memory. Follow AdaskoTheBeAsT's comments and make the changes to JsonSerializer.CreateDefault too.
https://json.codeplex.com/discussions/347099

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment