Creating a RESTful Web Service Using ASP.Net MVC Part 3 – Multiple Representations

October 2, 2008 00:10 by admin

This post continues my look at creating a RESTful web service using ASP.Net MVC Preview 5. The previous posts are

In this post I want to take the basic web service and add support for representations other than XHTML. I’m doing this because it is interesting and more legitimately, some representations might be more easily consumed by particular clients. I decided that my web service should support two extra representations: XML and JSON.

The first question is: how does the client indicate which representation they want to receive. My first version used the Accept header in the HTTP request. This allows a client to indicate the representations it can accept. For example passing “application/json” indicates the client can receive a JSON representation.

As pointed out by Aaron Lerch, browsers pass in multiple acceptable representations, and in fact FireFox seems to indicate it prefers XML over XHTML. However, the server does not have to strictly obey the Accept header contents. So the FireFox problem could be solved by the server overruling the client’s order of preference, and instead “preferring” to serve XHTML if that is at all acceptable to the client. So I wrote some code similar to this to show it was possible:

string[] acceptTypes = ControllerContext.HttpContext.Request.AcceptTypes;

 

if (acceptTypes.Length > 0)

{

    if (Array.IndexOf(acceptTypes, "application/xhtml+xml") > -1)

    {

        // Serve XHTML

    }

    else if (Array.IndexOf(acceptTypes, "application/json") > -1)

    {

        // Serve JSON

    }

    else if (Array.IndexOf(acceptTypes, "application/xml") > -1)

    {

        // Serve XML

    }

}

Instead of looping through the AcceptTypes looking for the first representation the service can serve, it checks if the client can accept the default representation and if not falls back to the next representation and so on.

After implementing it, I threw away this code. Instead I decided to follow Richardson and Ruby’s advice (again) and put the representation in the URI. The advantage is that the URI contains everything the server needs to service the request in one location. A second advantage for me is that this approach allows non-standard representations to be requested, something I’ll do in my next post. So my web service will look for a “format” variable in the QueryString collection:

var method = HttpContext.Request.QueryString["format"] ?? string.Empty;

 

// Decide which method to use

switch (method.ToLowerInvariant())

{

    case("xml"):

        // Serve XML

        break;

 

    case("json"):

        // Serve JSON

        break;

 

    default:

        // Serve XHTML

        break;

}

Having specified how the client will inform the server which representation is required, I needed to decide how the server will change the representation from the default XHTML. As I was reading the MVC Preview 5 release notes I came across the changes to Action Filters. Action Filters allow you to decorate an “Action” method with an attribute which can pre and post filter the inputs and outputs of that Action. This seemed like my solution.

I wrote a RepresentationFilterAttribute derived from ActionFilterAttribute. In its override of the OnActionExecuted method, it took the ViewResult created by the Controller and replaced it with either a JsonResult or a ContentResult (containing the View Data serialised using the XmlSerializer).

It all worked, but I wasn’t happy with it. I had two issues: it seemed messy (for example a ViewResult was always created then potentially discarded) and it would mean decorating every Action method in my entire web service with this new attribute. I decided it would be better to write a base Controller from which my web service controllers could all derive.

The MVC Controller class already has helper methods that create particular ActionResults (e.g. Json() will create a JsonResult) but these rely on each individual action deciding which representation to use. So the first addition to my new base controller was a helper method, that selected which type of ActionResult to return based on the format variable:

protected internal virtual ActionResult SelectActionResult(string name, object model)

{

    ActionResult result = null;

 

    if (model != null)

    {

        base.ViewData.Model = model;

    }

 

    // Get the (lowercase) render mode from the querystring

    var method = HttpContext.Request.QueryString["format"] ?? string.Empty;

 

    // Decide which method to use

    switch (method.ToLowerInvariant())

    {

        case("json"):

            result = Json(model);

            break;

 

        default:

            result = View(name, model);

            break;

    }

 

    return result;

}

I provided some overloads to mimic the overloads available for the existing ActionResult helper methods. Once the last line of the GuidController’s action was updated to call SelectActionResult instead of View, the web service was able to serve JSON representations as well as XHTML by just changing the URI:

     http://localhost/RESTfulMVCWebService/Guid?count=15&format=json

The next step was trickier… serving an XML representation. In MVC Preview 5 there is no built in XmlResult class. In SelectActionResult I could have simply serialised the model parameter (using XmlSerializer) then packaged the Xml string into a generic ContentResult. However, one guideline for good design of services is not to expose your internal schema directly to the outside world… always have a mapping, however light, so that you can evolve internal and external schemas independently. Therefore I decided to always run my serialised object through a transform… what I actually needed was not XmlResult but XsltResult…. which also does not exist in MVC Preview 5!

I tried to keep my expectations of XsltResult high, it needed to:

  • Be supported within the controller in the same way that ViewResult, JsonResult etc. are supported;
  • Find the XSLT file to execute against the serialised object in much the same way as ViewResult would find an ASPX file;
  • Use caching of compiled templates to speed everything up.

So XsltResult was born. Here is its overridden ExecuteResult method:

public override void ExecuteResult(ControllerContext context)

{

    if (context == null)

    {

        throw new ArgumentNullException("context");

    }

 

    if (string.IsNullOrEmpty(this.XsltName))

    {

        this.XsltName = context.RouteData.GetRequiredString("action");

    }

 

    // Turn the data into an xml string, then transform into the format expected externally

    string dataAsInternalXml = Xml.ObjectToXml(Data, false);

 

    XslCompiledTransform xslCompiledTransform = FindTransform(context);

    string dataAsExternalXml = Xslt.Apply(dataAsInternalXml, xslCompiledTransform);

 

    context.HttpContext.Response.ContentType = "application/xml";

    context.HttpContext.Response.Write(dataAsExternalXml);

}

It checks the input parameter and then, if required, supplies a default transform name. A helper function is used to serialise the Data object to an Xml string. A compiled transform is then used to convert the Xml from the internal schema to the external schema.

So how does the ExecuteResult find the template? Well I borrowed the code used to find ASPX pages. I created a class called VirtualPathProviderXsltEngine which was based on the MVC class VirtualPathProviderViewEngine. The main changes are:

  • Rather than look for ASPX or ASCX files it looks find XSLT files in the same locations;
  • Instead of caching file locations, it caches the compiled version of the XSLT.

Due to time constraints I didn’t fully implement the infrastructure for finding XSLT files… for example, I dropped the ability to add different lookup mechanisms (i.e. being able to supply alternate XsltEngines).

Now this framework is in place, we have a read only web service that can serve results in one of three representations: XHTML; XML and JSON. Before moving on to other features, my next post will cover one more representation. That additional representation will allow you to test the web service.

My zipped solution can be downloaded:

Note I have changed some of the class names and method names from those mentioned in my post so far. Also the zip file does not contain the MVC DLLs, the .proj file references them in their default install location so you may need to remove then re-add these references.


Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading