ASP.NET MVC – Create easy REST API with JSON and XML
From: http://aleembawany.com/2009/03/27/aspnet-mvc-create-easy-rest-api-with-json-and-xml/
Mar 27th, 2009
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:
< A rrayOfAnyType 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>
</ A rrayOfAnyType >
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.