Nine MVP's Blog

02/02/2013

ASP.NET MVC Series: High Performance JSON Parser for ASP.NET MVC

Filed under: ASP.NET, ASP.NET MVC, C#, javascript — Nine MVP @ 9:17 pm

กลับมาอีกครั้งกับบทความตอนใหม่ จากที่ผมได้พูดเปรยไว้ในโพสนึงในกลุ่ม ASP.NET & MVC Developers Thailand Group ว่าอย่าทนใช้ Serializer ของเดิมๆ จึงขอขยายความด้วยบทความตอนนี้

ในปัจจุบัน JSON ได้ถูกใช้งานกันอย่างกว้างขวางจากหลาย application ซึ่งเป็นที่นิยมบน HTTP ใช้ในการส่งข้อมูลกันไปมา Request/Response

โดยปกติใครที่เขียน MVC/JQuery บ่อยๆก็มักจะได้ใช้งาน JSON และแม้ว่าปกติก็ทำงานได้อยู่แล้ว #ทำงานได้ไม่ได้หมายความว่าทำได้ดีที่สุด#

ดังนั้นสำหรับตอนนี้เราจะมาแนะนำให้รู้จักเจ้า JavaScriptSerializer Class ที่มีอยุ่ใน .NET Framework และก็ถูกเรียกใช้เยอะหลายจุดเลยทีเดียวใน MVC โดยบทความตอนนี้จะนำ Serializer ที่ดีกว่าจะหาของที่ดีกว่ามาแทนที่ของเดิมโดยทำงานเร็วกว่าหลายเท่าตัว

Performance Issue:

ก่อนอื่นทำความรุ้จักกับ JavaScriptSerializer Class เป็นคลาสที่เอาไว้ช่วยทำการแปลงจาก .NET Object ไปเป็น JSON (Serialization) และใช้แปลง JSON ไปเป็น .NET Object (Deserialization) นั่นเอง

การใช้งานก็เรียกใช้แบบนี้

string jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

//Call Deserialize to .NET object
Person person = serializer.Deserialize<Person>(jsonIn);

//modify object value
person.Name = "ASP.NET MVC";
person.Salary = 10000;

//Call Serialize to JSON string
string jsonOut = serializer.Serialize(person);

ซึ่งหากไม่คิดอะไรมากก็ใช้งานได้แล้วครับ เพียงแต่อยากให้ดูผลการทดสอบของเว็บไซต์ที่ผมนำมาอ้างอิงกันก่อน J

The results (in milliseconds) as well as the average payload size (for each of the 100K objects serialized) are as follows.

Graphically this is how they look:



ref: http://theburningmonk.com/2012/08/performance-test-json-serializers-part-iii/

จากกราฟด้านบนจะเห็นได้ว่า JavaScripSerializer นั้นทำงานได้ช้ามาก (แต่ JayRock แย่สุด) เมื่อเทียบกับ library อื่นๆแล้ว แต่นี่ก็ยังไม่ใช่ผลการทดสอบของเรา

ผมจึงหยิบเอาตัวที่ใกล้ชิดกับเรามากที่สุดมาทำการทดสอบคือ JavaScriptSerializer, JSON.NET (มีอยู่ใน MVC project) และ ServiceStack.Text (ผลการทดสอบดีที่สุด

และจะนำมาใช้งานในบทความนี้)

JSON.NET : http://json.codeplex.com/

ServiceStack.Text : https://github.com/ServiceStack/ServiceStack.Text

 

TEST CASE:

การทดสอบจะใช้ Stopwatch ในการจับเวลาการทำงานโดย for loop 1,000,000 ครั้ง

โดยทดสอบทั้งการ Serialize และ Deserialize ดังนี้

int times = 1000000;
string output = "";


//JavaScriptSerializer Class
var startMSJson = Stopwatch.StartNew();
var msjs = new System.Web.Script.Serialization.JavaScriptSerializer();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = msjs.Serialize(testObject);
}
startMSJson.Stop();
output += "Javascript Serializer : " + startMSJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startMSJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = msjs.Deserialize<Person>(jsonIn);
}
startMSJson.Stop();
output += "Javascript Deserializer : " + startMSJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;



//JSON.NET
var startJSONNET = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = Newtonsoft.Json.JsonConvert.SerializeObject(testObject);
}
startJSONNET.Stop();
output += "JSON.NET Serializer : " + startJSONNET.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startJSONNET = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = Newtonsoft.Json.JsonConvert.DeserializeObject<Person>(jsonIn);
}
startJSONNET.Stop();
output += "JSON.NET Deserializer : " + startJSONNET.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;



