OWINのパイプラインとMiddleware作成ガイド
- 2014-01-06
あけおめました。振り返る~系の記事はこっ恥ずかしいのでいつまでも先頭に出ていると嫌なので、割と流したくてshoganaiので、記事をでっち上げます。実際切実。記事あげてる場合じゃなくても、これはこれでsetsujitsuなので許してあげてほしいのね。
Node.jsでKoaというフレームワークが盛り上がっているらすぃ。で、新しいWebフレームワーク Koa についてを見てて、あー、まんまKatana - Microsoft.Owinで置き換えられるなぁと思ったので、書いてみました。
public class Startup
{
public void Configuration(IAppBuilder app)
{
// KoaとOwinを比較して
// http://blog.kazupon.jp/post/71041135220/koa
// 3. Response Middleware
app.Use(async (context, next) =>
{
Console.WriteLine(">> one");
await next();
Console.WriteLine("<< one");
});
app.Use(async (context, next) =>
{
Console.WriteLine(">> two");
await context.Response.WriteAsync("two");
await next();
Console.WriteLine("<< two");
});
app.Use(async (context, next) =>
{
Console.WriteLine(">> three");
await next();
Console.WriteLine("<< three");
});
}
}
比較すると、そっくりそのまま。Koaはフレームワークというか小さなツールキット、Connect/Koa = Katana, Express = ASP.NET MVC/Web API、みたいな図式で捉えればいいのでしょうね。
OWINの成り立ちについては、OWIN - Open Web Interface for .NET を使うで解説されていますが、Ruby- Rack/Python - WSGI/Perl- PSGIと同じようなものと捉えられます。OWINとKatanaに関しては、PerlのPSGIとPlackの関係性を見れば、そのまま当てはめることが可能です。
よって、OWINの基本的なことは、OWIN関連を漁るよりも、Plack Handbookを読んだほうがピッと理解できそうです。GitHubのリポジトリには日本語の生Markdown原稿もあるので、目を通しておくと、理解がとっても進みます。
Pluggable Pipe Dream
OWINがASP.NETにもたらしたものは2つ。一つはバックエンドの自由、IISでもSelfHostでもー、という側面。もう一つはプラガブルなMiddleware。そしてこれは、パイプラインになっているのですね、こちらのほうが開発者にとって注目に値する、影響力の大きなものです。
最初の例で書いた app.Use(async (context, next) => はMicrosoft.Owinによるもので、ラムダ式でその場でMiddlewareを定義していることに等しい(AnonymousMiddleware!)わけですが、まずはこっちから書いてったほうが、わかりやすいかな、と。(ちなみにRunはnextのないバージョン、つまりMiddlewareの終点)
app.Use(async (context, next) =>
{
try
{
// 実行前の処理が書ける
await next(); // 次のミドルウェアの実行(これを呼ばないことでパイプラインのキャンセルも可能)
// 正常実行後の処理が書ける
}
catch
{
// 例外時の処理が書ける
}
finally
{
// 後処理が書ける
}
});
こういったパイプラインは.NETには偏在しています。HttpClientのDelegatingHandler - HttpClient詳解、或いは非同期の落とし穴についてや、LINQ to Objects - An Internal of LINQ to Objectsの中身と変わらない話です、特にDelegatingHandlerは近いイメージ持ってもらうと良いかな、と。図にすればこんな感じ。
next()を呼び出すことで、円の中央、次のパイプラインに進む。きっと、一番最後、中心のMiddlewareはフレームワークとしての役割を担うでしょう(ResponseStreamに書いたりなど処理をかなり進めてしまうので、フレームワークが後続のMiddleware読んでも無意味というか逆に死んだりするので、フレームワーク部分では意図的にnext呼ばない方がいい←だから実際app.RunもNext呼ばないしね)、とはいえ構造上ではFrameworkとMiddlewareに別に区別はないです。処理が終わったら、今度は円の内側から外側に向かって処理が進んでいきます。Nextを呼ばなければ、途中で終了。1-2-3-4-5-4-3-2-1を、3で止めれば、1-2-3-2-1になる、といった感じです。これはASP.NET MVCのfilterでResultに小細工したり、Exceptionを投げたりして中断するようなものです。
HttpModuleだってHTTPパイプラインぢゃーん、というツッコミもあっていいですけれど、それよりもずっと単純明快な仕組みだというのがとても良いところ。こういった薄さであったり単純さであったり、をひっくるめたLightweightさって、とっても大事です。
Mapping
LightNode - Owinで構築するMicro RPC/REST FrameworkではURLは決め打ち!と言いましたが、むしろそもそも、URLのルーティングはLightNodeが面倒見るものではなくて、他のMiddlewareが面倒見るものなのです。例えば、APIのバージョン管理でv1とv2とで分けたい、というケースがあったとしましょう。その場合、MapWhen(Katanaに定義されてます)を使うと、条件指定で利用するMiddlewareのスタックを弄ることができます。
// Conditional Use
app.MapWhen(x => x.Request.Path.Value.StartsWith("/v1/"), ap =>
{
// Trim Version Path
ap.Use((context, next) =>
{
context.Request.Path = new Microsoft.Owin.PathString(
Regex.Replace(context.Request.Path.Value, @"^/v[1-9]/", "/"));
return next();
});
ap.UseLightNode(new LightNodeOptions(AcceptVerbs.Post, new JsonNetContentFormatter()),
typeof(v1Contract).Assembly);
});
app.MapWhen(x => x.Request.Path.Value.StartsWith("/v2/"), ap =>
{
// 手抜きなのでコピペ:)
ap.Use((context, next) =>
{
context.Request.Path = new Microsoft.Owin.PathString(
Regex.Replace(context.Request.Path.Value, @"^/v[1-9]/", "/"));
return next();
});
ap.UseLightNode(new LightNodeOptions(AcceptVerbs.Post, new JsonNetContentFormatter()),
typeof(v2Contract).Assembly);
});
LightNodeはサービスの記述された読み込むアセンブリを指定できるので、v1用アセンブリとv2用アセンブリを分けて貰って、Request.Pathを書き換えてもらえれば(/v1/部分の除去)動きます。これは単純な例ですが、複雑なルーティングだって頑張れば出来るでしょう。きっと。
OWINにはSuperscribeというグラフベースルーティング(何だそりゃ)とかもありますし、そういうのと組み合わせれば、実際LightNodeでうまく使えるかどうかは知りませんが、まぁ、そういうことです。やりたければ外側で好きにやればいいのです。プラガブル!
Headerが送信されるタイミング
話は突然変わって、Middleware実装上のお話。表題のことなのですけれど、原則的には「最初にWriteされた時」です。原則的には、ね。最初にFlushされた時かもしれないし、そもそもされないかもしれないこともあるかもですが、とはいえ原則的には最初にWriteされた時です。どーいうことか、というと
app.Run(async (context) =>
{
try
{
// Writeしてから
await context.Response.WriteAsync("hogehoge");
context.Response.Body.Flush();
// StatusCodeやHeaderを書き換えると
context.Response.StatusCode = 404;
}
catch (Exception ex)
{
// 例外出る
Debug.WriteLine(ex.ToString());
}
});
これをMicrosoft.Owin.Host.SystemWebでホストすると、「HTTP ヘッダーの送信後は、サーバーで状態を設定できません。」というお馴染みのような例外を喰らいます。ちなみにHttpListenerによるSelfHostでは無反応という、裏側のホストするものによって挙動は若干違うのだけは注意。とはいえ、どちらも共通して、ヘッダーが送信された後には幾らStatusCodeやHeaderを書き換えても無意味です。上の例だと、404にならないで絶対200になっちゃうとか、そういう。
当たり前といえば当たり前なのですが、生OWIN、生Katanaだけで色々構築すると、Middlewareの順序によっては、そーなってしまうことも起きてしまいがちかもしれません。
なお、Katanaのソースコード読む時はHttpListenerのほうを中心に追ったほうが分かりやすいですね。System.Webのほうは、つなぎ込みがややこしかったり、すぐにブラックボックスに行っちゃったりで読みにくいので。若干の挙動の差異はあるとはいえ、概ね流れや処理は同じですから、まずは読みやすいほう見たほうが楽でしょう。
バッファリングミドルウェア
さて、そんな、Writeが前後して厄介というのを防ぐためのMiddlewareを作ってみましょう。解決策は単純で、上流のパイプラインでバッファリングしてやればいいわけです。
app.Use(async (context, next) =>
{
var originalStream = context.Response.Body;
using (var bufferStream = new MemoryStream())
{
context.Response.Body = bufferStream; // 差し替えて
await next(); // 実行させて
context.Response.Body = originalStream; // 戻す
if (context.Response.StatusCode != 204) // NoContents
{
context.Response.ContentLength = bufferStream.Length;
bufferStream.Position = 0;
await bufferStream.CopyToAsync(originalStream); // で、コピー
}
}
});
単純簡単ですね!そう、Middlewareとか別にあんまり構える必要はなくて、Global.asax.csに書いていたのと同じようなノリでちょろちょろっと書いてやればいいわけです。そして、それが膨らみ始めたり、汎用的に切り離せそうだったら、独立したMiddlewareのクラスを立ててやれば再利用可能。これはIHttpModuleを作るのと同じ話ですけれど、Middlewareは、それよりもずっとカジュアルに作れます。
さて、上のコード、しかしこれだとMemoryStreamが中で使ってる奴にCloseされちゃったりするとCopyToAsyncでコケてしまいます。いや、誰がCloseするんだ?という話はありますが、でも、例えばStreamWriterを使って、usingして囲んでStreamに書いたりすると、内包するStreamまでCloseされちゃうんですねぇ。
usingしないように注意する、というのも、パイプラインに続くMiddleware全てで保証なんて出来ないので、ここもまた上流で防いでやるのがいいでしょう。LightNodeではUnclosableStream.csというものでラップしています。どういうものかというと
internal class UnclosableStream : Stream
{
readonly Stream baseStream;
public UnclosableStream(Stream baseStream)
{
if (baseStream == null) throw new ArgumentNullException("baseStream");
this.baseStream = baseStream;
}
// 以下ひたすらStreamを移譲
// そしてCloseとDisposeは空白
public override void Close()
{
}
protected override void Dispose(bool disposing)
{
}
}
という単純なもの。これを、ついでに独立したMiddlewareにしてみますか、すると、
public class BufferingMiddleware : Microsoft.Owin.OwinMiddleware
{
public BufferingMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(Microsoft.Owin.IOwinContext context)
{
var originalStream = context.Response.Body;
using (var bufferStream = new MemoryStream())
{
context.Response.Body = new UnclosableStream(bufferStream); // Unclosableにラップする
await this.Next.Invoke(context); // Microsoft.Owin.OwinMiddleware使うとthis.Nextが次のMiddleware
context.Response.Body = originalStream;
if (context.Response.StatusCode != 204)
{
context.Response.ContentLength = bufferStream.Length;
bufferStream.Position = 0;
await bufferStream.CopyToAsync(originalStream);
}
}
}
}
フレームワークレベルのものを作る時は、このレベルまで気を使ってあげたほうが間違いなくいいかと思います。
Owin vs Microsoft.Owin
Middleware作るのにMicrosoft.Owin.OwinMiddlewareを実装する必要はありません、InvokeとTaskと、などなどといったシグネチャさえあってればOKです。同様にIOwinContextはKatanaで定義してあるものであり、Owin自体はIDictionary<string, object>が本体です。
Katana(Microsoft.Owin)は便利メソッドの集合体です。Dictionaryから文字列Keyで引っ張ってResponseStream取り出すより、context.Response.WriteAsyncと書けたほうが当然楽でしょふ。他にも、Cookieだったりヘッダだったり、Middlewareの定義用ベースクラスだったり、などの基本的な、基本的な面倒事を全てやってくれる薄いツールキットがKatanaです。冒頭の、Node.jsのKoaみたいなものであり、PerlのPlackに相当するようなもの、と捉えればいいんじゃないでしょーか。
LightNodeはMicrosoft.Owinを参照していません。これは、依存性を最小限に抑えたかったからです。その分だけ、面倒事もあるので、楽したかったり社内用Middlewareを少し作るぐらいだったら、Katana使っちゃっていいと思いますですね。リファレンス実装、でありますが、どうせ事実上の標準として収まるでしょうし。フレームワークレベルでがっつし作ってみたいという時に、依存するかしないか、どちらを選ぶかは、まぁお好みで。依存したって全然構わないし、依存しないようにするのもそれはそれでアリだと思いますし。
HTMLを書き換えるMiddlewareを作る
というわけで、応用編行くよー。mayuki先生の作られているCarteletというHTMLパーサー/フィルターライブラリがあるのですが(某謎社で使われているらしいですよ)、それをOwinに適用してみましょう。Carteletのできることは
HTMLのそれなりに高速でそれなりなパース 出力時にCSSセレクターで要素に対してマッチして属性や出力フィルター処理 フィルターしない部分は極力非破壊 ASP.NET MVCのViewEngine対応 CSSのstyle属性への展開 (Cartelet.StylesheetExpander)
だそうです。
例として、class="center"という属性を、style="text-align:center"に展開するというショッパイ決め打ちな例を作ってみます。こんなMiddlewareを作ります。
// Cartelet Filter Middleware
app.Use(async (context, next) =>
{
var originalStream = context.Response.Body;
using (var bufferStream = new MemoryStream())
{
context.Response.Body = bufferStream; // 差し替えて
await next(); // 実行させて
context.Response.Body = originalStream; // 戻す
if (context.Response.StatusCode != 204) // NoContents
{
// Carteletによるフィルタリングもげもげ
var content = Encoding.UTF8.GetString(bufferStream.ToArray());
var htmlFilter = new HtmlFilter();
htmlFilter.AddHandler(".center", (ctx, nodeInfo) =>
{
nodeInfo.Attributes.Remove("class");
nodeInfo.Attributes["style"] = "text-align:center";
return true;
});
var node = HtmlParser.Parse(content);
var sw = new StreamWriter(context.Response.Body); // usingしない、stream閉じないために(leaveOpenオプションもあるのでそちらのほうが望ましいけど横着した)
var cartelet = new CarteletContext(content, sw);
htmlFilter.Execute(cartelet, node);
await sw.FlushAsync(); // usingしない時はFlushも忘れないように。。。
}
}
});
Carteletの受け取るのがStringなので、全パイプラインが完了するまではバッファリングします。で、それで手に入れたStringをCarteletに流し込んで、本来のBodyに流し込む。(Content-Lengthの設定を省いてるので直に流し込んでますが、設定が必要なら再再バッファリングががが、まぁ、どうせ更に上流でgzipとかするだろうから、ここでContent-Length入れる必要はあんまにゃいかな!)
実際に結果を見てみると、
// これを実行すると
app.Run(async context =>
{
context.Response.StatusCode = 200;
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync(@"
<html><body>
<div class=""center"">
ほげほげ!
</div>
</body></html>");
});
<html><body>
<div style="text-align:center">
ほげほげ!
</div>
</body></html>
というHTMLが出力されます。へーへーへー。色々応用効きそうですね!
OWINはパイプラインの夢を見るか?
色々出来る、しかも色々簡単!素晴らしい素晴らしい!プラガブル!はたして本当に?実際、フレームワーク書いたりミドルウェア書いたりしてると、ふつふつふつと疑問が湧いてきます。そういう時は先行事例を見ればいい、というわけでPythonのWSGIでは以下の様な話が。
すべてがプラガブルで、モノリシックなアプリケーションフレームワークを持つ理由がもはや一つもないような未来を思い描いていました。すべてライブラリ、ミドルウェア、デコレータでまかなえるからです。 悲しいことに、そんな理想的な未来はやってきませんでした。
OWINでも、一個のでっかいフレームワークを持つ必要なんてない!と言える時が来るか、というと、さすがにそれはないでしょうねえー。また、たとえ分離可能なコンポーネントであっても、フレームワークの提供するシステム(フィルターやプラグイン)から離れられるかというと、必ずしもそうではないのかな、って。
Middlewareのパイプラインは、ASP.NET MVC/Web APIとかのフィルターのパイプラインとも同じようなものです。だったらフィルターで作るよりMiddlewareで作ったほうが、フレームワークという制限から離れられて良さそう。でも、Middlewareの欠点は、後続のパイプラインのコンテキストを知らないことです。認証を入れるにしても、[AllowAnonymous]属性が適用されているかなんてしらないから、全部適用するかしないか、ぐらいにしか出来ない。filterContext.ActionDescriptorのようなもの、というのは、フレームワークの内側のシステムしか持ち得ないのです。でも、そうしてフィルターとして実装すれば、フレームワークに深く依存することになる。
そんな悩ましさを抱えつつも、それは、あんま無理せずに、コンテキスト不要なら最大限独立性の高いOwin Middlewareとして。そうでないならアプリケーションのプラグイン(フィルター)として。でいいかな、って思ってます。今のところ。何れにせよIHttpModuleなんかよりは遥かに作りやすいし、その手の話だって今に始まったことじゃあないのよね?HttpModuleだってHTTPパイプラインぢゃーん、って。はい。
2014/1/18(土)に開催されるめとべや東京#3(開催場所は謎社です)では、LTで5分でサービスAPIをOwin/LightNodeを使って作って実際にAzure Web Sitesにホストするまで、デモしようと思ってますので、OWIN知りたい、どう動かすのか見てみたい、って人もどうぞ。めととは。