前回はNancyFxとTopShelfを使ったSelfHostなAPIサーバーについて紹介しました。
しかしプロダクション投入する前にNancyを辞めて、LightNodeに完全移行しました。そこで今回は、なぜLightNodeにしたのかについて書きます。
なぜLightNode にしたのか
NancyFxを選んだのは、Owinと組み合わせてのViewとAPIの提供にちょうど良かったからです。記事もそれなりにありますしStackOverflowに回答も多いです。
https://github.com/NancyFx/Nancy/wiki/Hosting-nancy-with-owin
https://thinkami.hatenablog.com/entry/2014/09/25/064020
では、なぜ情報も豊富なNancyFxからLightNodeにしたのかというと、LightNodeの方が圧倒的にお手軽でシンプルだったためです。
LightNodeがシンプルなのは、機能の取捨選択してOwinに任せられることは任せて機能を最小限にとどめていることにあります。
記事にもある通り、LightNodeにはある意味で制限があります。しかしシンプルというのはとても大事で、Nancyの時に苦しんだことの多くがLightNodeを使うことで解消されることが移行前にはっきりしていました。
| Nancyにあるツラさ | LightNode で得られること |
|---|---|
| Moduleで常にルーティングを意識する必要がある | {ClassName}/{MethodName} の単純なルールで縛られておりルーティングを考える必要がない |
| リクエスト処理を都度書く必要がある | POST は フォームパラメータ、GET は クエリ文字列のルールで統一されている |
| APIを実行するクライントコードを別途書く必要がある | T4 でサーバーサイドコードからの自動生成が可能 |
| API のテストをしようにも Swagger の導入が別途必要 | SwaggerでのAPIテストも容易 (パッケージがNuGetで配布されていて導入が超絶楽) |
| 実行の計測がめんどください | Glimps での計測も簡単 (同上) |
移行当初はSelfHostにまつわるバグやView周りのめんどくささもありましたが、すでに修正されており利用で困ることはないでしょう。
SelfHost で受ける制限
先に、SelfHostの場合の制限を挙げておきましょう。
ContextとGlimpseがあります。
GlimpseはIIS依存のため、現在利用できませんがv2でOwinサポートするよ! っといわれていますがいつになることやら。。。。
HttpListenerのころからあるContextが空っぽな件に関しては、OwinRequestScopeContextミドルウェアをLightNodeの前にはさみ込めばContextをLightNodeで利用できるので問題ありません。
それ以外は特に制限も感じずに利用できるので大変便利です。
リポジトリ
GitHubに今回の記事で作成したソリューションを置いておきます。
今回は、前回作成したNancy + TopShelfなSelfHostをLightNodeに移行します。
早速みていきましょう。今回はVS2015 RCで作っていきます。
LightNode の導入と移行
前回のNancySelfHostのプロジェクトをLightNodeSelfHostにいじります。



Nancyで追加された、いらないビルドイベントも消します。

NuGet
すでにOwin系は入っていますがNancyなどは邪魔なのでけしましょう。
もとあったのが、

こんな感じですかね。

続いて、さくさくっとNugetでパッケージを入れていきます。Package Manager Consoleで次のコマンドを入れていきましょう。
まずOwin系です。Microsoft.Owin.StaticFilesはMicrosoft.Owin.FileSystemと共にローカルにあるjsやcssを参照させるために使います。仕方ない...。
Microsoft.AspNet.WebPagesは、RazorEngineを今回ViewEngineに使うのですが、インテリセンスのためにbin/直下へ置く必要がありました。
Install-Package Microsoft.Owin.StaticFiles Install-Package Microsoft.AspNet.WebPages
今回は、NancySelfHostからの移行なのでこの程度ですね。新規で作るならMicrosoft.Owin.Host.HttpListenerとかもいりますよ!
LightNode関連です。今回は、JsonFomatterとSwagger統合まで入れます。
Install-Package LightNode.Server Install-Package LightNode.Formatter.JsonNet Install-Package LightNode.Swagger
ViewEngineです。今回はRazorEngineを使うことにします。Razor便利!
Install-Package RazorEngine
エラーの嵐
当然ですがNancyの参照を消したのでエラーの嵐です。エラーをまずきれいになくしましょうか。
LightNodeにおいて、NancyでやっていたModulesによるルーティングは不要です。消します。

NancyBootstrapperやNancyPathProviderも不要ですね。消します。

最後に、Nancyを使わないのでStartup.csからUseNancy();を消します。

これでエラーは消えましたね。

最後に、UseWebApiもいらなくなったので消します。