//ServiceStack.Text
var startSSTJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = ServiceStack.Text.JsonSerializer.SerializeToString(testObject);
}
startSSTJson.Stop();
output += "ServiceStack.Text Serializer : " + startSSTJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startSSTJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = ServiceStack.Text.JsonSerializer.DeserializeFromString<Person>(jsonIn);
}
startSSTJson.Stop();
output += "ServiceStack.Text Deserializer : " + startSSTJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

 

Output

  • Javascript Serializer : 11589 ms
  • Javascript Deserializer : 7769 ms
  • JSON.NET Serializer : 3783 ms
  • JSON.NET Deserializer : 6641 ms
  • ServiceStack.Text Serializer : 2069 ms
  • ServiceStack.Text Deserializer : 1211 ms

หลังทดสอบพบว่าผลการทำงานของ JavaScriptSerializer นั้นแย่มากจริงๆ รองลงมา JSON.NET ก็ทำได้ดี แต่ ServiceStack.Text ทำได้ดีที่สุด ซึ่งวัตถุประสงค์หลักของบทความนี้ต้องการจะบอกว่าใน ASP.NET MVC มีการใช้งาน Serializer ตัวที่แย่ที่สุดนั้นเอง

และเราจะนำตัว ServiceStack.Text มาใช้งานทดแทนเจ้า JavaScriptSerializer กัน แต่ว่าถูกใช้งานอยู่ที่ไหนบ้างหละ?

 

Where is JavaScriptSerializer used in ASP.NET MVC?

ต่อไปจะพากันไปหาจุดที่มีการเรียกใช้เจ้า JavaScriptSerializer ซึ่งมีอยู่หลายจุด แต่ขอนำจุดที่สำคัญของระบบจริงๆ มาแก้ไขกันในบทความนี้ ซึ่งจะเน้นไปที่ Input และ Output ซึ่งมีอยู่ 2 จุดคือ

Model Binder (INPUT)

จุดแรก model binder จะทำหน้าที่จับเอา request โดยตรวจดู content type ว่าเป็นชนิด json จากนั้นก็นำค่าไปทำการ deserialize ไปเป็น .net object ตามคลาสที่ประกาศไว้ใน Action นั้นๆ เช่น

public JsonResult NewPerson(Person person)
{
return Json(new Person());
}

จากตัวอย่างด้านบน Person จะเป็นคลาสที่กำหนดไว้ใน NewPerson Action ของ Controller เมื่อมีการ request กลับมาที่ Controller/NewPerson ด้วยการ json post ตัวอย่างด้านล่างนี้

"{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }"

ก่อนที่จะเข้ามาใน NewPerson Action จะมีการสร้าง Person object จากส่งกลับเข้ามาโดยไปเรียก ModelBinder (หากไม่มี custom binder จะไปเรียก DefaultModelBinder มาทำงาน) โดยดักจับ content-type ว่าเป็น request ประเภทใด

ในกรณีนี้ส่งมาเป็น JSON ก็จะไปเรียกตัว JsonValueProviderFactory (ValueProviderFactory เป็น base class) ซึ่งเจ้านี่เองที่มีการใช้งาน JavaScriptSerializer Class ในการแปลง JSON มาเป็น object เพื่อส่งกลับมาใน Action

แกะดู code เจ้า JsonValueProviderFactory กัน

public sealed class JsonValueProviderFactory : ValueProviderFactory 

{

private static void AddToBackingStore(JsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value){ … }

private static object GetDeserializedObject(ControllerContext controllerContext)

{

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

return (object) null;


string input = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();



if (string.IsNullOrEmpty(input))

return (object) null;


else

return new JavaScriptSerializer().DeserializeObject(input);

}



public override IValueProvider GetValueProvider(ControllerContext controllerContext){ }


private static string MakeArrayKey(string prefix, int index) { }


private static string MakePropertyKey(string prefix, string propertyName) { }


private class EntryLimitedDictionary


{


………

private static int GetMaximumDepth()


{


NameValueCollection appSettings = ConfigurationManager.AppSettings;


if (appSettings != null)


{


string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");


int result;

if (values != null && values.Length > 0 && int.TryParse(values[0], out result))


return result;


}


return 1000;


}


}


}

