ASP.NetのApiControllerを自分で選択する

ASP.NetのApiControllerはシステムがURL等によって自動で選択してくれます。

例えばhttp://hogehoge.sample.com/api/HogeHoge/1をGETで通信するとApiControllerを継承したHogeHogeControllerのGetから始まるintを引数に持ったメソッドが自動で呼び出されます。

今回はこの自動の部分を自分で実装しようという試みです。

これをする理由

大きな理由はデフォルトの選択方法ではアセンブリに含まれるすべてのApiControllerを継承したクラスが選択されてしまい、しかもクラス名をその中で重複させることが出来ません。

(そうそうないだろうけど)なんらかの外部ライブラリにApiControllerを継承したクラスが含まれてしまっていたり。

特にありがちなのが、バージョンを名前空間で切り分けて同じコントローラを複数定義してしまった場合です。

// 初期バージョン(移行期間の為にしばらく残す)
namespace V1 {
    public class HogeController {}
}

// メジャーアップデートバージョン
namespace V2 {
    public class HogeController {}
}

旧バージョンを残すためにこのようにしたいのですが、このような場合HogeControllerはエラーが発生し呼び出すことが出来なくなります。

ざっくりしたメソッド選択までの仕組み

実装の前にまずASP.Netがどのようにしてコントローラとメソッドを選択しているかの流れを書きます。

ルーティング情報を生成する

事前に(Global.asaxで)設定したルーティング設定とURL、メソッドを元にルーティング情報を設定します。

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

このようなルーティング設定が存在すると思います。

この設定とURLによって、その{controller}にあたる部分と{id}に当たる部分が解析されます。

これはApiControllerControllerContext.RouteData.Values[“controller”]等でAPI中からでも取得できます。

まず、最初に行われるのはこの処理です。この部分はノータッチで任せます。

ただし、コントローラやメソッドの選択にcontrollerとid以外の情報が必要なら、routeTemplateに記述します。

Controllerの選択

次に先ほどのルーティング情報を元にコントローラを選択します。

デフォルトではRouteData.Values[“controller”]の後ろに「Controller」を付けた名前のApiControllerを継承したクラスが選択されるようになっています。

 

このコントローラの選択はControllerSelectorクラスが担っています。

このクラスを自作する事で選択されるコントローラを変更する事が出来ます。

メソッドの選択

最後に選ばれたコントローラのどのメソッドを使うかの選択をします。

{action}を付けていればメソッド名、つけていなければHttpメソッドと同じプレフィックスを持ったメソッドが選択されるようになっています。

 

このメソッドの選択はActionSelectorクラスが担っています。

同様にこのクラスを自作する事でControllerSelectorが選んだクラスのどのメソッドが呼び出されるかを変更することが出来ます。

ただし、ActionSelectorの実装は結構面倒な上に必要性があまりないので、今回は記事なしです。

※こちらはHttpActionDescriptorも自作する必要があります。

ControllerSelectorを自作する

まずはDefaulthttpControllerSelectorを継承します。

    public class CustomizedControllerSelctor
        : DefaultHttpControllerSelector
    {
        private static HttpConfiguration configuration_;

        public CustomizedControllerSelctor( HttpConfiguration configuration )
            : base( configuration )
        {
            configuration_ = configuration;
        }
    }

DefaultHttpControllerSelectorは名前のとおり、特に何も指定していないときのコントローラの選択を行っているクラスです。

細かい実装は面倒なので、クラスを選ぶところだけを実装します。

コントローラの選択を行うメソッドはSelectControllerですので、これをオーバーライドします。

プロトタイプは

public HttpControllerDescriptor SelectController( HttpRequestMessage request )

となっています。

引数のHttpRequestMessageはクライアントからの通信内容です。

ルーティング情報などはすでにこの中に格納されています。

 

返り値のHttpControllerDescriptorはクラスのTypeと名前を格納しているだけの物です。

つまり、requestの内容からTypeを導きだして、返却すればOKです。

