ASP.NETでRESTful Webサービスを実装してみる

休暇が終わり時間が取りづらくなりましたが,ちびちびと実装を始めています.
題材はブログシステムです.

URL設計

URL設計ですが,とりあえずGET系でこんな感じで考えています。

  1. /home.aspx
  2. /{user}.aspx
  3. /{user}/{year}.aspx
  4. /{user}/{year}/{month}.aspx
  5. /{user}/{year}/{month}/{day}.aspx
  6. /{user}/{year}/{month}/{day}/{seq}.aspx

本当だったら拡張子の.aspxは付けたくないところですが,付けないとIISからaspnet_isapi.dllにルーティングしないはず(IIS 6.0の場合.IIS7については未調査)なので,とりあえず付けた形で考えます.
1でユーザの一覧を取得します.2だとユーザの書いたブログエントリのタイトル(とリンク)を新しい順に取得します.3から5までも同様に,年・月・日の単位でタイトル(とリンク)を新しい順に取得します.6だと,そのブログエントリのタイトルと本文(=ブログエントリ全体)を取得します.
更新系は,

  • 1のPOSTで,ユーザの作成
  • 2のPOSTで,ブログエントリの作成
  • 2のDELETEで,ユーザの削除
  • 6のPUTで,ブログエントリの更新
  • 6のDELETEで,ブログエントリの削除

といったところかな?とりあえず必要最小限で考えています.使いやすいかどうかは?ですが,とりあえずはhackableにはなっているんじゃないかなと思っています.

HTTPハンドラ

通常ASP.NETでページを作成する場合,例えばDefault.aspxというファイルとそのコードビハインドであるDefault.aspx.csというファイルを実装します.すると当たり前ですが,/Default.aspxというURLでアクセスした際に先ほど実装したコンテンツが表示されます.しかし今回のURL構造の場合,そのようなやり方で無限にファイルを実装することは出来ません.さらに,そもそもレスポンスが(X)HTMLとは限りません.XMLかもしれないしJSONかもしれない.なのでちょっとやり方を変える必要があります.
そこで今回はHTTPハンドラを使うことにしました.HTTPハンドラとは何か,を説明するのは自分には荷が重すぎるので止めることにしますが,知りたい人は「ASP.NETパイプライン」といった単語でググるとよいでしょう.
このあたりでブログシステムのアプリケーション構成を説明します.

  • MyBlogSite
  • MyBlogController
  • MyBlogDao

MyBlogSiteは通常のASP.NET Web Site,下2つはClass Library(DLL)です.DaoをModelと言い切ってしまえば,(まぁ)MVCアーキテクチャといってもよいかもしれません.しかし今回Siteの出番はほとんどありません.とりあえず当面は,web.configに以下の設定を加えておくだけです.

<httpHandlers>
  <add verb="*" path="*.aspx" type="MyBlogController.MyBlogHandler, MyBlogController"/>
</httpHandlers>

これで,拡張子が.aspxのファイルについて,MyBlogControllerアセンブリのMyBlogHandlerクラス(名前空間:MyBlogController)で処理できるようになります.
MyBlogHandler:

public class MyBlogHandler : IHttpHandler
{
  public bool IsReusable{ get { return true; } }

  public void ProcessRequest(HttpContext context)
  {
    IMyBlogCommand command = MyBlogCommandFactory.Make(context);
    command.Execute(context);
  }
}

ProcessRequestメソッドのなかで,今回やりたい事をやります.具体的には以下のようなことです.

  • URLとメソッド(GET/POST/PUT/DELETE)から,要求を取得する
  • 正しい要求であれば,要求に合った正しい表現を返す(メソッドによってはサーバの状態を更新)
  • 不正な要求であれば,適切なステータスコードを持ったレスポンスを返す

この最初の部分をCommandFactoryクラスが,2番目と3番目をCommandクラスが,それぞれ担います.
CommandFactory#Makeは要求を取得し,適切なCommandにディスパッチします.この実装をどうするかが腕の見せ所,なのですが,とりあえず現在はベタに書いてます.(なので,晒しません)
Commandクラスですが,1のGETはこんな感じです.(コメントも入れました)

internal class UserListCommand : IMyBlogCommand
{
  public void Execute(HttpContext context)
  {
    // ユーザテーブルからデータを取得.
    IUserDao userDao = UserDaoFactory.CreateInstance();
    DataTable data = userDao.GetUser();

    // data.WriteXmlでDataTableをXMLに変換.
    string xml = CommandUtility.DataTableToXmlString(data);

    // XML宣言<?xml version="1.0" encoding="utf-16"?>を削除.
    // これをしないと,最終出力時に,UTF-8に変換できないという
    // エラーが出るので暫定対処した.
    // 本当にXML宣言をとってよいのか,未調査.
    string result = CommandUtility.RemoveXmlDeclaration(xml);

    // XML形式で出力.
    // もし(X)HTMLで出力する場合は,ここでは処理せず
    // ViewであるSiteのaspxにURL Rewriting.
    // DaoのデータはHttpContextを使って引き渡す.
    context.Response.ContentType = "text/xml";
    context.Response.StatusCode = 200;
    context.Response.Write(result);
  }
}

ここもなにかしらパターン化されそうではあるのですが,いまはこのままです.
Daoは特に変わったことはしていません.(TableAdapterを使うもよし,DLINQを使うもよし……)
以上がキーとなるコンセプトです.


少し長くなったのでまとめると,HTTPハンドラによるシングルコントローラを作って,Viewは軽くする.その分コントローラが重くなるが,そこはプログラマの腕とセンスが問われるところ.設定ファイルを使うか,それともコードの自動生成をするか,あるいは規約で縛る(要求に制限を設ける)のか.ここから先は,いろいろ試行錯誤しながら考えていくことになりそうです.


さて,このまま先に進んでもよいのですが,その前に考えておきたいことがあります.
認証・許可制御をどうするか?これについて次回考えてみたいと思います.