จะพบว่าถูกเรียกใน GetDesirializedObject() (ซึ่งถูก GetValueProvider() เรียกอีกที) นั่นเอง ทีนี้เราจะมาสร้างของเราใช้งานเองครับ J

ผมสร้าง Class ขึ้นใหม่โดย Inherit เจ้า ValueProviderFactory และใช้เจ้า ServiceStack.Text ใน GetValueProvider() ตรงๆแบบนี้ โดยใช้ AsExpandoObject() Extension Method แทนการทำงานอื่นๆของ JsonValueProviderFactory

public sealed class JsonServiceStackValueProviderFactory : 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).BaseStream;

return new DictionaryValueProvider<object>(
ServiceStack.Text.JsonSerializer.DeserializeFromStream<Dictionary<string, object>>(reader).AsExpandoObject(),
CultureInfo.CurrentCulture);
}
}

มาดู Extension Method ตัวที่ว่ากันครับ

public static class CollectionExtensions
{
public static ExpandoObject AsExpandoObject(this IDictionary<string, object> dictionary)
{
var epo = new ExpandoObject();
var epoDic = epo as IDictionary<string, object>;

foreach (var item in dictionary)
{
bool processed = false;

if (item.Value is IDictionary<string, object>)
{
epoDic.Add(item.Key, AsExpandoObject((IDictionary<string, object>)item.Value));
processed = true;
}
else if (item.Value is ICollection)
{
var itemList = new List<object>();
foreach (var item2 in (ICollection)item.Value)
if (item2 is IDictionary<string, object>)
itemList.Add(AsExpandoObject((IDictionary<string, object>)item2));
else
itemList.Add(AsExpandoObject(new Dictionary<string, object> { { "Unknown", item2 } }));

if (itemList.Count > 0)
{
epoDic.Add(item.Key, itemList);
processed = true;
}
}

if (!processed)
epoDic.Add(item);
}

return epo;
}
}

การนำไปใช้งานก็ไปที่ Global.asax.cs วางโค้ดลงไปตาม

protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();

ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new JsonServiceStackValueProviderFactory());


}

การทำงานคือเราจะไป remove เจ้า JsonValueProviderFactory ออกจาก ValueProviderFactories Class และเพิ่ม JsonServiceStackValueProviderFactory อันใหม่ของเราเข้าไป

JsonResult (Output)

จุดที่สองจะเป็นการ render JSON string กลับออกไปยัง Client เรามาดูโค้ดของ JsonResult Class ที่ ASP.NET MVC ใช้อยู่ในปัจจุบัน

public class JsonResult : ActionResult
{

public override void ExecuteResult(ControllerContext context)
{

if (context == null)

throw new ArgumentNullException("context");

if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))

throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);

HttpResponseBase response = context.HttpContext.Response;

response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;

if (this.ContentEncoding != null) response.ContentEncoding = this.ContentEncoding;

if (this.Data == null) return;

JavaScriptSerializer scriptSerializer = new JavaScriptSerializer();

if (this.MaxJsonLength.HasValue)


scriptSerializer.MaxJsonLength = this.MaxJsonLength.Value;

if (this.RecursionLimit.HasValue)


scriptSerializer.RecursionLimit = this.RecursionLimit.Value;


response.Write(scriptSerializer.Serialize(this.Data));

}

}

ต่อไปเราจะสร้าง JsonResult ของเราเอง โดยสร้างคลาสใหม่ขึ้นมา แล้ว Inherit เจ้า JsonResult ให้กับคลาส

แล้วก็ทำการ override ExecuteResult() ใหม่ตามนี้


public class JsonServiceStackResult : JsonResult
{

public override void ExecuteResult(ControllerContext context)
{


HttpResponseBase response = context.HttpContext.Response;


response.ContentType = !String.IsNullOrEmpty(ContentType) ? ContentType : "application/json";



if (ContentEncoding != null)
{


response.ContentEncoding = ContentEncoding;


}

if (Data != null)
{


response.Write(ServiceStack.Text.JsonSerializer.SerializeToString(Data));


}


}


}

ตั้งชื่อซะยาวเรียกใช้คงลำบาก งั้นเราก็ไปสร้าง Base Controller ของเราไว้ใช้งานแทนเจ้า return Json(); แบบนี้


public abstract class MyBaseController : Controller
{


protected new JsonResult Json(object data, JsonRequestBehavior behavior)


{ return Json(data, "application/json", Encoding.UTF8, behavior); }


protected new JsonResult Json(object data)
{

return Json(data, "application/json", Encoding.UTF8, JsonRequestBehavior.DenyGet);


}



protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
{

return new JsonServiceStackResult
{

Data = data,


ContentType = contentType,


ContentEncoding = contentEncoding


};


}


}

