前回は、TopShelf アプリケーションのデプロイをDSCで自動化する例を紹介しました。
今回は、LightNode + TopShelf を使うことでIISに依存しないAPIサーバーを作ってみましょう。と、書いていたのですが、その前にNancy だとどうなるのか書いていたら長くなったので LightNodeは次回です。ゴメンニャサイ。
目次
- 目次
- APIサーバーを作りたい?
- リポジトリ
- NancyFx/Nancy によるAPIサーバーを作ってみよう
- TopShlef の起動
- TopShelf によるサービスインストールとアンインストール
- OWIN + NancyFx/Nancyという選択肢
APIサーバーを作りたい?
「APIサーバーをさくっと作りたい。簡易的な View 付きで。けど、IIS上で ASP.NET MVC や Web API を書くほどでもないしもっとさくっとAPIに集中して楽をしたい。」
せっかくなので HttpListener や TCPListener、Socket を使ってもにょもにょ書き比べてたりしましたがツラぽ。そして、今はもう自分で実装する必要はもうアリマセン。OWIN があります。
OWIN を使うことで、HttpListener を手触りする苦痛から解放されてAPIの実装に集中できます。ホストもIISから依存脱却してSelfHostも視野に入れることができます。特にIISからの分離はかなり大きく、TopShelf によるサービス稼動も視野に入れることができます。サーバーとアプリの分離うれしいです。*1
OWIN と Framework
OWIN のページには、Framework がいくつか紹介されています。
Server/Hosts として、Microsoft 実装の Katana。
https://katanaproject.codeplex.com/katanaproject.codeplex.com
Framework として、NancyFx/Nancyや SignalR。どれも一度は目にしたことある有名どころです。
この中で、「APIサーバー + View」をやってくれる望んだ機能を持っていたのが NancyFx/Nancyです。NancyFx/Nancyを使えば、API以外にも Razor 構文で Viewを返すことができます。使い慣れた cshtml を使ってさくっと作れるのは魅力的でしょう。ちなみに他の ViewEngine もあり MarkDownなども....ただ MarkDown はかなり気持ち悪い挙動をしたのでもう使うことはナイデショウ。
今回、LightNode で APIサーバーが超簡単に作れる紹介をしようと思ったのは、私が NancyFx/Nancyから LightNode に乗り換えてとても楽な思いをしたからでした。そこで、まずは Nancy による簡単なAPIサーバーを作成してみましょう。*2
リポジトリ
GitHub に今回の記事で作成したソリューションを置いておきます。
では、早速みてみましょう。ここでは VS2015 RC で作成しています。
NancyFx/Nancy によるAPIサーバーを作ってみよう
VS2015 でコンソールアプリを作成しましょう。
NuGet
続いて、さくさくっとNugetでパッケージを入れていきます。
Package Manager Console で次のコマンドを入れていきましょう。
Owin 関連です。
Install-Package Owin Install-Package Microsoft.AspNet.WebApi.Owin Install-Package Microsoft.AspNet.WebApi.Client Install-Package Microsoft.AspNet.WebApi.OwinSelfHost Install-Package Microsoft.Owin Install-Package Microsoft.Owin.Host.HttpListener Install-Package Microsoft.Owin.Hosting
Nancy関連です。
Install-Package Nancy Install-Package Nancy.Owin
今回 ViewEngine として Razor を利用します。
Install-Package Nancy.Viewengines.Razor
View 用に bootstrap を入れましょう
Install-Package bootstrap
TopShlef の起動
まずは、Program.cs で TopShelfの起動を書きます。
細かなことは本家のドキュメントで
Welcome to Topshelf’s documentation! — Topshelf 3.0 documentation
OWIN + NancyFx/Nancyを 通常のセルフホストそして起動するだけなら、普通にOWIN Startupクラスを呼びだすだけですが。。。。
using Microsoft.Owin.Hosting; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NancySelfHost { class Program { static void Main(string[] args) { var uri = args.Length == 0 ? "https://localhost:8080/" : args[0]; using (WebApp.Start<Startup>(uri)) { Console.WriteLine("Started"); Console.WriteLine("Press any key to continue."); Console.ReadKey(); Console.WriteLine("Stopping"); } } } }
今回は TopShelf として起動するので、Startupクラスに定義した .Start()メソッド と .Stop() メソッドを呼びだすようにします。
.Start() メソッドは、サービスの開始時に呼び出されるメソッドです。
.Stop() メソッドは、サービスの停止時に呼び出されるメソッドです。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Topshelf; namespace NancySelfHost { class Program { private static readonly string _serviceName = "NancySelfHost"; private static readonly string _displayName = "NancySelfHost"; private static readonly string _description = "NancySelfHost Test Application."; static void Main(string[] args) { HostFactory.Run(x => { // Automate recovery x.EnableServiceRecovery(recover => { recover.RestartService(0); }); // Reference to Logic Class x.Service<Startup>(s => { s.ConstructUsing(name => new Startup(_serviceName)); s.WhenStarted(sc => sc.Start()); s.WhenStopped(sc => sc.Stop()); }); // Service Start mode x.StartAutomaticallyDelayed(); // Service RunAs x.RunAsLocalSystem(); // Service information x.SetServiceName(_serviceName); x.SetDisplayName(_displayName); x.SetDescription(_description); }); } } }
OWIN + NancyFx/Nancy の記述
次にOWIN の Startup クラスを作成します。これがOWIN のエントリーポイントとなります。
NameSpaceの上に指定した[assembly: OwinStartup(typeof(NancySelfHost.Startup))]
でOWINのスタートアップを指定しているのがポイントの1つです。これをすることで、OWINのエントリーポイントクラスを明示します。
先ほど Program.cs で呼びだした .Start() メソッドはこのStartupクラスに定義しておき、WebApp.Start<Startup>(uri);
で OWIN呼び出して、public void Configuration(IAppBuilder application)
にてOWINを読み込み開始しています。
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; [assembly: OwinStartup(typeof(NancySelfHost.Startup))] namespace NancySelfHost { public class Startup { public string ServiceName { get; set; } private static IDisposable _application; public Startup(string serviceName) { this.ServiceName = serviceName; } /// <summary> /// TopShelfからの開始用 /// </summary> public void Start() { string uri = string.Format("https://localhost:8080/"); _application = WebApp.Start<Startup>(uri); ; } /// <summary> /// TopShelfからの停止用 /// </summary> public void Stop() { _application?.Dispose(); } public void Configuration(IAppBuilder application) { // API 用の読み込みだよ UseWebApi(application); // Nancy つかうぉ application.UseNancy(options => options.Bootstrapper = new NancyBootstrapper()); } /// <summary> /// Provide API Action /// </summary> /// <param name="application"></param> private static void UseWebApi(IAppBuilder application) { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); application.UseWebApi(config); } /// <summary> /// Check Directory is exist or not /// </summary> /// <param name="path"></param> /// <returns></returns> private static bool IsDirectoryExist(string path) { return Directory.Exists(path); } /// <summary> /// Provide Current Build Status is Debug or not /// </summary> /// <returns></returns> public static bool IsDebug() { #if DEBUG return true; #endif return false; } } }
Startupクラスで使っていた NancyBootstrapper
は、DefaultNancyBootstrapperを継承したクラスです。
IRootPathProcider
を継承した NancyPathProcider.cs と合わせて作成します。
NancyPathProcider.cs はこうなります。
これで、Debug構成かどうかでルートパスを定めています。これが、Modules や Views の基底パスの指定となります。
using Nancy; using System; using System.IO; namespace NancySelfHost { class NancyPathProvider : IRootPathProvider { public string GetRootPath() { return Startup.IsDebug() ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\") : AppDomain.CurrentDomain.BaseDirectory; } } }
NancyBootstrapper.cs で、トレースや静的ファイルの設定が行えます。
using Nancy; using Nancy.Conventions; namespace NancySelfHost { class NancyBootstrapper : DefaultNancyBootstrapper { protected override IRootPathProvider RootPathProvider { get { return new NancyPathProvider(); } } protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines) { StaticConfiguration.EnableRequestTracing = true; StaticConfiguration.DisableErrorTraces = false; } protected override void ConfigureConventions(Nancy.Conventions.NancyConventions nancyConventions) { nancyConventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("Scripts", @"Scripts")); nancyConventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("fonts", @"fonts")); base.ConfigureConventions(nancyConventions); } } }
Nancy のルーティング
NancyFx/Nancyは View や Api へのルーティングを NancyModule
を継承した Moduleクラス で行っています。
例えば Index.cshtml のViewを返すルーティングならこんな感じです。
Modules/IndexModule.cs
using Nancy; using System.IO; namespace NancySelfHost.Modules { /// <summary> /// Index Pageに関するModuleです /// </summary> public class IndexModule : NancyModule { /// <summary> /// Index Pageを返却します。 /// </summary> public IndexModule() : base("/") { Get["/"] = parameters => { return View["index"]; }; } } }
返す Index.cshtml はこんな感じ。
Views/Index.cshtml
<div class="page-header"> <h1>目次</h1> </div> <div class="page-header"> <h2>なんかいろいろ</h2> <p>目次とかだすんです?</p> </div> <div class="row"> <div class="col-md-15"> <table class="table"> <thead> <tr> <th>タイトル1</th> <th>タイトル2</th> </tr> </thead> <tbody> <tr> <td>API</td> <td><a href="/api/test">/api/test</a></td> </tr> </tbody> </table> </div> </div>
View の調整
さくっと css や javascript などを整えましょう。View は苦手なので、BootStrap からもにょもにょいじります。
まずは _layout.css の追加
Contect/_layout.css
body { padding-top: 70px; padding-bottom: 30px; } .theme-dropdown .dropdown-menu { position: static; display: block; margin-bottom: 20px; } .theme-showcase > p > .btn { margin: 5px 0; } .theme-showcase .navbar .container { width: auto; }
他のcss や fonts は、ビルド時にコピーされるようにプロパティをいじっておきます。
これをしないと、ビルド時しても 成果物にcss などがなくて残念なことになるので注意です。
お次は、ASP.NET MVC でおなじみの _Layout.cshtml
や _ViewStart.cshtml
の生成です。
お決まりなので、さくっと BootStrap からてきとーにテーマをもってきた雑実装です。
Views/Shared/_Layout.cshtml
@using System.Runtime.Remoting.Contexts <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" href="/icon.png"> <title>NancySelfHost</title> <!-- Bootstrap core CSS --> <link href="/Content/bootstrap.min.css" rel="stylesheet"> <!-- Bootstrap theme --> <link href="/Content/bootstrap-theme.min.css" rel="stylesheet"> <!-- Custom styles for this template --> <link href="~/Content/_layout.css" rel="stylesheet"> <!-- Just for debugging purposes. Don't actually copy these 2 lines! --> <!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]--> <!--<script src="/assets/js/ie-emulation-modes-warning.js"></script>--> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> </head> <body role="document"> <!-- Fixed navbar --> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">NancySelfHost</a> <!-- Request Url Auto showing --> </div> <div id="navbar" class="navbar-collapse collapse"> </div><!--/.nav-collapse --> </div> </nav> <div class="container theme-showcase" role="main"> <!-- Main jumbotron for a primary marketing message or call to action --> @RenderBody() </div> <!-- /container --> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> <script src="/Scripts/bootstrap.min.js"></script> <!--<script src="/assets/scripts/docs.min.js"></script>--> </body> </html>
Views/_ViewStart.cshtml
@{ Layout = "Shared/_Layout.cshtml"; }
デバッグ
ここまでやると、ソリューションはこんな構成になっているでしょう。
サンプルのApi を作っていませんが、いったんはこれでデバッグしてみましょう。
Startup.cs で指定していた https://localhost:8080/ にアクセスすると、Index.cshtml が表示されると思います。
Api の追加
単純に、ホスティングしているサーバーの HostName と IPAddress をページ上に表示するようにしてみましょう。
まずは Modules に TestModule.cs を追加します。
base に /api
、Get に /test
を指定しているので、https://localhost/api/test
でアクセスできるように指定しています。
using Nancy; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; namespace NancySelfHost.Modules { /// <summary> /// Index Pageに関するModuleです /// </summary> public class TestAPIModule : NancyModule { /// <summary> /// Index Pageを返却します。 /// </summary> public TestAPIModule() : base("/api") { var hostName = System.Net.Dns.GetHostName(); Get["/test"] = parameters => { var model = new { HostName = hostName, IPAddress = Dns.GetHostAddresses(hostName).FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork).ToString() }; return View["test", model]; }; } } }
次に、Views に Api/Test.cshtml を追加します。先ほど Modules で View に渡した model は、@Model.プロパティ名
でアクセスできます。
<div class="panel panel-warning"> <ul> <li>HostName : @Model.HostName </li> <li>IPAddress : @Model.IPAddress </li> </ul> </div>
デバッグで見てみましょう。
うまく表示されたようです。
TopShelf によるサービスインストールとアンインストール
見ての通り、TopShelf はデバッグ実行では通常のコンソールアプリケーションと変わらず実行できます。これがデバッグがはかどる由縁です。
では、Windows Serviceとしてインストール、実行するにはどうするのでしょうか?
管理者権限で起動した cmd や PowerShell にて、ビルドした生成物 + install
引数で実行するとインストールします。uninstall
でアンインストールです。
今回の場合は、こんな感じです。
NancySelfhost.exe install
インストールされていますね。管理者権限でないとインストールできないので注意です。
サービスの一覧を見ると、TopShelf で指定したサービス名でインストールされていることがわかります。
Get-Service NancySelfHost
早速サービスを起動してみましょう。今回は、延々とデバッグメッセージがでますが、もちろん消せばいいでしょう。
うまく起動できました。コンソールアプリケーションと変わらず実行できています。
アンインストールしたければさくっとこれで。
NancySelfhost.exe uninstall
これできれいになっています。
OWIN + NancyFx/Nancyという選択肢
OWIN + NancyFx/Nancyどうでしたでしょうか?一度作ってしまえば、Api も View も追加が容易で非常に簡単という印象です。実際、OWIN に初めて触れる時には NancyFx/Nancyを使っていました。ところどころはまりながらも、比較的スムーズに慣れられたように思います。
が、今は LightNode に移行完了しており、LightNode にして正解だったと思っています。それは、ルーティング周りだったり、Swagger などの Apiテストだったり細かいところから始まります。*3
LightNode で同様の実装まで記事にしたかったのですが、長くなりすぎたので 次回に.....!
この記事が、初めて OWIN + Nancy を触る人にとって少しでも助けになれば幸いです。