まずは超簡単なルーティング情報を無視したサンプルです。

    public class CustomizedControllerSelector
        : DefaultHttpControllerSelector
    {
        private static HttpConfiguration configuration_;

        public CustomizedControllerSelctor( HttpConfiguration configuration )
            : base( configuration )
        {
            configuration_ = configuration;
        }

        public override HttpControllerDescriptor SelectController( HttpRequestMessage request )
        {
            return new HttpControllerDescriptor(configuration_, "Sample", typeof( SampleController ));
        }
    }

SampleControllerは事前に定義されているとします。

ただ、このままでは作ったセレクターが利用されないのでシステムに登録します。

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Services.Replace(typeof(IHttpControllerSelector), new CustomizedControllerSelector(config));
        }
    }

Services.Replaceで既存のDefaultHttpControllerSelectorを今回作ったCustomizedControllerSelectorに変更しています。

 

ブレークポイントを貼ってみたりして処理が通るか確認してみてください。

デフォルトのapi/HogeHoge/1はもちろんapi/FugaFuga/1のようなコントローラ名の部分を変更してもSampleControllerが利用されるようになっていると思います。

 

応用編:名前空間がV2のものを検索する

アセンブリ内のすべてのクラスを検索して、その中から名前空間V2のものをピックアップします。

        private static Dictionary<string, HttpControllerDescriptor> Initilize()
        {
            var assembly = Assembly.GetExecutingAssembly();
            return (from x in assembly.GetTypes() where !string.IsNullOrEmpty(x.Namespace) && x.Namespace.EndsWith( "V2" ) select x)
                        .ToDictionary(
                            ( x ) => x.Name.Substring( 0, x.Name.Length - "Controller".Length ),    // 末尾のControllerを消す
                            ( x ) => new HttpControllerDescriptor( configuration_, x.Name, x )
                        );
        }

こんな感じです。

Assembly.GetTypesですべてのクラスを取得しています。

その中から名前空間がV2で終わるものを検索して、名前とHttpControllerDescriptorを組んだ連想配列を作っています。

それを踏まえて

    public class CustomizedControllerSelector
        : DefaultHttpControllerSelector
    {
        private static HttpConfiguration configuration_;

        private static Lazy<Dictionary<string, HttpControllerDescriptor>> controllers_ = new Lazy<Dictionary<string, HttpControllerDescriptor>>( Initilize );

        public CustomizedControllerSelector( HttpConfiguration configuration )
            : base( configuration )
        {
            configuration_ = configuration;
        }

        private static Dictionary<string, HttpControllerDescriptor> Initilize()
        {
            var assembly = Assembly.GetExecutingAssembly();
            return (from x in assembly.GetTypes() where !string.IsNullOrEmpty(x.Namespace) && x.Namespace.EndsWith( "V2" ) select x)
                        .ToDictionary(
                            ( x ) => x.Name.Substring( 0, x.Name.Length - "Controller".Length ),    // 末尾のControllerを消す
                            ( x ) => new HttpControllerDescriptor( configuration_, x.Name, x )
                        );
        }

        public override HttpControllerDescriptor SelectController( HttpRequestMessage request )
        {
            HttpControllerDescriptor type;
            var controllerName = request.GetRouteData().Values["controller"].ToString();
            if ( controllers_.Value.TryGetValue( controllerName, out type ) ) {
                return type;
            }
            throw new HttpException( 404, "コントローラないよ" );
        }

        public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return controllers_.Value;
        }
    }

Lazyを使って遅延初期化+スレッドセ―フにしています。

SelectControllerでは連想配列から名前があるかを調べて返しています。

 

ついでにGetControllerMappingも実装しています。

HelpPage等、コントローラに何があるかを取得する際にこのメソッドがよばれる為コチラも実装しておいた方が良いです。なくても最低限動きますが、連想配列返すだけですし……

 

実際に名前空間V2やそれ以外を作って試してみてください。

V2直下のものだけしか呼ばれないようになっています。

 

 

タイトルとURLをコピーしました