views:

160

answers:

2

Hi folks,

i've got an action method like the following

public JsonResult Index(string version)
{
   .. do stuff, return some data v1 or v2. Default = v2.
}

So, this action method returns some data, which can be formatted either as Version 1 or Version 2 (whatever output that is ... just know that they are schemantically different).

So, when a user wants to call access this resource, they the following :

http://www.blah.com/api/Index

nothing too hard.

they can also do this...

http://www.blah.com/api/Index?version=1.0

BUT, is it possible to make it so that the user can use the query string params version or v

eg. http://www.blah.com/api/Index?v=1.0 

and this will populate the version parameter in the ActionMethod. Possible?

+1  A: 

I'm guessing you could manipulate the action method parameter(s) using an action filter.

Basically just check for a 'v' in the QueryString collection, and if it exists, throw it into the ActionParameters collection.

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    var version = filterContext.HttpContext.Request.QueryString["v"];
    if (!string.IsNullOrEmpty(version))
        filterContext.ActionParameters["version"] = version;
}

HTHs,
Charles

EDIT: Making it a bit more generic...

public class QueryStringToActionParamAttribute : ActionFilterAttribute
{
    private string _queryStringName;
    private string _actionParamName;

    public QueryStringToActionParamAttribute(string queryStringName, string actionParamName)
    {
        _queryStringName = queryStringName;
        _actionParamName = actionParamName;
    }

    public override void OnActionExecuting(ActionExecutedContext filterContext)
    {
        var queryStringValue = filterContext.HttpContext.Request.QueryString[_queryStringName];
        if (!string.IsNullOrEmpty(queryStringValue))
        {
            filterContext.ActionParameters[_actionParamName] = queryStringValue;
        }
    }
}

Then you could call it like:

[QueryStringToActionParam("v", "version")];
Charlino
Goo Idea. I wonder if it is possible to make it a bit more generic and pass in the *name* of the querystring value (ie. 'v') and then the *name* of the actionparamater (ie. version). So then we could use this action filter multiple times and on multiple action methods .. ?
Pure.Krome
Good idea, see my edit.
Charlino
sauce of awesome.
Pure.Krome
A: 

An alternate way to handle the versioning of the API is to actually have different versions of the controller for each API version, that way you dont need to have all the checking for every version number in each action method. Each controller is only applicable to one version of the API.

Its cleaner (IMO) to me handle the versioning at the route time rather than action time. You can do this with a routing constraint to check for the version number.

In the below example, the controller V10 and V20 can only be routed to if the route contraint passed - i.e. the header was present, if no header the default (which is v2).:

routes.MapRoute(
            "EmployeeListingv1",
            "employees",
            new { controller = "V10Employees", action = "Index" },  // Parameter defaults
            new { ApiV = new ApiVersionConstraint(ApiVersion.Version10) }
    );

routes.MapRoute(
                "EmployeeListingv2",
                "employees",
                new { controller = "V20Employees", action = "Index" },  // Parameter defaults
                new { ApiV = new ApiVersionConstraint(ApiVersion.Version20) }
        );

You could do it using the query string to pass the version like you are currently doing and just change to a route constraint, however I've found it easier to maintain using an optional header in the request. (its also more "RESTful' without getting into that whole debate). No header means default (latest) version of the API.

Sample API Version constraint:

/// <summary>
/// Enable routing of requests to controllers based on the 
/// API version contained in the header.
/// </summary>
public class ApiVersionConstraint : IRouteConstraint   
{
    const string VersionHeader = "X-MY-API-NAME-HERE-VERSION";

    private ApiVersion _version = ApiVersion.Unsupported;

    public ApiVersionConstraint(ApiVersion version)
    {
        this._version = version;
    }

    #region IRouteConstraint Members

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        string vers = string.Empty;

        if (httpContext.Request.Headers[VersionHeader] != null)
        {
            vers = httpContext.Request.Headers[VersionHeader];
        }
        else
        {
            vers = "2.0"; // set default here.
        }

        ApiVersion fromHeader = ApiVersion.Unsupported;

        switch (vers)
        {
            case "1.0":
                {
                    fromHeader = ApiVersion.Version10;
                    break;
                }
            case "2.0":
                {
                    fromHeader = ApiVersion.Version20;
                    break;
                }

            default:
                {
                    fromHeader = ApiVersion.Unsupported;
                    break;
                }
        }

        return fromHeader == _version;

    }

    #endregion
}
RM
that's a pretty kewl idea, actually :) i really like it. But with header values, i can't just call the api from a browser, though .. right?
Pure.Krome