1つのNancyアプリで、ASP.NET(IIS)とSelfHostingでホスティングできるように構成してみた

前回写経した書籍「Instant Nancy」の中に、「Separating applications and hosting (Advanced)」という章があり、1つのNancyアプリでASP.NETとSelfHostingでホスティングするという章がありました。

ただ、写経時点ではいくつか例外が出ており、また、その例外がNancy以外にも関係するものだったため、例外を解決するよりもNancy全体の理解を優先して先に進めました。

前回で写経も一段落したので、復習を兼ねて1つのNancyアプリで構成してみました。

なお、OWINでのホスティングは次のエントリで試しています。
C# + Nancyを使って、1つのアプリでHerokuとAzure Websitesの両方へデプロイできるように構成する - メモ的な思考的な

 

環境

   
具体的な流れは以下の通りです。

 

ソリューション・プロジェクトの作成

ソリューションの構成は以下とします。

プロジェクト名 プロジェクトテンプレート名 内容 プロジェクトの参照
NancyApp クラスライブラリ NancyのViewやBootstrapperなど、メインのNancyアプリ -
AspNetHosting ASP.NET 空のWebアプリケーション ASP.NET (IIS) でのホスティング設定 NancyAppを参照に追加
SelfHosting コンソールアプリケーション セルフホスティング設定 NancyAppを参照に追加

 
また、それぞれのプロジェクトに含むNuGetパッケージは以下の通りです。なお、今回はViewEngineとしてSSVEとRazorの2つを試します。

パッケージ名 バージョン NancyApp AspNetHosting SelfHosting
Nancy 1.0.0 o o o
Nancy.Hosting.AspNet 1.0.0 - o -
Nancy.Hosting.Self 1.0.0 - - o
Nancy.ViewEngines.Razor 1.0.0 o - -
Microsoft.AspNet.Razor 2.0.30506.0 o - -

 

Viewを使わずに「hello world」を表示

NancyAppプロジェクトにHomeModuleクラスを追加

/ というパスで文字列を表示できるようにします。

public class HomeModule : Nancy.NancyModule
{
    public HomeModule()
    {
        Get["/"] = _ => "hello world";
    }   
}

 

SelfHostingプロジェクトのProgram.csを実装

ApsNetHostingプロジェクトは上記だけで動作しますが、SelfHostingプロジェクトの場合はProgram.csの実装が必要になります。

class Program
{
    static void Main(string[] args)
    {
        var port = 8765;
        var nancyHost = new Nancy.Hosting.Self.NancyHost(new Uri("http://localhost:" + port));
        Console.WriteLine("Running port: {0}", port);

        nancyHost.Start();

        Console.ReadKey();

        nancyHost.Stop();
    }
}

 
また、ポート8765を利用できるよう、名前空間予約の構成を追加します。

netsh http add urlacl url=http://+:8765/ user=Everyone

なお、不要になったら以下で削除します。

netsh http delete urlacl url=http://+:8765/

 

実行

AspNetHostingとSelfHosting、それぞれスタートアッププロジェクトにして実行すると、「hello world」が表示されます。

 
ここまでのコミットです。
thinkAmi-sandbox/NancyMultiHosting-sample at 4f3c4185a7372a16f7cd0ffe5dc8adc12b2ae3b7

 

ViewEngineを使わない、静的HTMLを表示

NancyAppプロジェクトに、HTMLファイルの追加
  • NancyAppプロジェクトに新しいフォルダとしてViewsを追加
  • その中にstatic.htmlというHTMLファイルを追加
  • HTMLの中身は、bodyタグに「hello static file」を記述
  • HTMLファイルのプロパティを変更
    • ビルドアクション: コンテンツ
    • 出力ディレクトリにコピー: 常にコピーする

 

HomeModuleの編集

/static というパスで静的HTMLを表示できるようにします。

public class HomeModule : NancyModule
{
    public HomeModule()
    {
        Get["/static"] = _ => View["static"];
    }   
}

 

実行して確認

SelfHostingでは動作するものの、AspNetHostingでは以下のエラーが出て動作できません。

Nancy.RequestExecutionException: Oh noes! ---> Nancy.ViewEngines.ViewNotFoundException: Unable to locate view 'static'
Currently available view engine extensions: sshtml,html,htm,cshtml,vbhtml
Locations inspected: views/Home/static-ja,views/Home/static,Home/static-ja,Home/static,views/static-ja,views/static,static-ja,static
Root path: path\to\project\AspNetHosting\

 
エラーよりAspNetHostingではHTMLのあるフォルダを認識できていないようなので、HomeModuleにstatic.htmlのあるフォルダのパスを追加します。

Get["/static"] = _ => View["bin/Content/static"];

 
しかし、AspNetHostingは動作するようになりましたが、今度はSelfHostingの方がエラーで動作しなくなりました。

Nancy.RequestExecutionException: Oh noes! ---> Nancy.ViewEngines.ViewNotFoundException: Unable to locate view 'bin/Views/static'
Currently available view engine extensions: sshtml,html,htm
Locations inspected: views/Home/bin/Views/static-ja,views/Home/bin/Views/static,Home/bin/Views/static-ja,Home/bin/Views/static,views/bin/Views/static-ja,views/bin/Views/static,bin/Views/static-ja,bin/Views/static
Root path: D:\Sandbox\NancyMultiHosting-sample\SelfHosting\bin\Debug

 

