Sinatra風にASP.NET - ASP.NETルーティング機能を使って

ASP.NET MVC を使えば、ASP.NETRails っぽい事ができますが、JSON を返すだけの処理とかに ASP.NET MVC を使うのは大げさすぎる感じがしています。

そのため、ASP.NETSinatra 風のフレームワークが無いか調べてみたのですが、なかなか良さそうなのが見つかりません。
いろいろ調べていく内に、ASP.NET ルーティングの機能で似たような事ができそうな気がしたので少し試してみました。

使用した環境は以下。(ただし、IDE を使わず自前でファイルを作成)

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20100920/ です。

ASP.NET ルーティング

ASP.NET ルーティングは URL のパターンとその処理方法を指定できる機能で、ASP.NET MVC でも利用されており、Web.config などに特別な設定をする必要も無いので、比較的容易に利用できると思います。

基本的には、Global.asax ファイルの Application_Start イベントで RouteTable クラスの Routes(static プロパティ)に URL のパターンと処理クラスを格納した Route オブジェクトを追加する事で設定を行います。

ASP.NET ルーティングの例(Global.asax.cs)
・・・
using System.Web.Routing;

namespace SampleApp
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            //ルーティング定義の追加
            RouteTable.Routes.Add(new Route(
                "{category}/{action}/{id}", new SampleRouteHandler()
            ));
        }
    }
}

URL パターンに一致すると該当する処理クラスが実行されます。

Route のインスタンス化で使用している URL パターン "{category}/{action}/{id}" は Sinatra における "/:category/:action/:id" です。

なお、Route で指定する URL パターンは / から始めない点に注意です。(実行時にエラーが発生します)

Sinatra 風に実装する ASP.NET ルーティング

それでは、Sinatra 風に ASP.NET ルーティングを実装できるようにするため、URL パターンとラムダ式を指定すれば RouteTable.Routes に Route が追加されるような仕組みを作る事にします。

なお、URL のパターン指定は ASP.NET ルーティングのものをそのまま使用するようにし、Sinatra の構文には準拠しません。


とりあえず、GET と POST に対応し ASP.NET ルーティングへの設定を行うクラスの実装は以下のようになりました。Func を使うことで RequestContext を引数にし string を戻り値にしたラムダ式を指定できるようにし、 Get や Post を System.Web.HttpApplication に対する拡張メソッドとして定義しました。

また、ASP.NET ルーティングでは、Route オブジェクトの Constraints で URL パラメータの値を制限したりできるようになっています。(今回は HTTP のメソッド GET や POST を指定するために利用)

CustomRouting.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Routing;

namespace Fits.Sample.Web.Routing
{
    //ASP.NET ルーティングへの設定を行うクラス
    public static class CustomRouting
    {
        //Get メソッドを HttpApplication への拡張メソッドとして定義
        public static void Get(this HttpApplication app, string pattern, Func<RequestContext, string> proc)
        {
            Action("GET", pattern, proc);
        }

        //Post メソッドを HttpApplication への拡張メソッドとして定義
        public static void Post(this HttpApplication app, string pattern, Func<RequestContext, string> proc)
        {
            Action("POST", pattern, proc);
        }

        public static void Action(string methodType, string pattern, Func<RequestContext, string> proc)
        {
            //ルーティングの設定を追加
            RouteTable.Routes.Add(new Route(pattern, new CustomRouteHandler(proc))
            {
                //HTTP Method による制約を指定
                Constraints = new RouteValueDictionary{{"httpMethod", new HttpMethodConstraint(methodType)}}
            });
        }

        private class CustomRouteHandler : IRouteHandler
        {
            private Func<RequestContext, string> proc;

            public CustomRouteHandler(Func<RequestContext, string> proc)
            {
                this.proc = proc;
            }

            public IHttpHandler GetHttpHandler(RequestContext requestContext)
            {
                return new CustomHttpHandler(this.proc, requestContext);
            }
        }

        private class CustomHttpHandler : IHttpHandler
        {
            private Func<RequestContext, string> proc;
            private RequestContext reqCtx;

            public CustomHttpHandler(Func<RequestContext, string> proc, RequestContext reqCtx)
            {
                this.proc = proc;
                this.reqCtx = reqCtx;
            }

            public bool IsReusable
            {
                get { return true; }
            }

            public void ProcessRequest(HttpContext context)
            {
                //proc の処理を実行
                string res = this.proc(this.reqCtx);

                if (res != null)
                {
                    //proc の処理結果を HTTP レスポンスに出力
                    context.Response.Write(res);
                }
            }
        }
    }
}

上記クラスを使って Global.asax 内で ASP.NET ルーティングの定義を行った例が以下です。

Global.asax.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;

using Fits.Sample.Web.Routing;

namespace Fits.Sample.Web
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // test/:index への POST に対する定義
            //(例) "test/1" への POST で WebForm1.aspx にリダイレクト
            this.Post("test/{index}", ctx =>
            {
                ctx.HttpContext.Response.Redirect("/WebForm1.aspx");
                return null;
            });

            // /:name/:index への GET に対する定義
            //(例) "test/1" への GET で "hello test - 1" という文字列を表示
            this.Get("{name}/{index}", ctx =>
            {
                var param = ctx.RouteData.Values;
                return string.Format("hello {0} - {1}", param["name"], param["index"]);
            });
        }

    }
}

なお、デフォルトで存在するファイルが優先される点に注意。
上記例で "test/default.html" を GET した場合、test/default.html ファイルが存在していればそのファイルの内容が表示される事になります。(ファイルが無ければルーティングの設定が適用され "hello test - default.html" と表示される)

一応、Sinatra っぽくなったかなと思います。
ASP.NET ルーティングは初めて使いましたが比較的容易に使えるので、色々と活用できそうな気がしています。