tech.guitarrapc.cóm

Technical updates

Nancy からLightNode へ移行のススメ

前回はNancyFx と TopShelf を使った SelfHost な APIサーバーについて紹介しました。

tech.guitarrapc.com

しかしプロダクション環境に投入する前に Nancy を辞めて、LightNode に完全移行しました。

そこで今回は、なぜLightNode にしたのかについて書きたいと思います。

目次

なぜLightNode にしたのか

NancyFxを選んだのは、Owin と組み合わせての View と API の提供にちょうど良かったからです。Wikiも豊富で記事もそれなりにありますしStackOverflow に回答も多いです。

github.com

thinkami.hatenablog.com

blog.shibayan.jp

stackoverflow.com

では、なぜ情報も豊富な NancyFx から LightNode にしたのかというと、LightNode の方が圧倒的にお手軽でシンプルだったためです。

github.com

www.slideshare.net

LightNodeがシンプルなのは、機能の取捨選択を行って Owinに任せられることは任せて機能を最小限にとどめていることにあります。

neue cc - LightNode - Owinで構築するMicro RPC/REST Framework

記事にもある通り、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サポートするよ!っといわれていますがいつになることやら。。。。

github.com

HttpListener のころからあるContextが空っぽな件に関しては、OwinRequestScopeContext ミドルウェアを LightNode の前にはさみ込めば ContextをLightNodeで利用できるので問題ありません。

github.com

それ以外は特に制限も感じずに利用できるので大変便利です。

リポジトリ

GitHub に今回の記事で作成したソリューションを置いておきます。

github.com

今回は、前回作成した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直下に置く必要がアリマシテ!*1

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便利!

github.com

Install-Package RazorEngine
エラーの嵐

当然ですが Nancy の参照を消したのでエラーの嵐です。エラーをまずきれいになくしましょうか。

LightNode において、Nancy でやっていた Modules によるルーティングは不要です。消します。

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

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

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

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

Configuration への LightNode 組み込み

まずは、LightNode を Startup.cs で読み込むように Configuration を触ります。

事前処理

using のエラーは、Ctrl + . か R# なら Alt + Enter で処理できます。嫌な方は次の 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フォルダを切って、Testsクラスを作成します。

public class としてから、using に LightNode.Server を追加し LightNodeContract を継承してください。

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/ にアクセスしてみてください。*2

無事に 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 使って自由にいじってください。

LightNode で得られたメリット

いかがでしたでしょうか?Nancy でやっていた Api も View も LightNode で同様に実現できました。

ここまでの手間をかけて 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 は果たしています。

まとめ

書く書く詐欺マンでした!*3

これを気に、Nancy でつらい思いをしている人が、LightNode で楽になることを祈っています。

*1:哀しみでしかない

*2:/Swagger だとだめで、/Swagger/ でないといけません

*3:まだ書いてない記事たくさんだ!