読者です 読者をやめる 読者になる 読者になる

tech.guitarrapc.cóm

C#, PowerShell, Unity, Cloud, Serverless Technical Update and Features

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

C# OWIN

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

tech.guitarrapc.com

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

目次

APIサーバーを作りたい?

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

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

OWIN — Open Web Interface for .NET

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

github.com

OWIN と Framework

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

f:id:guitarrapc_tech:20150518233322p:plain

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

katanaproject.codeplex.com

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

github.com

github.com

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

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

リポジトリ

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

github.com

では、早速みてみましょう。ここでは VS2015 RC で作成しています。

NancyFx/Nancy によるAPIサーバーを作ってみよう

VS2015 でコンソールアプリを作成しましょう。

f:id:guitarrapc_tech:20150518235911p:plain

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の起動を書きます。

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

github.com

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 ? "http://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を読み込み開始しています。

f:id:guitarrapc_tech:20150519000756p:plain

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("http://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 は、ビルド時にコピーされるようにプロパティをいじっておきます。

f:id:guitarrapc_tech:20150519020259p:plain

これをしないと、ビルド時しても 成果物に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";
}
デバッグ

ここまでやると、ソリューションはこんな構成になっているでしょう。

f:id:guitarrapc_tech:20150519020706p:plain

サンプルのApi を作っていませんが、いったんはこれでデバッグしてみましょう。

Startup.cs で指定していた http://localhost:8080/ にアクセスすると、Index.cshtml が表示されると思います。

f:id:guitarrapc_tech:20150519020733p:plain

Api の追加

単純に、ホスティングしているサーバーの HostName と IPAddress をページ上に表示するようにしてみましょう。

まずは Modules に TestModule.cs を追加します。

base に /api、Get に /test を指定しているので、http://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>

デバッグで見てみましょう。

f:id:guitarrapc_tech:20150519021343p:plain

うまく表示されたようです。

TopShelf によるサービスインストールとアンインストール

見ての通り、TopShelf はデバッグ実行では通常のコンソールアプリケーションと変わらず実行できます。これがデバッグがはかどる由縁です。

では、Windows Serviceとしてインストール、実行するにはどうするのでしょうか?

管理者権限で起動した cmd や PowerShell にて、ビルドした生成物 + install 引数で実行するとインストールします。uninstall でアンインストールです。

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

NancySelfhost.exe install

インストールされていますね。管理者権限でないとインストールできないので注意です。

f:id:guitarrapc_tech:20150519021734p:plain

サービスの一覧を見ると、TopShelf で指定したサービス名でインストールされていることがわかります。

Get-Service NancySelfHost

f:id:guitarrapc_tech:20150519021824p:plain

早速サービスを起動してみましょう。今回は、延々とデバッグメッセージがでますが、もちろん消せばいいでしょう。

f:id:guitarrapc_tech:20150519022009p:plain

うまく起動できました。コンソールアプリケーションと変わらず実行できています。

f:id:guitarrapc_tech:20150519022027p:plain

アンインストールしたければさくっとこれで。

NancySelfhost.exe uninstall

f:id:guitarrapc_tech:20150519023714p:plain

これできれいになっています。

OWIN + NancyFx/Nancyという選択肢

OWIN + NancyFx/Nancyどうでしたでしょうか?一度作ってしまえば、Api も View も追加が容易で非常に簡単という印象です。実際、OWIN に初めて触れる時には NancyFx/Nancyを使っていました。ところどころはまりながらも、比較的スムーズに慣れられたように思います。

が、今は LightNode に移行完了しており、LightNode にして正解だったと思っています。それは、ルーティング周りだったり、Swagger などの Apiテストだったり細かいところから始まります。*3

LightNode で同様の実装まで記事にしたかったのですが、長くなりすぎたので 次回に.....!

この記事が、初めて OWIN + Nancy を触る人にとって少しでも助けになれば幸いです。

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

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

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