読者です 読者をやめる 読者になる 読者になる

しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Web API でバージョニングを実現する方法を調べた

一般的に Web API のバージョニングを実現する方法として、URL にバージョンを含める方法とクエリパラメータや HTTP ヘッダーでバージョンを指定する方法の 2 種類が使われています。

例を挙げると Twitter や Instagram の API は URL でバージョンを指定します。

https://api.twitter.com/1.1/statuses/user_timeline.json

https://api.instagram.com/v1/media/popular?client_id=CLIENT-ID

対して Azure の REST API では、クエリパラメータや HTTP ヘッダーでバージョンを指定します。

https://[search service name].search.windows.net/indexes?api-version=2015-02-28

今回は Twitter や Instagram ような URL ベースのバージョニングを、ASP.NET Web API で実現する方法を調べました。思ったよりめんどくさい感じでした。

IHttpControllerSelector を実装して実現

Web API の DefaultHttpControllerSelector はコントローラをクラス名だけでグルーピングしてしまうので、名前空間が異なっていたとしても同じものとして扱われてしまいます。

string controllerName = controllerTypeGroup.Key;

foreach (IGrouping<string, Type> controllerTypesGroupedByNs in controllerTypeGroup.Value)
{
    foreach (Type controllerType in controllerTypesGroupedByNs)
    {
        if (result.Keys.Contains(controllerName))
        {
            duplicateControllers.Add(controllerName);
            break;
        }
        else
        {
            result.TryAdd(controllerName, new HttpControllerDescriptor(_configuration, controllerName, controllerType));
        }
    }
}
ASP.NET MVC / Web API / Web Pages - Source Code

クラス名に V1 とか V2 と付ければ問題無いですが、やはり名前空間でバージョンを分けておきたいので IHttpControllerSelector を実装して対応します。

規約ベースで実装

最初に考えられるのは規約ベースでバージョンを決めてしまう方法だと思います。実際に MSDN Blog でもバージョンを URL ルーティングに追加し、名前空間にマッピングさせる方法が紹介されています。

ASP.NET Web API: Using Namespaces to Version Web APIs - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

規約ベースで書けるので追加の設定など必要なく、名前空間とコントローラを増やすだけで複数バージョンに対応出来ますし、サンプルコードが CodePlex で提供されているので、そのまま使うことが出来そうです。

http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/NamespaceControllerSelector/

規約ベースで開発している場合は、こっちを選んだ方がスムーズな気がします。

Attribute Routing で実装

既に ASP.NET Web API で Attribute Routing を使った開発をしている場合には、名前空間でコントローラを分けて RoutePrefix でバージョンを直接指定する方法も考えられます。

namespace SampleWebApi.Controllers.V1
{
    [RoutePrefix("v1/products")]
    public class ProductsController : BaseApiController
    {
        [Route]
        public async Task<IHttpActionResult> GetProduct()
        {
            return Ok();
        }
    }
}

残念ながら DefaultHttpControllerSelector の制限でこのままでは動作しませんし、サンプルコードで用意されている NamespaceControllerSelector を使っても対応出来ないので、自前で同じように実装しました。

重要な部分は SelectController メソッドなので、その部分だけ引っ張り出してきました。

public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    var routeData = request.GetRouteData();

    if (routeData == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    var subRoute = routeData.GetSubRoutes().FirstOrDefault();

    if (subRoute == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    var actions = (HttpActionDescriptor[])subRoute.Route.DataTokens["actions"];

    if (actions.Length == 0)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    return actions[0].ControllerDescriptor;
}

Attribute Routing を使った場合には MS_SubRoutes というキーが RouteData に含まれていて、更にその中に HttpActionDescriptor が入っているので辿っていきます。

最後に HttpControllerDescriptor を返せば、そのコントローラが使われるようになります。

ライブラリを使って実現

自分で IHttpControllerSelector を実装するのもめんどくさい場合には、手軽に公開されているライブラリを使うという方法もあります。

このライブラリを使えば URL と HTTP ヘッダーを使ったバージョニングの両方が使えるようになるみたいです。実際に使い方を紹介している方が居たのでリンクを載せておきます。

Versioning ASP.Net Web API | Culbertson Exchange

最近は Attribute Routing ばかり使っているので、IHttpControllerSelector を実装して解決しました。名前空間を無視するかどうかを設定で切り替えさせてほしかったですね。