以下の内容はhttps://tech.guitarrapc.com/entry/2015/05/19/025657より取得しました。


NancyFx/Nancy と TopShelf でIISに依存しないAPIサーバーを作ってみよう

前回は、TopShelfアプリケーションのデプロイをDSCで自動化する例を紹介しました。

https://tech.guitarrapc.com/entry/2015/05/13/015406

今回は、LightNode + TopShelfを使うことでIISに依存しないAPIサーバーを作ってみましょう。と、書いていたのですが、その前にNancyだとどうなるのか書いていたら長くなったのでLightNodeは次回です。ゴメンニャサイ。

APIサーバーを作りたい?

「APIサーバーをさくっと作りたい。簡易的なView付きで。けど、IIS上でASP.NET MVCやWeb APIを書くほどでもないしもっとさくっとAPIに集中して楽をしたい」

せっかくなのでHttpListenerTCPListenerSocketを使ってもにょもにょ書き比べてたりしましたがツラぽ。そして、今はもう自分で実装する必要はもうアリマセン。OWINがあります。

https://owin.org/

OWINを使うことで、HttpListenerを手触りする苦痛から解放されてAPIの実装に集中できます。ホストもIISから依存脱却してSelfHostも視野に入れることができます。特にIISからの分離はかなり大きく、TopShelfによるサービス稼動も視野に入れることができます。サーバーとアプリの分離うれしいです。*1

https://github.com/aspnet/DNX

OWIN と Framework

OWIN のページには、Frameworkがいくつか紹介されています。

Server/Hostsとして、Microsoft実装のKatana。

https://katanaproject.codeplex.com/

Frameworkとして、NancyFx/NancyやSignalR。どれも一度は目にしたことある有名どころです。

https://github.com/NancyFx/Nancy

https://github.com/SignalR/SignalR

この中で、「APIサーバー + View」をやってくれる望んだ機能を持っていたのがNancyFx/Nancyです。NancyFx/Nancyを使えば、API以外にもRazor構文でViewを返すことができます。使い慣れたcshtmlを使ってさくっと作れるのは魅力的でしょう。ちなみに他のViewEngineもありMarkDownなども....ただMarkDownはかなり気持ち悪い挙動をしたのでもう使うことはナイデショウ。

今回、LightNodeでAPIサーバーが超簡単に作れる紹介をしようと思ったのは、私がNancyFx/NancyからLightNodeに乗り換えてとても楽な思いをしたからでした。そこで、まずはNancyによる簡単なAPIサーバーを作成してみましょう。*2

リポジトリ

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

https://github.com/guitarrapc/NancySelfHost

では、早速みてみましょう。ここでは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の起動を書きます。

細かなことは本家のドキュメントで

https://github.com/Topshelf/Topshelf

https://docs.topshelf-project.com/en/latest/

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呼び出して、WebApp.Start<Startup>(uri);にて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_Layout.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に/apiを指定しているので、/apiでアクセスできるように指定しています。

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引数で実行するとインストールします。installでアンインストールです。

今回の場合は、こんな感じです。

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で同様の実装まで記事にしたかったのですが、長くなりすぎたので次回に。

*1:ASP.NET vNext (DNX) が来たら... という声もありますが、今は現実的な選択肢としてありだと判断しました。そんなこと言ったらSignalRどうするんだという気もしますし。

*2:記事を書きながら思いだしつつ作ったので抜けがありそうですが....

*3:組み込みじゃないって面倒なんですよね。




以上の内容はhttps://tech.guitarrapc.com/entry/2015/05/19/025657より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14