Introduction to the queue for the queue ...
I have a resource defined in
http://my-awesome-product.com/api/widgets/3
which is a widget with an id of 3. In the Web API, I would define a controller to serve this resource as follows:
public class WidgetsController : ApiController { public Widget Get(int id) { return new Widget(...); } }
Now the Widget class can potentially be quite large, and it wants to save bandwidth from the database to the web server and from the web server to the client, I create several DTO classes that contain a limited number of fields from the total amount of Widget . For instance:
public class WidgetSummary { public int Id { get; set; } public string Code { get; set; } public string Description { get; set; } public decimal Price { get; set; } } public class FullWidgetForEditing { public int Id { get; set; } public string Code { get; set; } public string Description { get; set; } public decimal Weight { get; set; } public decimal Price { get; set; } public Color Color { get; set; } public decimal Width { get; set; } public decimal Height { get; set; } public decimal Depth { get; set; } } public class WidgetForDropDownList { public int Id { get; set; } public string Code { get; set; } }
With these views, the Widget client requests WidgetSummary to display the entire Widget in the system grid, it requests FullWidgetForEditing on the edit page of a specific Widget , and it requests WidgetForDropDownList for use in the drop-down list on the order form.
From what I understand in REST, there must be one URL to access Widget , since it is the only resource, regardless of its presentation, and the client must specify Accept headers with a media type parameter to extract Widget in different forms, for example my-awesome-product.type=widgetsummary , my-awesome-product.type=fullwidgetforediting and my-awesome-product.type=widgetfordropdownlist .
I could achieve this in the Web API by checking the request headers like this:
public class WidgetsController : ApiController { public object Get(int id) { if (Request.Headers.Accept.Any(x => x.Parameters.Any(y => y.Name == "my-awesome-product.type" && y.Value == "widgetsummary"))) { return new WidgetSummary(...); } else if (Request.Headers.Accept.Any(x => x.Parameters.Any(y => y.Name == "my-awesome-product.type" && y.Value == "fullwidgetforediting"))) { return new FullWidgetForEditing(...); } else if (Request.Headers.Accept.Any(x => x.Parameters.Any(y => y.Name == "my-awesome-product.type" && y.Value == "widgetfordropdownlist"))) { return new WidgetForDropDownList(...); } throw new HttpResponseException(HttpStatusCode.NotAcceptable); } }
However, this is quickly becoming erratic as the number of types grows and makes unit testing difficult. I would really like to do the following:
public class WidgetsController : ApiController { [HttpGet(MediaType = "my-awesome-product.type", MediaTypeValue = "widgetsummary")] public WidgetSummary GetWidgetSummary(int id) { return new WidgetSummary(); } [HttpGet(MediaType = "my-awesome-product.type", MediaTypeValue = "fullwidgetforediting")] public FullWidgetForEditing GetFullWidgetForEditing(int id) { return new FullWidgetForEditing(); } [HttpGet(MediaType = "my-awesome-product.type", MediaTypeValue = "widgetfordropdownlist")] public WidgetForDropDownList GetWidgetForDropDownList(int id) { return new WidgetForDropDownList(); } }
And you have an API for a specific action method based on a media type. I explored the ApiControllerActionSelector override, however I started pulling out a lot of the existing code for this attribute because I really need the default action selection, but in case of ambiguous actions, I want to filter the list of actions based on the media type. I really want this to be non-intrusive, so that I can mix convention routing, standard attribute routing (which is in Web API v2) and this hypothetical attribute routing in the same controller, if necessary.
A question of time: is it possible to implement such a routing strategy using the Web API in its current form? Do I have to completely override ApiControllerActionSelector to achieve this (and then make sure my reevaluation remains up to date)?