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.
Get to see that you finally jumped off of the ASP.NET webforms bandwagon, now when will you fall in love with ORM?
Well, with MVC going RTM it was hard to resist. Web forms are still good for what they do (VS designer, third-party controls, rapid prototyping with automatic handling of sessions states and postbacks) but for my present case, ASP.NET MVC is the better choice.
And now that ADO.NET Entity Framework has also gone RTM, I’m giving it a good, hard look but my inclination is toward Linq to SQL (No NHibernate for now due to lack of jump-start scenarios with MVC or VS 2008).
You should try SubSonic out I think that it flows with MVC a lot more as Rob Conery is now working on MVC and SubSonic.
Thanks, I will certainly check it out.
[…] previously published a JSON / POX Filter for MVC developers working with RESTful services. It works really well in that it allows a […]
Have a look at my series on creating a RESTful web service using MVC. I’ve implemented XSLT based views, a “Help” representation etc. Hopefully it will be of use to you!
I have perused through your posts but they seems overly complicated for something that should be fairly trivial. Regarding JSON/XML, in part 3 you handle it by having switch statements in your action methods which is tedious and repetitive with a management overhead–it should be done in a common location. Also I am not sure I agree with format type information as a query parameter (?format=json). It doesn’t seem RESTful because that can be inferred by HTTP headers.
The filter here is very easy to implement, you just need to decorate your action methods and that’s pretty much it. You don’t need to write any special code to handle JSON/POX–it just works automatically.
[…] was introduced to the various serialization options in .NET while trying to build the JSON and XML filter for ASP.NET MVC. In this post I’ll take a look at the different JSON serializers in .NET and the reasons to […]
Great tip but I found that when I run this on a POST action e.g.
[AcceptVerbs(HttpVerbs.Post)]
[JsonPox]
public ActionResult DeleteUser(Guid? id)
The ContentType is equal to “application/x-www-form-urlencoded”
Instead, I use request.AcceptTypes[0]
PS: I am using jquery to perform an ajax post e.g.
$.post(url,data,callback,"json")
The content type should not be form-urlencoded if you are serializing the form as json. If it is its will not be handled the by the filter. It’s possible the submit button triggers the submission and preempts the Javascript.
Thanks a lot for sharing this. I was not able to login into your svn repo to get the updated version. Do I need username/password?
I nuked that repository some time back but I have updated the code in the post to the latest version I had lying around on my machine. Hope it’s useful.
Ciao Aleem,
Used your previous jsonpox, updated to the last source code u provided but can’t compile: it tells me that in BuildWrapper class the Dictionary requires two type arguments.
private Type BuildWrapperClass(Dictionary parameterInfo)
what am I missing?
thanks, Filippo
Change to the Generic type:
Dictionary
You may also have to do this for the KeyValuePair, or change to var.
Dictionary fails to compile in VS 2010
Error 18
Using the generic type ‘System.Collections.Generic.Dictionary’ requires 2 type arguments
I just tryed to download the source code, but browser askes me for user name and password!? What should I do?
[…] Otra aproximación totalmente distinta (pero muy interesante) que usa un action filter para ello. Está en el blog de Aleem Bawany, en el post “ASP.NET MVC – Create easy REST API with JSON and XML”. […]
Hello!
I have recreated your actionfilter in VB.net. Everything runs, but the data is always null, it doesn’t matter if its text/html, text/xml or JSON. Does anyone have a clue why it always return null?
Even though no one knows the answer now, my vb code might help other people.
Here is my VB.net version: http://gist.github.com/604325
Greetings I recently finished reading through your blog and also I’m very impressed. I do have a couple questions for you personally however. Think you’re thinking about doing a follow-up putting up about this? Will you be going to keep bringing up-to-date too?
I bow down humbly in the presence of such grteneass.
Thanks for the great step-thru and code on github. I was shocked to not find more people doing this with ASP MVC, especially after the press that Web Hooks has been receiving over the last couple of years.
Reflection without cache? You’re nuts!
Great tool, but I am having an issue with MVC 3.0. When I create the object that has a property for the ICollection that references another set of objects, I found that the Serialization into XML and JSON failed. I found an article here that talks about disabling Lazy Loading during the serialization but that didn’t work either. Any suggestions? Is there a way to override the XML and JSON serialization methods? Or something even more elegant that could be added into the JsonPox class?
[…] Update: Community answers led to a fuller implementation for a filter for JSON/POX. […]
[…] JSON/XML I have written an XML/JSON Action Filter that makes it very easy to tackle without handling special cases in your action handler (which is […]
[…] Update: Community answers led to a fuller implementation for a filter for JSON/POX. […]