Bootstrapperの追加

そこで、両方でViewを認識させるためにNancyAppプロジェクトにBootstrapperクラスを用意し、静的HTMLのあるフォルダをViewのフォルダとして追加します。
View location conventions · NancyFx/Nancy Wiki · GitHub

public class BootStrapper : Nancy.DefaultNancyBootstrapper
{
    protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
    {
        base.ApplicationStartup(container, pipelines);

        this.Conventions.ViewLocationConventions.Add((viewName, model, context) =>
        {
            return string.Concat("bin/Views/", viewName);
        });
    }
}

 
これでAspNetHostingでもフォルダを認識できるようになったため、HomeModuleから不要なパスは削除しておきます。

Get["/static"] = _ => View["static"];

 
ここまでのコミットです。
thinkAmi-sandbox/NancyMultiHosting-sample at 00ff4cbacfd22978af6a8af3d71ac26914e9ba40

 

SuperSimpleViewEngine(SSVE)のViewを試す

まずはデフォルトのViewEngineである、SSVEを試します。

 

Viewの追加

よく使いそうなMasterページを含むViewを作ります。

  • NancyAppプロジェクトのViewsフォルダの下に、main.sshtmlssve.sshtmlを作成'
    • HTMLファイルテンプレートを使ったあと、拡張子sshtmlへと変換
  • 2つのsshtmlファイルとも、プロパティを変更
    • ビルドアクション: コンテンツ
    • 出力ディレクトリにコピー: 常にコピーする
  • master.sshtmlは、bodyタグにhello ssveを追加
  • ssve.sshtmlは、すべてのタグを削除し、@Master['master.sshtml']のみを記載

 

HomeModuleへの追加

/ssve というパスでSSVEのViewを表示できるようにします。

Get["/ssve"] = _ => View["ssve"];

 
以上により、SSVEを使ったAspNetHostingとSelfHostingの両方が動作しました。

 
ここまでのコミットです。
thinkAmi-sandbox/NancyMultiHosting-sample at f1b7e518ca03c2a6f862abf55595cc8ce3acf438

 

Razor ViewEngine のViewを試す

NancyでRazorを使う場合、NuGetパッケージを追加しただけではRazorのViewを表示できません。
Razor View Engine · NancyFx/Nancy Wiki · GitHub

上記にはいくつか方法が記載されていますが、動かすのが簡単と書かれているIRazorConfigurationを使う方法で試してみます。

 

NancyAppプロジェクトに、インタフェースIRazorConfigurationを実装したクラスを作成

NancyAppプロジェクトに RazorConfig クラスを作成します。なお、Razor ViewEngineを使うのはNancyAppプロジェクト内だけなので、指定するアセンブリ名前空間NancyAppだけです。

public class RazorConfig : Nancy.ViewEngines.Razor.IRazorConfiguration
{
    public IEnumerable<string> GetAssemblyNames()
    {
        yield return "NancyApp";
    }

    public IEnumerable<string> GetDefaultNamespaces()
    {
        yield return "NancyApp";
    }

    public bool AutoIncludeModelNamespace
    {
        get { return true; }
    }
}

 
これ以外には追加する必要はなく、このままでRazor ViewEngineが動作します。

 

Viewの追加
  • NancyAppプロジェクトのViewsフォルダの下に、layout.sshtmlrazor.sshtmlを作成
    • HTMLファイルテンプレートを使ったあと、拡張子cshtmlへと変換
  • 2つのcshtmlファイルとも、プロパティを変更
    • ビルドアクション: コンテンツ
    • 出力ディレクトリにコピー: 常にコピーする
  • それぞれのファイル内容は以下の通り

 

layout.cshtml
<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
    @RenderSection("js", required:false)
</head>
<body>
    @RenderBody()
</body>
</html>

 

razor.cshtml
@{
    Layout = "layout.cshtml";
}

<h1>hello razor</h1>

@section js{
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
}

 

HomeModuleの追加

/razor というパスでRazorのViewを表示できるようにします。

Get["/razor"] = _ => View["razor"];

 
AspNetHostingとSelfHostingとも、エラーが出ることなくRazorで表示でき、HTMLソースにもjavascriptタグが追加されています。

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
    
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>

</head>
<body>
    

<h1>hello razor</h1>


</body>
</html>

 

ソースコード

Razor ViewEngineまでを含めた最終的なものはこちらです。
thinkAmi-sandbox/NancyMultiHosting-sample

 

Azure WebsitesとHerokuへのデプロイ (2015/2/5追記)

上記のソースコードを使った場合、Azure Websitesはデプロイしても動作しますが、Herokuは以下の例外で動作しませんでした。

...
[ERROR] FATAL UNHANDLED EXCEPTION: System.TypeInitializationException: An exception was thrown by the type initializer for Nancy.Bootstrapper.AppDomainAssemblyTypeScanner ---> System.Reflection.ReflectionTypeLoadException: The classes in the module cannot be loaded.
  at System.Reflection.Assembly.GetExportedTypes () [0x00000] in <filename unknown>:0
...

同じようなエラーはstackoverflowにありました。
c# - The type initializer for 'Nancy.Bootstrapper.AppDomainAssemblyTypeScanner' threw an exception - Stack Overflow

ただ、バージョン1.0.0のNancyアプリを最初から作成していることと、次のエントリによりHerokuはOWINを使えば動作することが分かったため、今回はこれ以上追求しません。

 

参考