ASP.NET MVC – Create easy REST API with JSON and XML

So I just hopped on the ASP.NET MVC bandwagon. As my first task, I undertook custom Action Filters for returning either JSON or XML as determined by the HTTP Request. Fortunately Omar AL Zabir did most of the work for creating a RESTful API with ASP.NET MVC.

JSON and XML Action Filter Code

The following is a filter which makes the whole thing much cleaner. The filter looks for Content-Type headers in the HTTP request. If it matches text/xml then Plain Old XML (POX) is returned and if it matches application/json the output is JSON. This eliminates the need to write separate actions for JSON/XML and Views.

using System;
using System.Web;
using System.Web.Mvc;
using System.IO;
using System.Xml;
using System.Text;
using System.Collections.Generic;
using System.Web.Script.Serialization;
using System.Runtime.Serialization;
using System.Reflection;
using System.Reflection.Emit;

namespace AleemBawany.ActionFilters
{
  public class JsonPox : ActionFilterAttribute
  {
    private String[] _actionParams;

    // for deserialization
    public JsonPox(params String[] parameters)
    {
      this._actionParams = parameters;
    }

    // SERIALIZE
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      if (!(filterContext.Result is ViewResult)) return;

      // SETUP
      UTF8Encoding utf8 = new UTF8Encoding(false);
      HttpRequestBase request = filterContext.RequestContext.HttpContext.Request;
      String contentType = request.ContentType ?? string.Empty;
      ViewResult view = (ViewResult)(filterContext.Result);
      var data = view.ViewData.Model;

      // JSON
      if (contentType.Contains("application/json") || request.IsAjaxRequest())
      {
        using (var stream = new MemoryStream())
        {
          JavaScriptSerializer js = new JavaScriptSerializer();

          String content = js.Serialize(data);
          filterContext.Result = new ContentResult
          {
            ContentType = "application/json",
            Content = content,
            ContentEncoding = utf8
          };
        }

      }

      // POX
      else if (contentType.Contains("text/xml"))
      {
        // MemoryStream to encapsulate as UTF-8 (default UTF-16)
        // http://stackoverflow.com/questions/427725/
        //
        // MemoryStream also used for atomicity but not here
        // http://stackoverflow.com/questions/486843/
        using (MemoryStream stream = new MemoryStream(500))
        {
          using (var xmlWriter =
            XmlTextWriter.Create(stream,
              new XmlWriterSettings()
              {
                OmitXmlDeclaration = true,
                Encoding = utf8,
                Indent = true
              }))
          {

            new DataContractSerializer(
              data.GetType(),
              null, // knownTypes
              65536, // maxItemsInObjectGraph
              false, // ignoreExtensionDataObject
              true, // preserveObjectReference - overcomes cyclical reference issues
              null // dataContractSurrogate
              ).WriteObject(stream, data);
          }

          filterContext.Result = new ContentResult
          {
            ContentType = "text/xml",
            Content = utf8.GetString(stream.ToArray()),
            ContentEncoding = utf8
          };
        }
      }
    }

    // DESERIALIZE
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

      if (_actionParams == null || _actionParams.Length == 0) return;

      HttpRequestBase request = filterContext.RequestContext.HttpContext.Request;
      String contentType = request.ContentType ?? string.Empty;
      Boolean isJson = contentType.Contains("application/json");

      if (!isJson) return;
      //@@todo Deserialize POX

      // JavascriptSerialier expects a single type to deserialize
      // so if the response contains multiple disparate objects to deserialize
      // we dynamically build a new wrapper class with fields representing those
      // object types, deserialize and then unwrap
      ParameterDescriptor[] paramDescriptors =
          filterContext.ActionDescriptor.GetParameters();
      Boolean complexType = paramDescriptors.Length > 1;

      Type wrapperClass;
      if (complexType)
      {
        Dictionary parameterInfo = new Dictionary();
        foreach (ParameterDescriptor p in paramDescriptors)
        {
          parameterInfo.Add(p.ParameterName, p.ParameterType);
        }
        wrapperClass = BuildWrapperClass(parameterInfo);
      }
      else
      {
        wrapperClass = paramDescriptors[0].ParameterType;
      }

      String json;
      using (var sr = new StreamReader(request.InputStream))
      {
        json = sr.ReadToEnd();
      }

      // then deserialize json as instance of dynamically created wrapper class
      JavaScriptSerializer serializer = new JavaScriptSerializer();
      var result = typeof(JavaScriptSerializer)
              .GetMethod("Deserialize")
              .MakeGenericMethod(wrapperClass)
              .Invoke(serializer, new object[] { json });

      // then get fields from wrapper class assign the values back to the action params
      if (complexType)
      {
        for (Int32 i = 0; i < paramDescriptors.Length; i++)
        {
          ParameterDescriptor pd = paramDescriptors[i];
          filterContext.ActionParameters[pd.ParameterName] =
              wrapperClass.GetField(pd.ParameterName).GetValue(result);

        }
      }
      else
      {
        ParameterDescriptor pd = paramDescriptors[0];
        filterContext.ActionParameters[pd.ParameterName] = result;
      }
    }

    private Type BuildWrapperClass(Dictionary parameterInfo)
    {
      AssemblyName assemblyName = new AssemblyName();
      assemblyName.Name = "DynamicAssembly";
      AppDomain appDomain = AppDomain.CurrentDomain;
      AssemblyBuilder assemblyBuilder =
          appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
      ModuleBuilder moduleBuilder =
          assemblyBuilder.DefineDynamicModule("DynamicModule");
      TypeBuilder typeBuilder =
          moduleBuilder.DefineType("DynamicClass",
          TypeAttributes.Public | TypeAttributes.Class);

      foreach (KeyValuePair entry in parameterInfo)
      {
        String paramName = entry.Key;
        Type paramType = entry.Value;
        FieldBuilder field = typeBuilder.DefineField(paramName,
                    paramType, FieldAttributes.Public);
      }

      Type generatedType = typeBuilder.CreateType();
      // object generatedObject = Activator.CreateInstance(generatedType);

      return generatedType;
    }

  }
}

Last Updated: 28 March, 2010

Usage Example

To use this code in your Controller Action, you simply need to decorate it with the [JsonPox] attribute:

// Depending on HTTP Content-Type header
// this returns JSON, XML or the default View

[JsonPox]
public ActionResult Index()
{
        ArrayList stuff = new ArrayList();
        stuff.Add("Hello World");
        stuff.Add(999);
        stuff.Add(1.0001);
        ViewData.Model = stuff;
        return View();
}

Sample Output

If Content-Type: text/xml HTTP header is present the output is:

<ArrayOfAnyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <anyType xsi:type="xsd:string">Hello World</anyType>
  <anyType xsi:type="xsd:int">999</anyType>
  <anyType xsi:type="xsd:double">1.0001</anyType>
</ArrayOfAnyType>

For the HTTP header Content-Type: application/json the output is:

["Hello World",999,1.0001]

And if neither of those headers are present, the default View is returned.

Latest version: If you intend to use it, get the latest bits for JsonPoxFilter.cs here. The source is open so feel free to fork it or ping me if you want to get write access to the repo.

26 Responses



This article is no longer open for comments