ASP.NETでRESTful Webサービスを実装してみる
休暇が終わり時間が取りづらくなりましたが,ちびちびと実装を始めています.
題材はブログシステムです.
URL設計
URL設計ですが,とりあえずGET系でこんな感じで考えています。
- /home.aspx
- /{user}.aspx
- /{user}/{year}.aspx
- /{user}/{year}/{month}.aspx
- /{user}/{year}/{month}/{day}.aspx
- /{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は軽くする.その分コントローラが重くなるが,そこはプログラマの腕とセンスが問われるところ.設定ファイルを使うか,それともコードの自動生成をするか,あるいは規約で縛る(要求に制限を設ける)のか.ここから先は,いろいろ試行錯誤しながら考えていくことになりそうです.
さて,このまま先に進んでもよいのですが,その前に考えておきたいことがあります.
認証・許可制御をどうするか?これについて次回考えてみたいと思います.