しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET の App Settings と Connection Strings の値を環境変数からオーバーライド可能にする

App Service は Azure Portal から設定した App Settings と Connection Strings の値を、いい感じにオーバーライドしてくれるのが非常に便利だったので、同じように環境変数からオーバーライド出来るようにするライブラリを書きました。主に Docker 向けとして考えてます。

リポジトリは用意しましたが、NuGet とかにはしてないです。今後考えます。

やってることは App Service と同じプリフィックスが付いた環境変数を読み取って、ConfigurationManager の値を書き換えているだけです。App Service がどう実装してるのかは謎です。

Application_Start より早く処理しておきたいので、PreApplicationStartMethod を使いました。

using System;
using System.Collections;
using System.Configuration;
using System.Linq;
using System.Web;

[assembly: PreApplicationStartMethod(typeof(OverrideConfig.OverrideBootstrapper), "Start")]

namespace OverrideConfig
{
    public class OverrideBootstrapper
    {
        public static void Start()
        {
            var environmentVariables = Environment.GetEnvironmentVariables()
                                                  .Cast<DictionaryEntry>()
                                                  .ToDictionary(x => (string)x.Key, x => (string)x.Value);

            foreach (var appSetting in environmentVariables.Where(x => x.Key.StartsWith(AppSettingsPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                ConfigurationManager.AppSettings[appSetting.Key.Substring(AppSettingsPrefix.Length)] = appSetting.Value;
            }

            var connectionStrings = new ConnectionStringSettingsCollectionWrapper(ConfigurationManager.ConnectionStrings);

            foreach (var connectionString in environmentVariables.Where(x => x.Key.StartsWith(SqlServerConnStrPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                connectionStrings.AddOrUpdate(connectionString.Key.Substring(SqlServerConnStrPrefix.Length), connectionString.Value, SqlServerProviderName);
            }
        }

        private const string AppSettingsPrefix = "APPSETTINGS_";
        private const string SqlServerConnStrPrefix = "SQLSERVERCONNSTR_";

        private const string SqlServerProviderName = "System.Data.SqlClient";
    }
}

ループを 2 つ回してるのがちょっとイケてない感じしますが、Connection Strings は後付けだったので暇なときに何とかする方向にします。

AppSettings は読み書きが自由なので問題なかったのですが、ConnectionStrings は読み取り専用になってしまっていて、そのままだとコレクションの操作が出来なかったです。少し調べてみると、リフレクションを使って内部のフラグを書き換えろとありました。

ちょっと無理やりな気もしますが、それ以外は Web.config を物理的に書き換える方法しか見つからなかったので、リフレクションを使って書き換える方法を採用することにしました。

最近は Reference Source のおかげで、こういった内部のフラグを調べるのが楽になりましたね。

https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ConfigurationElement.cs,58
https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ConfigurationElementCollection.cs,29

AppSettings とインターフェースを出来るだけ合わせるために、Wrapper を用意して内部でリフレクションを使ってフラグを書き換えるようにしました。

この時 Collection と Element の両方で書き換え処理が必要なので注意です。

internal class ConnectionStringSettingsCollectionWrapper
{
    public ConnectionStringSettingsCollectionWrapper(ConnectionStringSettingsCollection connectionStrings)
    {
        _connectionStrings = connectionStrings;

        typeof(ConfigurationElementCollection).GetField("bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
                                                .SetValue(_connectionStrings, false);
    }

    private readonly ConnectionStringSettingsCollection _connectionStrings;

    public void AddOrUpdate(string name, string connectionString, string providerName)
    {
        var settings = _connectionStrings[name];

        if (settings == null)
        {
            _connectionStrings.Add(new ConnectionStringSettings
            {
                Name = name,
                ConnectionString = connectionString,
                ProviderName = providerName
            });
        }
        else
        {
            typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
                                        .SetValue(settings, false);

            settings.ConnectionString = connectionString;
            settings.ProviderName = providerName;
        }
    }
}

既に同名のキーが存在する場合には上書き、存在しない場合には新規追加という風にしました。

最後に簡単に動作確認だけしておきます。今回は以下のような設定を Web.config に追加しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="TEST" value="11111"/>
  </appSettings>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Server=.\sqlexpress;Database=default;Trusted_Connection=True;" providerName="System.Data.SqlClient"/>
  </connectionStrings>
</configuration>

今回、環境変数として登録した値は以下の通りです。プリフィックスは省略しますが TEST / TEST2 / DefaultConnection をオーバーライドするように環境変数を追加してあります。

f:id:shiba-yan:20170506230052p:plain

そして上で作成したクラスライブラリを読み込むようにした ASP.NET アプリケーションを作成して、AppSettings と ConnectionStrings の値を一覧表示したものが以下になります。

f:id:shiba-yan:20170506230116p:plain

正しく環境変数に追加した値が反映されていることが確認できました。面倒だったので載せてはいないですが、Application_Start の時点でもオーバーライドされた値が取れるようになっているので安心です。

これで Windows Server Containers で ASP.NET アプリケーションを動かす時に、環境変数から接続文字列などを渡すことが出来るようになりました。アプリケーション側の改修が要らないのがメリットです。

AppVeyor を使って ASP.NET MVC アプリケーションの Docker Image を自動で作成してみる

前に AppVeyor の Windows Server 2016 と Docker 対応について調べたので、今回は実際に ASP.NET MVC アプリケーションをビルドして、Docker Image の作成までを行ってみます。

やることは CircleCI と Docker を使って App Service on Linux への CI/CD を試した時とほぼ変わりません。処理をそのまま AppVeyor と ASP.NET 向けに修正した形です。

Windows Server Core のイメージが 10GB ぐらいあるので、ビルドにも時間がかかって使い物にならないかもと思いましたが、今回の場合 2 分以下で終わりました。実用的な気がしてきます。

最初に ASP.NET MVC アプリケーションを作成してから、appveyor.yml を書いていきます。

appveyor.yml を用意する

とりあえず作成した appveyor.yml を全て載せておきます。build / test / deploy のそれぞれでカスタムなスクリプトを書いていますが、やっていること自体は大したことはありません。

テストでは作成したイメージを起動して、ちゃんとレスポンスを返すか確認しています。*1

version: '{build}'

image: Visual Studio 2017

environment:
  DOCKER_USER: XXXXXXX
  DOCKER_PASS:
    secure: XXXXXXX

before_build:
  - nuget restore

build_script:
  - msbuild .\AspNetApplication\AspNetApplication.csproj /nologo /verbosity:m /t:Build /t:pipelinePreDeployCopyAllFilesToOneFolder /p:_PackageTempDir="..\publish";AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release
  - docker build -t shibayan/appveyor-aspnet-docker .

test_script:
  - ps: |
      $cid = docker run -d shibayan/appveyor-aspnet-docker
      $hostip = docker inspect -f "{{ .NetworkSettings.Networks.nat.IPAddress }}" $cid
      curl "http://${hostip}" -UseBasicParsing

deploy_script:
  - docker login -u %DOCKER_USER% -p %DOCKER_PASS%
  - docker push shibayan/appveyor-aspnet-docker

面倒なのが IP を inspect で取得する部分です。これはしばらく改善しなさそうなので諦め気味です。

用意した Dockerfile は以前に試したものとほぼ変わらず、wwwroot 以下にビルドした成果物をコピーするだけの簡単なものです。ベースイメージは 4.6.2 とタグを固定しました。

FROM microsoft/aspnet:4.6.2

COPY ./publish/ /inetpub/wwwroot

MSBuild によってビルドされたファイルは publish というディレクトリに全て保存されるので、その中身をそっくり wwwroot にコピーしています。

イメージのテストが通れば、後は Docker Hub に push するだけという簡単な処理です。

AppVeyor で実際にビルド

GitHub にリポジトリを作成して、AppVeyor で実際にビルドしてみました。事前に調べていた甲斐もあって、特に失敗することなく Docker Hub への push まで上手くいきました。

MSBuild 周りが一番複雑な部分ですが、App Service で良く使っていたコマンドなので問題なかったです。

f:id:shiba-yan:20170502184818p:plain

MSBuild が終わると Docker Image を作成します。microsoft/aspnet の pull に 1 分ぐらいかかるので、予め pull しておきたい気持ちでいっぱいです。

Docker Image の作成が終わると、実際に起動してテストを行っています。

f:id:shiba-yan:20170502184851p:plain

今回のコマンドでは Razor 周りのプリコンパイルまでは行っていないので、起動するまでに少し時間がかかりますが、それでも数秒で成功します。

最後に Docker Hub に push して完了ですが、例によって Azure Container Registry などに push する形にしても良いかと思います。特に ACS を使う場合には便利だと思います。

作成したイメージの動作確認

AppVeyor でのビルドが成功すると Docker Hub に作成されたイメージが追加されているので、検証用の Windows Server 2016 が入ったサーバーを使って動作確認をしておきます。

テストで利用しているコマンドとほぼ同じものを実行すると、Docker Hub から pull してくれます。

f:id:shiba-yan:20170502182213p:plain

一応 PowerShell 上でも同じように URL を叩いてレスポンスが返ってくるのを確認しました。

コンテナホストからは確認できたので、外部の PC からブラウザを使って確認してみました。外部からのアクセス時には WinNAT によってポート変換がされるので 8080 が必要です。

f:id:shiba-yan:20170502182915p:plain

すんなりと ASP.NET MVC アプリケーションが動作しました。コンテナとホストでビルド番号を合わせる必要があるという手間はかかってきますが、手軽にアプリケーションを実行できるのは魅力です。

Azure Container Service や EC2 Container Service などの Windows Server Containers に対応した環境にそのまま持っていけるはずですが、まだ正式リリースされていないのが課題という感じです。

*1:個人的にはちゃんと動かないイメージを生焼けと呼んでます。どうでもいい話だった。

Docker Compose を使って DB 付きの ASP.NET アプリケーションを実行する

ASP.NET MVC アプリケーションは microsoft/aspnet イメージを使うことで簡単に Windows Containers で動作しましたが、実際のアプリケーションには大抵 DB がセットなので、DB を含めた形でアプリケーションを Docker で動かしてみました。

SQL Server 2016 も Docker Hub でイメージが公開されているので、これを使っていきます。

アプリケーションと DB のコンテナ両方を立ち上げる必要があるので、管理には Docker Compose を使います。依存関係を定義できるので、管理が非常に簡単になります。

Docker Compose の新しいフォーマットで書いてみたところ、コンテナの起動時にエラーになってしまいましたが、以下のサンプルに含まれる定義を参考にして解消できました。

networks の設定を追加しておかないと、現在の Windows Containers では問題になるようです。これもまた WinNAT の制約っぽいです。

作成した docker-compose.yml は以下になります。web と db の 2 つを定義してあります。

version: '2'

services:
  web:
    build: ./app
    depends_on:
      - "db"
    ports:
      - "8080:80"

  db:
    image: microsoft/mssql-server-windows-express
    ports:
      - "1433:1433"
    volumes:
      - "C:/temp/:C:/temp/"
    environment:
      - sa_password=P@ssw0rd
      - ACCEPT_EULA=Y
      - attach_dbs="[{'dbName':'SampleDB','dbFiles':['C:\\temp\\AdventureWorksLT2012_Data.mdf']}]"

networks:
  default:
    external:
      name: nat

web は Dockerfile に基づいてイメージを作成するようにしてあります。DB は attach_dbs でアタッチしたい DB ファイルを指定しておき、コンテナ起動後に SampleDB という名前で使えるようにしてあります。

この docker-compose.yml があるディレクトリで docker-compose up -d を実行すると、ビルドが行われた後にコンテナが依存関係に従って起動されます。

f:id:shiba-yan:20161207221936p:plain

用意した ASP.NET アプリケーションは DB の内容を一覧表示するだけの、とても単純なものです。

毎回 IP アドレスを docker inspect で取得するのが面倒になったので、別マシンから確認してみました。

f:id:shiba-yan:20161207215645p:plain

上の例ではプリコンパイル済みの ASP.NET アプリケーションを使いましたが、Dockerfile を工夫してプロジェクトフォルダをマウントし、サイトの物理パスを書き換えると開発しながら実行することもできます。

FROM microsoft/aspnet:4.6.2

RUN %windir%\system32\inetsrv\appcmd set vdir "Default Web Site/" -physicalPath:"C:\app"

appcmd を使って Default Web Site の物理パスを C:\app に変更しています。

このままだと IIS に設定したパスにはディレクトリすら存在しないので、docker-compose.yml 側で ASP.NET プロジェクトのディレクトリをマウントするように設定を追加します。

services:
  web:
    build: .
    depends_on:
      - "db"
    ports:
      - "8080:80"
    volumes:
      - "C:/app/WebApplication2/:C:/app/"

これでコンテナを起動すると ASP.NET プロジェクトのパスをサイトのルートとして IIS が起動するので、ビューを変更してリロードすると再コンパイルが行われて画面に反映されるようになります。

過去プロジェクトの環境を維持するのが面倒でしたが、Docker Compose を使えば DB のファイルを保持しておくだけで、簡単に維持が出来そうです。

Windows Containers を使って既存の ASP.NET MVC アプリケーションを動かす

Azure App Service の Linux 版が Docker に対応したり、Azure Container Registry がプレビューになったりと、Docker と Container に対応しておくと色々幸せになれそうなので、既存の ASP.NET アプリケーションに絞って調べることにしました。

Azure Container Service は Windows Containers 対応がプライベートプレビューになっていますし、ASP.NET 4.6 アプリケーションを Windows Containers 対応にしておくと有利になりそうです。

今後は App Service でも Windows Containers 対応が来る可能性もありますし、対応しておいて損はないと思います。対応させるための手間もほとんどかかりません。

既に Docker Hub で公開されている IIS イメージを使って ASP.NET アプリケーションは動かしてますが、もう少し掘り下げていくことにします。

Docker Hub に公開されている ASP.NET 向けのイメージは .NET Framework 3.5 と 4.6.2 向けの 2 つが公開されてます。分かりにくいことに ASP.NET Core も混ざっているので、注意が必要です。

ビルドに使われている Dockerfile のリポジトリは公開されています。

ありがたいことに ASP.NET MVC アプリケーションを Docker で動かす方法もドキュメントとして公開されています。関係ないですが docs.microsoft.com はかなり見やすくて良いですね。

このドキュメント通りで問題ないですが、少し冗長な部分があったので Dockerfile をもっとシンプルに書き換えて、Windows Containers で動かしてみました。

適当に ASP.NET MVC アプリケーションを作成し、Dockerfile を追加します。

f:id:shiba-yan:20161123163252p:plain

Dockerfile には以下のように記述します。microsoft/aspnet イメージは IIS に ASP.NET 4.5 の追加や ENTRYPOINT の指定を行ってくれているので、アプリケーションを wwwroot にコピーするだけで動きます。

FROM microsoft/aspnet:4.6.2

COPY . /inetpub/wwwroot

COPY コマンドで現在のディレクトリにあるファイルを全て wwwroot にコピーするようにします。

プロジェクトに追加した Dockerfile は、プロパティからビルドアクションをコンテンツに変えておきます。こうしないと発行時にファイルがコピーされません。

f:id:shiba-yan:20161123163612p:plain

アプリケーション側の準備はたったこれだけで終わりました、非常に簡単ですね。URL Rewrite や ARR が必要な場合には Web PI 経由でインストールしたり、MSI を実行したりと追加しないといけませんが、そのイメージを作ってしまえば済む話です。

このアプリケーションの Docker イメージを作成するために、発行プロファイルを作成していきます。

f:id:shiba-yan:20161123163324p:plain

イメージを作るためにはファイルシステムに必要なファイルを書き出す必要があるので、発行方法としてファイルシステムを選んで出力先を適当に選びます。

f:id:shiba-yan:20161123163419p:plain

Razor ビューもすべてプリコンパイルするようにして、起動時のパフォーマンスを改善します。

f:id:shiba-yan:20161123163450p:plain

これで発行を行うと Dockerfile を含む全てのファイルが出力されます。あとはこのファイルを元に Docker コマンドを使ってイメージを作成するだけです。

f:id:shiba-yan:20161123163708p:plain

イメージの作成は docker build コマンドを使います。あっという間に完了します。

f:id:shiba-yan:20161123163802p:plain

本来なら作成したイメージを Docker Hub や Azure Container Registry に追加して、ACS や ECS などのオーケストレーションツールを使う形になると思いますが、両方とも対応していないのでローカルで行います。

イメージを実行するために docker run コマンドを使います。

f:id:shiba-yan:20161123164011p:plain

イメージの起動後に IP アドレスを取得して、ブラウザからアクセスするとページが表示されます。

f:id:shiba-yan:20161123164058p:plain

Visual Studio Tools for Docker の Windows Containers 対応が来ると簡単に対応できるようになりそうですが、Visual Studio 2017 待ちになりそうです。あと、いい加減に WinNAT の対応を行ってほしい気持ちです。

Azure App Service や AWS Elastic Beanstalk で Windows Containers が使えるようになると、かなり利用が簡単になると思うので対応を期待してます。

Windows Server 2016 の Windows Containers と Docker を使って IIS と ASP.NET を動かしてみた

Windows Server 2016 が MSDN からダウンロード出来るようになっていたので、Service Fabric Cluster 用に買った Intel NUC にインストールしてみました。

f:id:shiba-yan:20161014002113p:plain

Hyper-V 上に入れた 2016 で Windows Containers を試したとき、1 日経ってもコンテナが起動しなかったのでホスト OS に Windows Container をインストールして再度試しました。

MSDN のドキュメントも更新されているようなので、Docker を含んだセットアップは手順通り行いました。

ドキュメントにある .NET Core のサンプルはあっさり動作したので、前に Hyper-V 上だと実行に失敗した microsoft/iis イメージを実行してみます。

既に Windows Container と IIS を使って ASP.NET を動作させる方法が色んなところで紹介されてます。

ASP.NET アプリケーションを用意するのは正直面倒だったので、とりあえず IIS のデフォルトページが表示されるところまでやります。

イメージをそのまま pull して、実行するだけという簡単なコマンドです。

docker pull microsoft/iis

docker run -d -p 80:80 microsoft/iis

pull には物凄く時間がかかりますが、一度イメージを作成してしまえば起動は高速です。docker run を実行してから数秒でコンテナが起動しました。

docker ps で実行中のコンテナを確認できます。ポートの設定も同時に見れるので便利です。

f:id:shiba-yan:20161014003103p:plain

これでコンテナと同時に IIS も起動しているので、ブラウザで見るといつものページが表示されます。

ここまで時間は 30 分ぐらいかかりましたが、9 割がイメージのダウンロードと作成でした。

f:id:shiba-yan:20161014002753p:plain

理由はよくわかりませんでしたが、コンテナホストから localhost は見れませんでした。別のマシンからは表示されたので、実際にはあまり問題にならない気はします。

実行中のコンテナには docker exec を使えば入ることが出来ます。コンテナの ID が必要なので、予め docker ps を実行してコピーしておきます。

docker exec -i -t CONTAINER_ID cmd

powershell を指定してもいいのですが、背景色が狂ってしまって見にくかったのでとりあえず cmd で行きます。これを実行すると、コンテナに入ることが出来ます。

f:id:shiba-yan:20161014004026p:plain

コンテナホストは日本語 OS ですが、コンテナの中は英語になっていることが分かります。IIS がインストールされたコンテナなので inetpub がありますが、コンテナホストには存在していません。抜け出すには exit と入力するだけです。

機嫌よくなってきたので ASP.NET も簡単なアプリを用意して動かしてみることにしました。こっちも先人の知恵を頼りに Dockerfile を用意してイメージを作成します。

SHELL で powershell を使うようにすると、コマンドレットがそのまま使えるので便利ですね。ASP.NET の実行に必要な機能をインストールしておきます。

FROM microsoft/iis

SHELL ["powershell"]

RUN Install-WindowsFeature NET-Framework-45-ASPNET ; \
    Install-WindowsFeature Web-Asp-Net45

COPY . C:\\inetpub\\wwwroot

CMD ["ping", "-t", "localhost"]

何もしないとコンテナがすぐ終わってしまうので、終わらないような処理を最後に書いておきます。これは他にもっと良い手があるのではないかと思います。

Dockerfile をビルドした ASP.NET アプリケーションと同じディレクトリに置いて、docker build を実行してイメージを作成します。

f:id:shiba-yan:20161014011401p:plain

これも 2 回目以降はキャッシュが使われるので非常に高速です。

後はこれまで通り docker run に作成したイメージ ID を渡せば、そのイメージが起動します。

f:id:shiba-yan:20161014011912p:plain

ブラウザでアクセスすると、ASP.NET アプリケーションが実行されているのを確認できます。マシン名は Windows Containers によって適当に付けられているみたいです。

普通の Windows Server と IIS が動いているので、Dockerfile に必要な IIS 拡張をインストールする処理を書けば、URL Rewrite や ARR も問題なく使えると思います。Docker の理解が深まりました。

ASP.NET MVC で reCAPTCHA を使ってみた

今更感が半端ないですが、ASP.NET MVC で開発したアプリケーションに reCAPTCHA の version 2 を組み込む方法を調べました。何番煎じか分かりませんが、小ネタ程度にまとめておきます。

reCAPTCHA を使うためには Google のサイトから URL を登録して、トークンを貰う必要があります。

reCAPTCHA: Easy on Humans, Hard on Bots

ドキュメントも数ページしかないので、これを読めば誰でも実装は出来るはずです。

Developer's Guide  |  reCAPTCHA  |  Google Developers

流れとしてはビューにタグを追加しておき、POST されたデータを API に投げて正しいかチェックするだけです。この流れは ValidateAntiForgery と似てるので、例によって Action Filter として実装します。

とりあえず作成した ValidateReCaptcha 属性のコードを丸ごと載せておきます。

public class ValidateReCaptchaAttribute : ActionFilterAttribute
{
    private const string SiteVerifyEndpoint = "https://www.google.com/recaptcha/api/siteverify";
    private const string ResponseFieldKey = "g-recaptcha-response";

    private static readonly string SecretKey = ConfigurationManager.AppSettings["ReCaptcha:SecretKey"];

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var data = new NameValueCollection
        {
            { "secret", SecretKey },
            { "response", filterContext.HttpContext.Request.Form[ResponseFieldKey] },
            { "remoteip", filterContext.HttpContext.Request.UserHostAddress }
        };

        var json = new WebClient().UploadValues(SiteVerifyEndpoint, data);

        var result = JsonConvert.DeserializeObject<ReCaptchaVerifyResponse>(Encoding.UTF8.GetString(json));

        if (!result.Success)
        {
            throw new ReCaptchaValidationException(result.ErrorCodes[0]);
        }
    }
}

public class ReCaptchaVerifyResponse
{
    public bool Success { get; set; }

    [JsonProperty("error-codes")]
    public string[] ErrorCodes { get; set; }
}

使い方の説明は要らない気がしますが、検証したいアクションに属性を付けるだけです。

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    [ValidateReCaptcha]
    public ActionResult Index(FormCollection collection)
    {
        return View("Success");
    }
}

用意したビューは以下のような感じです。特に言うことはありません。

<script src="https://www.google.com/recaptcha/api.js"></script>

<h2>Index</h2>

@using (Html.BeginForm())
{
    <div class="g-recaptcha" data-sitekey="..."></div>
    <br/>
    <button type="submit">Submit</button>
}

これで準備が出来たので、実際に実行して動作を確認しておきます。ブラウザでページを表示すると、誰もが 1 回は見たことあると思われる CAPTCHA が表示されます。

f:id:shiba-yan:20160113160941p:plain

チェックを入れて Submit を行うと、検証が成功するのでページが表示されます。

f:id:shiba-yan:20160113160823p:plain

チェックを入れずに Submit を行うと、検証に失敗して例外が投げられるので、そこで処理が中断されます。

f:id:shiba-yan:20160113161019p:plain

思ったより簡単な仕組みだったので、昼休みの暇潰し程度で試せました。ASP.NET MVC 6 だと Action Filter が Task ベースになるので、HttpClient を使いたいところです。

FatAntelope を使って XML Document Transform (XDT) を自動で生成する

主に ASP.NET アプリケーションの Web.config 変換で使われている XML Document Transform (XDT) ですが、個人的にはあれを手書きするのはかなり嫌いでした。

XML の差分を持っているだけなので、自動生成できるのではないかと長い間思ってましたが、最近 FatAntelope というまさに待ち望んでいたライブラリが公開されました。

2 つの XML を指定すると、自動的に差分を取って XDT として書き出してくれます。最高ですね。コマンドラインツールやライブラリの形で公開されているので、簡単に使い方の紹介をしておきます。

XDT を生成する

FatAntelope のコマンドラインツールは GitHub で公開されているので最新版をダウンロードしてきます。

今回はサンプルとして Web.config っぽいものを適当に用意しました。開発時に使うものになります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=DevelopmentDB;Integrated Security=True"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" />
  </system.web>
</configuration>

そしてこっちが本番用の Web.config になります。内容としては接続文字列を本番向けにして、リリースビルドを行うというものです。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Server=tcp:***.database.windows.net,1433;Database=ProductionDB;User ID=***;Password=***;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" />
  </system.web>
</configuration>

この 2 つのファイルを FatAntelope に与えると、XDT が生成されます。生成された XDT はひとまず Web.Release.config とでも名前を付けて保存するようにします。

FatAntelope Web.config Production.config Web.Release.config

コマンドを実行すると、コンソールにメッセージが出るのでエラーにならなければ成功です。

f:id:shiba-yan:20151225130633p:plain

そして出力された XDT が以下のようになります。ちゃんと差がある部分だけを抽出してくれています。

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add connectionString="Server=tcp:***.database.windows.net,1433;Database=ProductionDB;User ID=***;Password=***;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;" xdt:Transform="SetAttributes(connectionString)" />
  </connectionStrings>
  <system.web>
    <compilation xdt:Transform="RemoveAttributes(debug)" />
  </system.web>
</configuration>

手書きの場合と比較すると xdt:Locator が無かったりと、少し違和感があるかもしれませんが、XDT の仕様としてはこれで問題はありません。XML 宣言は何故か付かないので、後で追加しましょう。

C# から XDT を生成する

FatAntelope は NuGet でライブラリが公開されているので、API には多少癖がありますが、自分のアプリケーションなどに簡単に組み込むことが可能です。

当然ながら NuGet を使って FatAntelope をインストールする必要があります。

Install-Package FatAntelope

FatAntelope の API は多少癖があるので、最低限の XDT 生成を行うコードを紹介しておきます。

using System;
using System.Xml;

using FatAntelope;
using FatAntelope.Writers;

namespace ConsoleApplication17
{
    class Program
    {
        static void Main(string[] args)
        {
            // 元になる XML
            var baseXml = new XmlDocument();

            baseXml.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.config");

            // 新しい XML
            var newXml = new XmlDocument();

            newXml.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Production.config");

            // XmlDocument から XTree を作成しておく
            var baseTree = new XTree(baseXml);
            var newTree = new XTree(newXml);

            // XDiff を使って差分を計算し、newTree に格納
            XDiff.Diff(baseTree, newTree);

            // 差分が格納された XTree を元に XDT として書き出し
            var patch = new XdtDiffWriter().GetDiff(newTree);

            patch.Save(Console.Out);

            Console.WriteLine();
        }
    }
}

XDiff.Diff を呼び出した結果がイマイチな感じですね。これを実行すると XDT がコンソールに出力されます。

f:id:shiba-yan:20151225131700p:plain

生成された XDT をどのように使うかはお任せします。MSBuildTask などにしてしまうのも手だと思います。

おまけ:XDT を XML に反映させる

完全におまけですが、元になる XML と XDT を使って、差分が反映された XML を生成する方法も紹介しておきます。ちなみにこちらは Microsoft 純正のライブラリが提供されています。

XDT (XML Document Transform) released on codeplex.com - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

ライブラリは NuGet で公開されているので、FatAntelope と同じようにサクッと使えます。

基本的に XmlDocument を継承したクラスを使って XML に対して XDT を適用する形になります。こちらもサンプルコードを紹介しておきます。

using System;

using Microsoft.Web.XmlTransform;

namespace ConsoleApplication17
{
    class Program
    {
        static void Main(string[] args)
        {
            // 変換元の XML を読み込む
            var source = new XmlTransformableDocument();

            source.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.config");

            // XDT を読み込んで Apply で反映させる
            var xdt = new XmlTransformation(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.Release.config");

            xdt.Apply(source);

            source.Save(Console.Out);

            Console.WriteLine();
        }
    }
}

実行すると XDT の内容が反映された XML がコンソールに出力されます。

f:id:shiba-yan:20151225132210p:plain

これまでは XDT を適用するという一方向のみの対応でしたが、FatAntelope のリリースによって XML から XDT を作成するという、双方向の変換が可能になりました。

個人的には Web.config 変換を書きやすくするための Visual Studio 拡張を作りたいですね。

ASP.NET MVC 5 アプリケーションを .NET Framework 4.6 と C# 6.0 にアップデートしてみた

リリースされたばかりの .NET Framework 4.6 が Azure Web Apps で使えるようになったらしいので、動かしていた ASP.NET MVC 5 なアプリケーションを ASP.NET 4.6 と C# 6.0 にアップデートしてみました。

8 月中にリリースという話でしたが、思ったよりも早かったです。

ポータルの表示はまだ 4.5 なので注意が必要です。Azure 公式ブログもアップデートされています。

Update on .NET Framework 4.6 and Azure | Microsoft Azure Blog

Azure 界の抱かれたい男 No.1 こと @kosmosebi、通称ぶちぞうさんも既にブログで Web Apps の .NET 4.6 対応について書いてます。要チェックです。

Azure App Service に.NET Framework 4.6がロールアウトされました | ブチザッキ

現時点では RyuJIT は無効化されているのと、Web Apps は Windows Server 2012 で動いていて HTTP/2 は使えないので、Web Forms を使っている人ぐらいにしか大きな恩恵は無さそうです。

これだけだとアレなので、C# 6.0 を使う方法を紹介することにします。

ASP.NET と C# 6.0 の関係

C# 6.0 の機能はコンパイラが提供するので .NET Framework 4.6 とは別扱いです。このあたりの関係がどうにもわかりにくく感じたので、表にしてみました。

ASP.NET 4.5 ASP.NET 4.6 ASP.NET 5
cs ファイル △ (Roslyn が必要) ○ (MSBuild のパスを変更)
ASPX / Razor △ (CodeDOM Provider が必要) △ (CodeDOM Provider が必要)

.NET 4.6 をインストールすると Roslyn 込みの MSBuild がインストールされているので、C# のコードは MSBuild のパスを変えるだけでビルド可能になりますが、実際には CodeDOM Provider が Roslyn もインストールするので、特に設定しなくても使えるようになります。

結局のところ CodeDOM Provider をインストールすれば C# 6.0 が使えるようになるってことです。

例外的存在の ASP.NET 5 は Roslyn が組み込まれているので、デフォルトで C# 6.0 に対応しています。ASP.NET 5 に関しては RC1 が出てから本気を出すつもりでいます。

ASP.NET MVC 5 で C# 6.0 を有効にする

ASPX や Razor で C# 6.0 の機能を使うためには CodeDOM Provider をインストールする必要があります。

Announcing ASP.NET 4.6 and ASP.NET 5 beta 5 in Visual Studio 2015 Release - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

NuGet パッケージを調べてみると、このパッケージは .NET 4.6 だけではなく .NET 4.5 でも動くみたいなので、C# 6.0 の機能はすぐに使え始めることが出来そうです。

ちなみに Visual Studio 2015 を使って ASP.NET MVC プロジェクトを作成すると、最初から CodeDOM Provider がインストールされています。なので、特に何も考えることなく C# 6.0 を使って開発が行えます。

f:id:shiba-yan:20150815185648p:plain

一緒にインストールされている Microsoft.Net.Compiler は Roslyn の本体になります。

展開されたパッケージを見ると csc や vbc が存在しているのが確認できます。

f:id:shiba-yan:20150817001758p:plain

MSBuild 用の targets ファイルも含まれているので、ASP.NET アプリケーション以外に WebJobs などのコンソールアプリケーションでも C# 6.0 の機能が利用可能です。

話が逸れましたが、この CodeDOM Provider をインストールすると Web.config には system.codedom 要素が追加されているので、この設定を変えることで C# のバージョンを変更したりできます。

f:id:shiba-yan:20150815185735p:plain

実際に行ったことは CodeDOM Provider のインストールだけですが、これだけで GitHub などのソース管理からのデプロイも問題なくできるようになります。もちろん Web Deploy も利用可能です。

以前に紹介した aspnet_compiler.exe を使ってプリコンパイルを行う方法もそのまま使えました。

コンパイル時間が前よりも大幅に遅くなった気がしますが、C# 6.0 が使えるので我慢しようかと思います。

ASP.NET アプリケーションを Azure Web Apps にソース管理からデプロイする時にプリコンパイルさせる方法

Azure Web Apps に ASP.NET アプリケーションを GitHub などからデプロイする場合に、ビューのプリコンパイルを行いたいと思っていましたが、カスタムデプロイスクリプトを書くことで対応できたので紹介します。

やはり同じことを考える人は居たようですが、スクリプト例が見つかりませんでした。

aspnet_compiler.exe について色々書かれていましたが、現行の Web Apps にインストールされているもので問題なくプリコンパイル出来ました。

スクリプトの重要な部分というか、修正が必要な部分を順に解説していきます。

プリコンパイルを行う準備

最初にプリコンパイル前のビルド結果を一時的に格納させるためのディレクトリと、aspnet_compiler.exe のパスを用意しておきます。基本的に存在しないはずなので常に新しく作ります。

SET PRECOMPILE_TEMP=%temp%\___precompileTemp%random%
mkdir "%PRECOMPILE_TEMP%"

SET ASPNET_COMPILER_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe

これでランダムなディレクトリがデプロイごとに作成されます。

MSBuild の出力ディレクトリを変更

通常のデプロイスクリプトでは MSBuild の実行結果を DEPLOYMENT_TEMP に書き出すのですが、プリコンパイルするために今回作成した PRECOMPILE_TEMP に書き出すように変更します。

call :ExecuteCmd "%MSBUILD_PATH%" /p:_PackageTempDir="%PRECOMPILE_TEMP%" %SCM_BUILD_ARGS%

このコマンドは長いの不要な部分は削除しました。オリジナルのスクリプトから _PackageTempDir を指定してる部分を探して、そこを PRECOMPILE_TEMP に設定します。

aspnet_compiler でプリコンパイル

PRECOMPILE_TEMP に MSBuild でビルドされたアプリケーションのファイル一式が入っているので、それをソースにして DEPLOYMENT_TEMP にプリコンパイルした結果を出力します。

call :ExecuteCmd "%ASPNET_COMPILER_PATH%" -v / -p "%PRECOMPILE_TEMP%" -c "%DEPLOYMENT_TEMP%"

仮想ディレクトリを表す -v オプションの値は / のままで基本は良いと思います。DEPLOYMENT_TEMP に結果を出力すると、後は KuduSync が wwwroot に差分をコピーしてくれます。

プリコンパイルに使ったディレクトリを削除

追加したプリコンパイル用のディレクトリは Kudu によって自動的に削除されないので、スクリプトの最後の方で削除するようにしておきます。

IF EXIST "%PRECOMPILE_TEMP%" rd /s /q "%PRECOMPILE_TEMP%"

一応存在チェックを行ってから削除するようにします。

完成したスクリプト例

ここまで紹介したスクリプトを全て組み込んだ例を最後に張り付けておきます。

当然ですが、そのままコピペで使えるというわけではないです。これで処理の全体像を掴んでください。

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

SET PRECOMPILE_TEMP=%temp%\___precompileTemp%random%
mkdir "%PRECOMPILE_TEMP%"

SET ASPNET_COMPILER_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe

echo Handling .NET Web Application deployment.

:: 1. Restore NuGet packages
IF /I "AspNet.sln" NEQ "" (
  call :ExecuteCmd nuget restore "%DEPLOYMENT_SOURCE%\AspNet.sln"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 2. Build to the temporary path
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\AspNet\AspNet.csproj" /nologo /verbosity:m /t:Build /t:pipelinePreDeployCopyAllFilesToOneFolder /p:_PackageTempDir="%PRECOMPILE_TEMP%";AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
) ELSE (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\AspNet\AspNet.csproj" /nologo /verbosity:m /t:Build /p:AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
)

IF !ERRORLEVEL! NEQ 0 goto error

:: 3. Precompile Razor
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%ASPNET_COMPILER_PATH%" -v / -p "%PRECOMPILE_TEMP%" -c "%DEPLOYMENT_TEMP%"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 4. KuduSync
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 5. Clean Precompile Dir
IF EXIST "%PRECOMPILE_TEMP%" rd /s /q "%PRECOMPILE_TEMP%"

デプロイスクリプトのひな形は Azure CLI を使うと簡単に作成できます。

Azure Web Apps (Websites) - Custom Deployment Scripts Generator

実際にこのスクリプトを組み込んだアプリケーションを Web Apps へ GitHub からデプロイすると、プリコンパイルが行われていることが確認できます。

f:id:shiba-yan:20150726235314p:plain

ビューをプリコンパイルしておくことで、アプリケーションの起動パフォーマンスを改善出来ます。

特に Azure Web Apps はアクセスが無いとインスタンスが落とされる関係上、仮想マシンなどに置いてあるアプリケーションよりも初回起動が多くなる傾向にあるので、プリコンパイルは有効な手段になります。

インストール時に MachineKey を自動生成する NuGet パッケージを作った

大昔に紹介した MachineKey をオンラインで生成できるサイトが、いつの間にかドメインごと消滅していてとても不便だったので、代わりになるものとして MachineKey を自動的に生成し、Web.config に設定まで行う NuGet パッケージを作りました。

このパッケージをインストールすると、自動的に Web.config に machineKey 要素が追加されます。今のところ AES256 と HMACSHA256 で固定にしています。

MachineKey に関してもう少し知りたいという場合には、下の記事を参考にしてください。

1 つのアプリケーションを複数の仮想マシンで動作させる場合、問題となることが多いです。

例としてパッケージマネージャーコンソールから、MachineKeyGenerator をインストールしてみました。

f:id:shiba-yan:20150703130056p:plain

インストールするとテキストファイルも追加されますが、これは NuGet の仕様でパッケージに content か lib が含まれていないと install.ps1 / uninstall.ps1 が実行されないので、仕方なく入れています。

Web.config を開くと machineKey 要素が追加されているのが確認できます。本当は 1 行で追加されますが、分かりにくいので改行してあります。

f:id:shiba-yan:20150703130236p:plain

パッケージをアンインストールすると、MachineKey も同時に削除するようにしています。*1

既に MachineKey を設定済み、もしくは NuGet パッケージの復元を行った場合には処理をスキップするようにしています。一度設定した MachineKey が変わることはありません。

f:id:shiba-yan:20150703131127p:plain

実際に NuGet パッケージの復元を行いましたが、MachineKey は変わっていません。

PowerShell 力は相変わらずですが、少しコードを整理した後に GitHub にでも置いておこうかと思います。

追記

GitHub にスクリプト一式を置いておきました。build.cmd を叩けば nupkg が生成されます。

*1:この仕様は後で変更する可能性ありそう