9

I have a search Api I'm working on that needs to return search results in a block of Html (using styles the client has defined on their end). I would also like to return results in Json, for future Api stuff we'll eventually be using. Currently, the routes look like this:

/api/1/search/json?param1=blah&param2=blah&etc
/api/1/search/html?param1=blah&param2=blah&etc

For reference, the pattern here is /{area}/1/{controller}/{action}.

I like the look of some Api's I've seen that return results in different formats depending on the 'extension' they have in the url, a la:

/api/1/search.json?param1=blah&param2=blah&etc

However, I haven't figured out how to configure Asp.Net's Mvc routing to support this style. The general routing in ApiAreaRegistration.cs is:

context.MapRoute(
    "Api_default",
    "Api/1/{controller}/{action}/{id}",
    new { action = "Index", id = UrlParameter.Optional });

I have tried the following, defined above the general one, which doesn't work:

//search api
context.MapRoute(
    "searchJson",
    "api/1/{controller}.{action}",
    new { controller = "SearchController" });

How would I configure routing to enable the .format-style urls?

Dusda
  • 3,347
  • 5
  • 37
  • 58

2 Answers2

9
context.MapRoute(
    "Api_default",
    "{area}/1/{controller}.{format}",
    new { action = "Index", id = UrlParameter.Optional });

is probably what you want. Then you could just return the different results based on the argument passed in.

Within the context of an Api Area, the SearchController would look like this:

public class SearchController : Controller
{
    public ActionResult Index(string format, SearchModel search)
    {
        var results = searchFacade.SearchStuff(search);

        if(format.Equals("xml"))
            return Xml(results); //using an XmlResult or whatever
        if(format.Equals("html"))
            return View(results);
        return Json(results, JsonRequestBehavior.AllowGet);
    }
}
Dusda
  • 3,347
  • 5
  • 37
  • 58
sontek
  • 12,111
  • 12
  • 49
  • 61
  • This works great with my SearchController; it defaults to Index, and I return a different ActionResult depending on the format specified, defaulting to json. Eventually I'd like to figure out how to override the ActionResult returned using an ActionFilter so I don't have to do so manually in the action, but this'll do the trick for now. – Dusda Aug 26 '11 at 21:52
6

The routing is a bit tricky because you're inserting a required parameter after an optional paramter - in general, I recommend using the Accept-Type header, which is both more RESTful and less tricky to route. However, with certain clients this could be problematic.

The routing would have to take the form with id and without id into account:

context.MapRoute(
    "Api_default",
    "Api/1/{controller}/{action}/{id}.{format}",
    new { action = "Index" });

context.MapRoute(
    "Api_default_2",
    "Api/1/{controller}/{action}.{format}",
    new { action = "Index" });

Since the results are typically not different except for the output serialization, you might not want to route to different actions. A custom ActionResult could be helpful. This way, the different serialization logic can be centralized and is easy to extend.

public class RestResult<T> : ActionResult
{
    public T Data { get; set; }

    public RestResult(T data)
    {
        Data = data;
    }

    private string SerializeToJson()
    {
        MemoryStream ms = new MemoryStream();
        YourFavouriteJsonSerializer.SerializeToStream(Data, Data.GetType(), ms);
        var temp = Encoding.UTF8.GetString(ms.ToArray());
        return temp;
    }     

    public override void ExecuteResult(ControllerContext context)
    {
        string resultString = string.Empty;
        string resultContentType = string.Empty;

        // alternatively, use the route value dictionary
        // or the accept-type, as suggested.
        var extension = SomeExtensionParserMethod(context.RequestContext.HttpContext.Request.RawUrl);
        string result = string.Empty;
        if (extension == "json")
        {
            result = SerializeJson()
        }
        else if(...)
        // etc

                    context.RequestContext.HttpContext.Response.Write(resultString);
        context.RequestContext.HttpContext.Response.ContentType = resultContentType;
    }
}
mnemosyn
  • 45,391
  • 6
  • 76
  • 82
  • I like this approach, except I would probably use an ActionFilter so I don't have to return custom ActionResults. When I apply this style to the *rest* of the Api (beyond search), I'll look more into this. – Dusda Aug 26 '11 at 21:50
  • That doesn't make a lot sense to me: If you're using an `ActionFilter` to modify your result, you'd allow your controllers to return any `ActionResult`, such as a `FileResult, ContentResult` or `ViewResult`, most of which don't make sense in a REST API context. Moreover, you'll want to have access to some strongly typed object which you can serialize to. So you'd have to use `Model` of `ViewResult` (and you're forced to use its actual type, not the declared type), `JsonResult.Data` and for ContentResult, there's no solution other than to forward the string? Kinda hacky... – mnemosyn Aug 26 '11 at 22:25
  • That's because my eventual plan is to attempt an Api integrated into the site itself, similar to how Vanilla works. See this: http://forums.penny-arcade.com/categories/games-and-technology and then this: http://forums.penny-arcade.com/categories/games-and-technology.json . The style seems very natural to me, basically just serializing the model used behind the scenes and exposing it. There are obvious security concerns to consider, but as a general idea I really like it. You're probably right, though, I might need a CustomResult in the end (to prevent unnecessary rendering and stuff). – Dusda Aug 26 '11 at 22:54
  • I had trouble with that approach: correct error handling in IIS is a mess, and getting it right for end-users *and* API users seems very tricky, I didn't manage to get that straight. Anyway, that doesn't have to do anything with the type of your `ActionResults`? – mnemosyn Aug 26 '11 at 23:36
  • I went the ActionFilter way too. It determines from my accept types (taken the from the extension, and if not querystring, and if not accept-header) what content-type (if any) it can return. It works well, and is easy to extend: http://bit.ly/qTxDAQ – Bealer Oct 20 '11 at 11:12