ASP.Net MVC3 JSON.Net Custom ValueProviderFactory

Mar 2, 2012 at 9:01 PM

Hello,

I just finished up this bit of code that I wanted to share :). Basically, you can swap out the MVC3 JsonValueProviderFactory cleanly with my JsonNetValueProviderFactory. It even supports collections!

In your MVC3 Application_Start() (removal of the MS Json (de)serializer):

 

        protected void Application_Start()
        {
            // setup ASP.Net MVC
            AreaRegistration.RegisterAllAreas();
            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
            // setup value providers
            ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
            ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory());
        }

JsonNetValueProviderFactory.cs (Client -> MVC3 Deserialization):

 

    public class JsonNetValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
            // first make sure we have a valid context
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            // now make sure we are dealing with a json request
            if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
                return null;

            // get a generic stream reader (get reader for the http stream)
            StreamReader streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
            // convert stream reader to a JSON Text Reader
            JsonTextReader JSONReader = new JsonTextReader(streamReader);
            // tell JSON to read
            if (!JSONReader.Read())
                return null;

            // make a new Json serializer
            JsonSerializer JSONSerializer = new JsonSerializer();
            // add the dyamic object converter to our serializer
            JSONSerializer.Converters.Add(new ExpandoObjectConverter());

            // use JSON.NET to deserialize object to a dynamic (expando) object
            Object JSONObject;
            // if we start with a "[", treat this as an array
            if (JSONReader.TokenType == JsonToken.StartArray)
                JSONObject = JSONSerializer.Deserialize<List<ExpandoObject>>(JSONReader);
            else
                JSONObject = JSONSerializer.Deserialize<ExpandoObject>(JSONReader);

            // create a backing store to hold all properties for this deserialization
            Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            // add all properties to this backing store
            AddToBackingStore(backingStore, String.Empty, JSONObject);
            // return the object in a dictionary value provider so the MVC understands it
            return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
        }

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

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

            // primitive
            backingStore[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;
        }
    }

JsonNetResult.cs (MVC3 -> Client Seserialization):

 

    public class JsonNetResult : JsonResult
    {
        public JsonSerializerSettings SerializerSettings { get; set; }

        public JsonNetResult() : base()
        {
            // create serializer settings
            this.SerializerSettings = new JsonSerializerSettings();
            // setup default serializer settings
            this.SerializerSettings.Converters.Add(new IsoDateTimeConverter());
        }

        public override void ExecuteResult(ControllerContext context)
        {
            // verify we have a contrex
            if (context == null)
                throw new ArgumentNullException("context");

            // get the current http context response
            var response = context.HttpContext.Response;
            // set content type of response
            response.ContentType = !String.IsNullOrEmpty(ContentType) ? ContentType : "application/json";
            // set content encoding of response
            if (ContentEncoding != null)
                response.ContentEncoding = this.ContentEncoding;

            // verify this response has data
            if (this.Data == null)
                return;

            // serialize the object to JSON using JSON.Net
            String JSONText = JsonConvert.SerializeObject(this.Data, Formatting.Indented, this.SerializerSettings);
            // write the response
            response.Write(JSONText);
        }
    }

ControllerExtensions.cs (Allows simple access to JsonNetResult from within a MVC3 Controller):

 

    public static class ControllerExtensions
    {
        public static JsonNetResult JsonNet(this Controller controller, object data)
        {
            return new JsonNetResult() { Data = data };
        }
    }

Example (from within a MVC3 Controller):

 

    public class StartController : Controller
    {
        public ActionResult Index()
        {
            // setup start
            Start start = new Start();
            // return view
            return View(start);
        }

        [HttpPost]
        public ActionResult Test(Mob[] m)
        {
            return this.JsonNet(m);
        }
    }

Enjoy!

Alex (awgneo@xbetanet.com)

 

May 15, 2012 at 6:14 AM

Works like a charm! Thank you very much!

May 28, 2012 at 1:40 AM

Unfortunately, this implementation of JsonNetValueProviderFactory does not support dictionaries.

May 28, 2012 at 7:40 PM
Edited May 28, 2012 at 7:42 PM

How are you trying to represent the dictionary in JSON?

{ 'key1': 'value1', 'key2': 'value2' }

And is this to or from MVC3?

You will likely have to play with:

// use JSON.NET to deserialize object to a dynamic (expando) object
            Object JSONObject;
            // if we start with a "[", treat this as an array
            if (JSONReader.TokenType == JsonToken.StartArray)
                JSONObject = JSONSerializer.Deserialize>(JSONReader);
            else
                JSONObject = JSONSerializer.Deserialize(JSONReader);

To get it to work :)

May 28, 2012 at 7:44 PM

Something along the lines of:

JSONObject = JSONSerializer.Deserialize<Dictionary<String, ExpandoObject>>(JSONReader);
Would be my guess.

May 29, 2012 at 8:58 AM

Actually my point is that there is no easy way to distinguish object and dictionary in JsonNetValueProviderFactory to return proper values in IValueProvider.

In this case only custom model binder or meta-property in json are available as options.

Jun 12, 2012 at 11:44 AM

This was helpful

Jul 3, 2013 at 6:59 PM
I am having trouble deserializing a custom object. I have created a CustomConverter class and added it to the property like so:
        <JsonConverter(GetType(TestJsonConverter))>
        Public Property Item As TestItem
            Get
                Return _item
            End Get
            Set(ByVal value As TestItem)
                item = value
            End Set
        End Property
When I step through the code in the JsonNetValueProviderFactory class, it does not reach my custom converter class. I am able to serialize it using the custom converter manually, but when it doesn't work in the GetValueProvider function.
Sep 26, 2013 at 10:17 AM
Edited Sep 26, 2013 at 10:18 AM
This lines may be a problem
if (this.Data == null)
    return;
So a empty response is generated instead of "null", what jQuery consider a fail.
Apr 15 at 8:52 PM
Doesn't seem to handle dates or nested dictionaries
Aug 15 at 7:31 PM
This has some issues with enums and the exact value not serializing if its a null enum property.
Aug 24 at 8:43 PM
One more thing - line with

// make a new Json serializer
JsonSerializer JSONSerializer = new JsonSerializer();

should look like (in 2014-08-24 ;) )
// make a new Json serializer
var jsonSerializer = JsonSerializer.CreateDefault();


so construction in Global.asax for example

protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);

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

will work ;)