Configuration への LightNode 組み込み
まずは、LightNodeをStartup.csで読み込むようにConfigurationを触ります。
事前処理
usingのエラーは、Ctrl + .かR# ならCtrl + .で処理できます。嫌な方は次のusingで。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Http; using System.IO; using System.Web.Http.ValueProviders; using Microsoft.Owin; using Microsoft.Owin.Hosting; using Owin; using LightNode.Server; using LightNode.Formatter; using LightNode.Swagger;
OwinRequestScopeContextでContext取得できるようにUseRequestScopeContext()します。
public void Configuration(IAppBuilder app) { // 各種有効化 app.UseRequestScopeContext(); // Context の取得 }
続いて、ローカルファイルのcssなどをOwinから呼びだせるようにUseFileServer()します。
public void Configuration(IAppBuilder app) { // 各種有効化 app.UseRequestScopeContext(); // Context の取得 app.UseFileServer(); // FileServer 用の読み込みだよ (Owin からFileアクセス許可を許容するために必要) }
Api処理
続いてApiの基底ルートを定めます。今回は/apiで入ってきたものをapi処理としましょう。
この辺はLightNodeのドキュメントにもあります。
LightNodeOptions(AcceptVerbs.Get | AcceptVerbs.Postで、GetとPostを受け入れています。
new LightNode.Formatter.JsonNetContentFormatter(), new LightNode.Formatter.JsonNetContentFormatter()でJsonNetをフォーマッターとして使用します。
あとはエラーのハンドリングですね。まぁ書いてある通りで!
// api 処理 app.Map("/api", builder => { var option = new LightNodeOptions(AcceptVerbs.Get | AcceptVerbs.Post, new LightNode.Formatter.JsonNetContentFormatter(), new LightNode.Formatter.JsonNetContentFormatter()) { ParameterEnumAllowsFieldNameParse = true, // If you want to use enums human readable display on Swagger, set to true ErrorHandlingPolicy = ErrorHandlingPolicy.ReturnInternalServerErrorIncludeErrorDetails, OperationMissingHandlingPolicy = OperationMissingHandlingPolicy.ReturnErrorStatusCodeIncludeErrorDetails, }; // LightNode つかうよ builder.UseLightNode(option); });
Page処理
APIだけじゃなくて、ブラウザでアクセスがあったらページを表示したくないですか? そのためのルートとして/pages/を切ってみましょう。
といっても、処理はLightNodeで行うので全然かわらないのですが!
// page 処理 app.Map("/pages", builder => { // LightNode つかうにゃ builder.UseLightNode(new LightNodeOptions(AcceptVerbs.Get, new JsonNetContentFormatter())); });
Swagger処理
Swaggerを使ってAPIのテストが簡単にできるのは間違いなくLightNodeの良さです。やり方も簡単です。まずはSwaggerを使うためのルートと、メソッドのxmlを指定します。
// Swagger くみこむにゃん app.Map("/swagger", builder => { var xmlName = "LightNodeSelfHost.xml"; var xmlPath = AppDomain.CurrentDomain.BaseDirectory + @"bin\" + xmlName; builder.UseLightNodeSwagger(new SwaggerOptions("LightNodeSelfHost", "/api") { XmlDocumentPath = xmlPath, IsEmitEnumAsString = true // Enumを文字列で並べたいならtrueに }); });
プロジェクトのプロパティで指定したパスにxmlを吐き出しましょう。出力 > XMLドキュメントファイルがソウデスネ。

一度ビルドして、生成されたXMLを常にコピーされるようにすれば準備okです。

Api の作成
LightNodeはApiの作成が超絶簡単です。適当に/api/Tests/ApiTestでHello Worldが返ってくるようにアクセスできるようにしてみましょう。
まずは、わかりやすくするためにApiフォルダを切って、Apiフォルダクラスを作成します。

public classとしてから、usingにLightNode.Serverを追加しLightNode.Serverを継承してください。
using LightNode.Server; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LightNodeSelfHost.Api { public class Tests : LightNodeContract { } }
あとは、Hello World!!を返すpubicメソッドを作成してみます。
public class Tests : LightNodeContract { /// <summary> /// Hellow World を返します。 /// </summary> /// <returns></returns> public string ApiTest() { return "Hello World!"; } }
Api を Swaggerでテストする
それでは、https://localhost:8080/Swagger/にアクセスしてみてください。*1
無事にSwaggerの画面が出たら成功です。

今回メソッドに属性を付けませんでしたが、[Post]属性を指定するなど、アクセス制御も容易です。


ではSwaggerで実行してみてください。意図通り、Hello World!!が返されればばっちりですね。

PowerShell での Apiアクセス
Swagger素晴らしいです。ここまで確認できていれば、PowerShellでも確認も容易ですね。
wget https://localhost:8080/api/Tests/ApiTest -Method Post
もちろん[Post]属性をつけたので、 Getでは拒否されます。

Postで意図通りに返ってきましたね。

現在、ContentにBOM付でメッセージが返ってくるので残念な■が見えていますが、次回のバージョンで修正される予定のようなので待ちましょう。
LightNodeでのTips
いくつかのTipsがあります。順番にみていきましょう。
HTML でレンダリングされた Viewを返したい。
当初ありませんでしたが、現在はRazorEngineを使って .cshtmlで書いて、ブラウザアクセスの時にHTMLを返すことができます。
Startup.csのConfigurationで定義した通り、/pagesのアクセスにはViewを返してみましょう。
まずは、RazorEngineでレンダリングして、Htmlで返すためのLightNodeContractとしてRazorContractBase.csを作ります。

コードは次の通りで、RazorEngineのレンダリング結果を取っています。
ポイントは、Viewのcshtmlを置く場所です。var viewPath = System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Views", name);で、VSのルート直下にある "Views"の中にあるcshtmlを対象にしています。
もし違うフォルダにcshtmlを置くなら、ここだけ変えましょう。
using System; using System.IO; using LightNode.Formatter; using LightNode.Server; using RazorEngine.Configuration; using RazorEngine.Templating; namespace LightNodeSelfHost { public abstract class RazorContractBase : LightNode.Server.LightNodeContract { static readonly IRazorEngineService razor = CreateRazorEngineService(); static IRazorEngineService CreateRazorEngineService() { var config = new TemplateServiceConfiguration(); config.DisableTempFileLocking = true; config.CachingProvider = new DefaultCachingProvider(_ => { }); config.TemplateManager = new DelegateTemplateManager(name => { // import from "Views" directory var viewPath = System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Views", name); return System.IO.File.ReadAllText(viewPath); }); return RazorEngineService.Create(config); } protected string View(string viewName) { return View(viewName, new object()); } protected string View(string viewName, object model) { var type = model.GetType(); if (razor.IsTemplateCached(viewName, type)) { return razor.Run(viewName, type, model); } else { return razor.RunCompile(viewName, type, model); } } } }
合わせて、中にLightNodeが上記をHtmlとして属性で指定できるようにしましょう。
public class Html : LightNode.Server.OperationOptionAttribute { public Html(AcceptVerbs acceptVerbs = AcceptVerbs.Get | AcceptVerbs.Post) : base(acceptVerbs, typeof(HtmlContentFormatterFactory)) { } }
残すは、先ほどのApiフォルダにViewを返すApiを用意します。

先ほどと違うのは、継承するのがLightNodeContractBaseではなく、先ほど作ったRazorContractBaseであることです。
[Html]属性をつけましょう。
[IgnoreClientGenerate]属性は、LightNode.Clientで生成対象外にしてくれるので便利!
RazorContractBaseで作った、Viewメソッドに、cshtmlのファイル名を指定するだけというお手軽さです。
using LightNode.Server; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LightNodeSelfHost.Pages { /// <summary> /// View を返します。 /// </summary> class Views : RazorContractBase { [IgnoreClientGenerate] [Html] public string Index() { return this.View("Index.cshtml"); } } }
では、ルートのViewsフォルダにNancyのころのcshtmlがあるのでちょちょっと触ります。
具体的には、以下のRazorセクションを足すだけです。
@using System.Linq; @using RazorEngine.Templating; @{ Layout = "Shared/_Layout.cshtml"; }

cshtmlの警告は、packagesにNugetでいれた、Microsoft.AspNet.WebPagesにあるdllをぶちこめば消えます。
View/Apiも用意したら、デバッグ実行してみてください。 https://localhost:8080/pages/Views/Indexにアクセスして、うまくViewが表示されればokですね!

実装したApi の一覧を返したい
Apiの一覧はLightNodeが一覧を持っており容易に返却可能です。LightNodeServerMiddleware.GetRegisteredHandlersInfo();がそれにあたります。
例えば、こんな風に書けばApiの一覧を返すApiが書けます。
private static readonly string _root = "/api"; /// <summary> /// 実装されているAPIの一覧を返します。 /// </summary> /// <remarks>APIのUriを返します。</remarks> /// <returns></returns> [Post] public string[] ListApi() { var apis = LightNodeServerMiddleware.GetRegisteredHandlersInfo(); var key = apis.Select(x => x.Key).First(); return apis[key].SelectMany(x => x.RegisteredHandlers).Select(x => x.Key).ToArray(); }
Swaggerでみてみましょう。

便利! ですね。Swaggerを見ずともApiの一覧が取れるのは使いようがあります。もちろんUri以外にも様々な情報が取れますよ。LINQ使って自由にいじってください。

まとめ
NancyでやっていたApi/ViewはLightNodeで同様に実現できました。移行したかいがあります。
Nancy では Apiの追加のたびにルートを切ったりリクエスト処理が必要
LightNodeでは、Apiのルートが{ClassName}/{MethodName}で固定です。わかりやすく、自動的に行ってくれるためApiの追加による負担を解消してくれます。
Modulesがめんどくさいと思った人は私だけじゃないはず。。。!
Swagger によるApi実行環境の標準提供
Swaggerがないともはややってられませんね! イヤホンと。Nancyに組み込むのも面倒なもので、標準プラグインとして提供されているのは相当素晴らしいです。
Glimpse 統合
SelfHostでは使えませんが、IIS + Owin + LightNodeなら、当然Glimpseが使えます。実行環境の測定は第一歩なので、できるの素晴らしい!
Production Ready
すでに謎社のインフラのデプロイ基盤はLightNodeを使った環境に移行しています。そう、Production Readyなのです。 デプロイを極限まで高速化したい。そのための重要なパーツをLightNodeは果たしています。
これを気に、Nancyでつらい思いをしている人が、LightNodeで楽になることを祈っています。
*1:/Swaggerだとだめで、/Swagger/ でないといけません