Custom JsonResult Class for ASP.Net MVC to Avoid MaxJsonLength Exceeded Exception
January 3, 2011 30 Comments
Shortly before Christmas, I was working on an application that sent a large data set to jqGrid using an Ajax JSON stream in ASP.Net MVC. We were trying out using a “get everything once” model where all of the I/O happens when jqGrid is initialized or reset and then all of the data is available on the client for fast sort and filter. It was working well with test data of ~5-6K rows until a colleague checked in a change that added a new column. Suddenly my jqGrid was blank while hers (with a different, somewhat smaller, set of test data) was fine. Usually this sort of behavior from jqGrid indicates that the JSON was broken. Sure enough when I fired up my Fiddler HTTP debugger, I saw an error 500 for the JSON ajax query.
Server Error in ‘/’ Application.
Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.
Source Error:
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below. |
Stack Trace:
|
Version Information: Microsoft .NET Framework Version:2.0.50727.4952; ASP.NET Version:2.0.50727.4955
The error is inside of a call from MVC to BCL
It turns out that the exception happens inside of JsonResult.ExecuteResult(ControllerContext context). What is going on in there? Well, fortunately, ASP.Net MVC code is open source (MS-PL) and the .NET Framework class library source code is available for reference as well. Let’s take a look.
The meat of JsonResult.ExecuteResult(ControllerContext)
if (Data != null) { JavaScriptSerializer serializer = new JavaScriptSerializer(); response.Write(serializer.Serialize(Data)); }
The meat of JavaScriptSerializer.Serialize(object obj)
public string Serialize(object obj) { return Serialize(obj, SerializationFormat.JSON); } private string Serialize(object obj, SerializationFormat serializationFormat) { StringBuilder sb = new StringBuilder(); Serialize(obj, sb, serializationFormat); return sb.ToString(); } internal void Serialize(object obj, StringBuilder output, SerializationFormat serializationFormat) { SerializeValue(obj, output, 0, null, serializationFormat); // DevDiv Bugs 96574: Max JSON length does not apply when serializing to Javascript for ScriptDescriptors if (serializationFormat == SerializationFormat.JSON && output.Length > MaxJsonLength) { throw new InvalidOperationException(AtlasWeb.JSON_MaxJsonLengthExceeded); } }
JavaScriptSerializer completely serializes object obj into StringBuilder output and then, having allocated that memory, checks the size of the StringBuilder and if it is larger than the MaxJsonLength property it throws an InvalidOperationException. JsonResult just creates a new JavaScriptSerializer and uses it so there is no way to change the default MaxJsonLength when using JsonResult in MVC. Since the memory is allocated before the InvalidOperationException is thrown, I’m not really clear what the point of MaxJsonLength is this deep in the framework. Surely whatever is going to use the JSON string would be in a better position to decide if the string returned by JavaScriptSerializer.Serialize() was too long to use?
Anyway, we have the problem isolated now for a solution. We need to implement our own ActionResult that will generate JSON while allowing the caller to twiddle the knobs on JavaScriptSerializer.
LargeJsonResult ActionResult class
using System; using System.Web.Script.Serialization; namespace System.Web.Mvc { public class LargeJsonResult : JsonResult { const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet."; public LargeJsonResult() { MaxJsonLength = 1024000; RecursionLimit = 100; } public int MaxJsonLength { get; set; } public int RecursionLimit { get; set; } public override void ExecuteResult( ControllerContext context ) { if( context == null ) { throw new ArgumentNullException( "context" ); } if( JsonRequestBehavior == JsonRequestBehavior.DenyGet && String.Equals( context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase ) ) { throw new InvalidOperationException( JsonRequest_GetNotAllowed ); } HttpResponseBase response = context.HttpContext.Response; if( !String.IsNullOrEmpty( ContentType ) ) { response.ContentType = ContentType; } else { response.ContentType = "application/json"; } if( ContentEncoding != null ) { response.ContentEncoding = ContentEncoding; } if( Data != null ) { JavaScriptSerializer serializer = new JavaScriptSerializer() { MaxJsonLength = MaxJsonLength, RecursionLimit = RecursionLimit }; response.Write( serializer.Serialize( Data ) ); } } } }
You can use return new LargeJsonResult(){ Data = data } from any Action method where you would have used return Json(data). Also, you have direct control over the MaxJsonLength and RecursionLimit properites of JavaScriptSerializer.
return new LargeJsonResult() { Data = output, MaxJsonLength = int.MaxValue };