ตอนจะใช้งานก็แค่นำ MyBaseController ไป inherit แทนที่ Controller Class แล้วค่อยเรียก Json() เหมือนปกติเดิม J

public class HomeController : MyBaseController
{


public JsonResult NewPerson(Person person)
{

return Json(new Person());

}

}

 
 
 

Conclusion

ในบทความนี้หวังว่าจะมีประโยชน์สำหรับผู้ที่ต้องการเพิ่มประสิทธิภาพให้กับตัว MVC App โดยบางท่านอาจจะยังไม่ทราบส่วนนี้ จนกว่าจะต้องทำ Performance tuning กันเพื่อเพิ่มประสิทธิภาพในการทำงานในจุดที่ช้าและทำงานได้ไม่ดี

และจากบทความตอนนี้ท่านสามารถนำสิ่งเหล่านี้ไปใช้งานได้ทั้ง ASP.NET MVC และ ASP.NET WEB API (ตัวนี้เหมาะที่จะเปลี่ยนใหม่เพราะทำงานกับ Data จำนวนมาก)

สำหรับตอนนี้ขอจบไว้เพียงเท่านี้ เจอกันใหม่ตอนหน้าครับ

 


SONY DSC

About Me:

Chalermpon Areepong : Nine (นาย)

Microsoft MVP Thailand ASP.NET

ASP.NET & MVC Developers Thailand Group :http://www.facebook.com/groups/MVCTHAIDEV/

Greatfriends.biz Community Leader : http://greatfriends.biz

Email : nine_biz-talk.net at hotmail dot com

Blog : https://nine69.wordpress.com

4 Comments »

  1. Love love

    Comment by chanupol — 03/02/2013 @ 6:11 am

  2. ลองเอาไปใช้ในโปรเจคปัจจุบันแล้วครับ

    Comment by go1f — 25/02/2013 @ 10:34 am

  3. When I have action method Create(ObjectType o1, ObjectType o2, int currentPage, int countOfPage) o1 i o2 object are always null in the action method. The parameters currentPage and countOfPage have values. This is the ajax method:

    $.ajax({
    type: “POST”,
    context: this,
    url: urlZaCuvanje,
    data: ko.mapping.toJSON({ “o1”: ko.mapping.toJS(this.o1), “o2”: ko.mapping.toJS(this.o2), currentPage: this.currentPage(), countOfPage: this.countOfPage() }),
    contentType: ‘application/json’,
    dataType: ‘json’,

    When I have action method Create(ObjectType o1) everything is fine. This is the ajax method:

    $.ajax({
    type: “POST”,
    context: this,
    url: urlZaCuvanje,
    data: ko.mapping.toJSON(ko.mapping.toJS(this.o1)),
    contentType: ‘application/json’,
    dataType: ‘json’,

    I think, the problem is with JsonServiceStackValueProviderFactory…

    Thank you in advance

    Comment by Dragan Mijailovic — 24/12/2015 @ 3:21 pm

    • Your JsonServiceStackValueProviderFactory can not prepare dictionary for ModelBinder when the action parameter has nested objects.
      I have class JsonNetValueProvider:

      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>(JSONReader);
      else
      JSONObject = JSONSerializer.Deserialize(JSONReader);

      // create a backing store to hold all properties for this deserialization
      Dictionary backingStore = new Dictionary(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(backingStore, CultureInfo.CurrentCulture);
      }

      private static void AddToBackingStore(Dictionary backingStore, string prefix, object value)
      {
      IDictionary d = value as IDictionary;
      if (d != null)
      {
      foreach (KeyValuePair 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;
      }
      }

      Method GetValueProvider of the class JsonNetValueProviderFactory creates a dictionary of 26031 items for just a hundred milliseconds (every property of the document item is a dictionary item. In web.config I set the value of the maxJsonDeserializerMembers parameter to 4194304). But, then it takes more than 20 seconds to the action method for saving the document.

      Is it so slow to receive JSON-formatted text and to model-bind the JSON text to parameters of action methods for a document with 1000 object items (every object item has 20 properties)?!

      Comment by Dragan Mijailovic — 25/12/2015 @ 7:28 pm


RSS feed for comments on this post. TrackBack URI

Leave a reply to Dragan Mijailovic Cancel reply

Blog at WordPress.com.