しばやん雑記

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

Library Manager を使ってクライアントサイドのライブラリをシンプルに利用する

Visual Studio 2017 の 15.7 ぐらいで入ると言われていて入らなかった Library Manager が、こないだリリースされた 15.8 で入るようになりました。

これまで Visual Studio では NuGet や Bower が標準でサポートされてましたが、今回の Library Manager は任意のプロバイダーから必要なライブラリをインストールする機能です。

コメントには厳しい意見もありますが、個人的にはこういうシンプルなアプローチは好きです。私はとりあえず規模は関係なく何でも webpack という最近のフロントエンドの流れには違和感を持っています。

Library Manager はシンプルに特定のライブラリを管理し、インストールする機能しか持っていません。なので、ビルドやバンドルが必要な場合は素直に webpack とかを使うことになります。

ドキュメントには ASP.NET Core とありますが、実は ASP.NET でも使えます。

とりあえず README にある "Reasons to use LibMan" と "Reasons NOT to use LibMan" はしっかりと読んで理解した上で選択しましょう。webpack を置き換えるようなものではなく、そもそも目的は別です。

プロバイダーを追加できる仕組みになっているので、Issue には GitHub を扱えるようにしたいなど、面白そうなものもありました。GitHub の Release を扱えるようになると地味に良さそう。

npm でインストール出来るのは理解してますが、webpack などを使わない場合は node_modules からファイルをコピーしましょうみたいな記事が出てくる現状はおかしいのでは、と思ってます。

実際に使って試すことにします。Visual Studio 2017 の 15.8 がインストールされていれば、右クリックで表示されるメニューに項目が追加されています。

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

翻訳は非常にアレですが、GitHub にあるリソースでは正しい表現になっているので謎です。

追加を選ぶとダイアログが表示されて、そこで名前を入れるとインストールできます。デフォルトでは cdnjs が選択されていますが、unpkg やファイルシステムも選べます。

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

インストールするファイルを選べば、いい感じに wwwroot 以下にインストールしてくれます。

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

簡単ですね。NuGet や Bower を使ってインストールしていた時と同じように扱えます。

続いてよく使いそうな Bootstrap もインストールしてみます。

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

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

これもあっという間に完了しました。外部のメジャーな CDN に乗っかることで、アップデートがリリースされたらすぐに扱えるのは面白い仕組みだと思いました。

インストールしたライブラリの情報は libman.json に保存されています。右クリックするとメニューが表示されるので、ライブラリの復元やクリーニングも行えます。

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

ビルド時の復元を有効にしておけば、他の環境や CI サーバーに持って行った時でもビルド出来るようになるはずです。この辺りは NuGet と同じような動作になるみたいです。

色々と賛否はあると思いますが、こういったシンプルなパッケージマネージャがあっても良いと思います。

ACR Build を使って ASP.NET アプリケーションの Docker Image をビルドする

ついに App Service での対応によって Windows Containers を利用する機運が個人的に高まってきたので、前から気になっていた ACR Build を使って ASP.NET 向けのイメージをビルドしてみました。

VSTS や AppVeyor でも Docker Image を作ることは出来ますが、ACR Build の方が優れている部分がいくつかあります。自動で OS Image が更新されたときにビルドしてくれるとか最高ですね。

ドキュメントには Windows という文字が全く出てこないのですが、Azure CLI 2.0 を叩いていると OS を指定できるようになってるので、今は問題なく使えます。

とりあえずチュートリアルを参考にマニュアルでのビルドを試します。

これまで VSTS や AppVeyor ではタスクを定義したり、yml を書いたりしてイメージをビルドしてきましたが、ACR Build では Dockerfile を読んでビルドする機能しかないので、予め既存の Dockerfile を Multi-stage builds を使うように変更します。

軽く調べた感じでは ASP.NET の Multi-stage builds を見なかったので良い機会です。

ASP.NET アプリケーションを Multi-stage builds 化

先に Multi-stage builds を使うように既存の Dockerfile を修正します。公式ドキュメントがあるので、読みながら進めていけば大体問題ない感じです。それよりも .NET 固有の部分のがはまります。

要するにビルド用のイメージと実行用のイメージが必要になるのですが、ちょっと前にイメージが整理されて .NET Framework のビルドイメージが利用しやすくなっています。

今のバージョンはビルドは問題ないですが、一部のコンポーネントが正しくインストールされていないので動作しない機能もあります。特に Razor のプリコンパイル周りが動作しないので、Issue を上げています。

Dockerfile の中で nuget restore と MSBuild を実行すれば良いので、割とシンプルに書けます。

# escape=`

FROM microsoft/dotnet-framework:4.7.2-sdk as build

COPY . .

RUN nuget restore; `
    msbuild .\AspNetApplication\AspNetApplication.csproj /nologo /v:m /t:Build /p:DeployOnBuild=true /p:PublishProfile=FolderProfile


FROM microsoft/aspnet:4.7.2

COPY --from=build ./publish/ /inetpub/wwwroot

注意点としては .NET Framework のイメージを使うと、デフォルトのシェルが PowerShell になっていることぐらいです。なので、よく使われるように && でコマンドを繋ごうとするとエラーになります。

それ以外は特に書くことがありませんでした。プロジェクト全体は以下のリポジトリで公開しています。

AppVeyor でのビルド定義も同時に置いてあるので参考にしてください。

ACR Build でイメージを作成

ASP.NET アプリケーションを Multi-stage builds に変更したので、1 コマンドで ACR Build を使って Docker Image の作成が行えます。OS の指定だけ間違えないように注意です。

az acr build --registry aspnetsample --image aspnetapp:v1 --os Windows https://github.com/shibayan/appveyor-aspnet-docker.git

Cloud Shell でコマンドを実行しましたが、大体 7,8 分ぐらいでビルドが完了しました。最低限の Server Core イメージはキャッシュされているみたいなので、AppVeyor よりは多少高速です。

最後の方にイメージの依存関係を出力してくれます。ビルドタスクを組んでいると、イメージが更新されたタイミングでリビルドしてくれるらしいです。

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

プッシュされたイメージは Azure Portal から確認できます。

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

ちゃんとプラットフォームは Windows になっているので問題ないです。ACR Build を使うと面倒な権限周りの設定が不要なのも利点ですね。

Web App for Containers にデプロイ

最後に作成したイメージを Web App for Containers にデプロイします。といっても Azure Portal からドロップダウンで選択していくだけなので特に説明不要ですね。

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

保存して暫く待つとイメージが入れ替わって、ACR Build で作成したものになります。中々変わらない場合は再起動すれば大体はイメージを取り直してくれます。

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

ちゃんと ASP.NET MVC のアプリケーションが動作するところまで確認出来ました。

本来なら ACR Build でのイメージ作成が完了して、ACR にプッシュされたタイミングでステージングスロットにデプロイし、コンテナを起動したのちにスワップで本番リリースという流れを組みたいのですが、今のところは Web App for Containers 側の対応が足りないので残念です。

とはいえ、Windows Containers を使った開発から CI/CD、実行環境まで整った感があるので、後は Web App for Containers の進捗を待つという形になりそうです。

ASP.NET Identity から ASP.NET Core Identity へ移行してみた

ASP.NET MVC 5 から Core MVC 2.1 への移行作業を行っていますが、地味に Identity 周りの移行ではまった部分が多かったのでメモとして残します。作業としては DB のマイグレーションがメインです。

移行に関してはドキュメントがありますが、正直これは役に立たないですね。

喜ばしいことに ASP.NET Core Identity になってもパスワードのハッシュ化形式は変更されていないので、ASP.NET Identity のデータをそのままでログインすることは出来ます。

しかし、テーブルのスキーマが変更されているので、マイグレーションを行う必要があります。パッと調べた感じではマイグレーションの方法は公式に用意されていないみたいなので、手動で対応しないといけません。

Identity 設定を移行する

とりあえず ASP.NET Identity と ASP.NET Core Identity のプロジェクトを用意して、DB の差分を確認することから始めます。以下はテンプレートそのままのプロジェクトです。

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

ASP.NET Identity 側にある ApplicationUserManager.Create で設定している情報は、ASP.NET Core Identity では Startup クラスの中で IdentityOptions への設定として行います。

具体的にコードで例を出してみます。以下は ASP.NET Identity でのデフォルト設定です。

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
{
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    // ユーザー名の検証ロジックを設定します
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };

    // パスワードの検証ロジックを設定します
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };

    // ユーザー ロックアウトの既定値を設定します。
    manager.UserLockoutEnabledByDefault = true;
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;

    return manager;
}

上の設定を ASP.NET Core Identity 向けに変更したら、以下のようになります。

大体はデフォルト値のままで問題ないですが、何故か RequireUniqueEmail の値だけは異なっていました。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<IdentityOptions>(options =>
    {
        options.User.RequireUniqueEmail = true;

        options.Password.RequiredLength = 6;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = true;

        options.Lockout.AllowedForNewUsers = true;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
    });
}

カスタマイズしている場合には、この設定を合わせておかないと不整合が発生して、下手したらログイン出来なくなるパターンもあるので注意したいですね。

Identity DB を移行する

両方とも実行してユーザーを作成すると、いい感じに LocalDB にデータベースが作られます。

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

Identity と Core Identity でテーブル名は変更されていないのですが、新しくテーブルが追加されたりしているので、この差分を何とかしないと正しく実行できません。

なので Visual Studio の SQL Server オブジェクト エクスプローラーから差分をチェックします。

これはソース DB とターゲット DB を指定すると、分かりやすく差分を表示してくれる上に変更用スクリプトも生成してくれる優れものです。手動で SQL を書く手間が省けました。

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

__MigrationHistory というテーブルは無視して良いですが、何だかんだで全テーブル構造が異なっているという結果になりました。これは結構厳しいです。

当然ながら、この状態で Core Identity 側で Identity が作成した DB を使うようにするとエラーになります。

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

先ほどチェックした差分から変更用スクリプトを作成して実行すれば、データを保持したまま構造を変えてくれますが、今回のケースでは LockoutEndDateUtc というカラムが LockoutEnd にリネームされているので、その部分だけはデータを捨てるなりスクリプトを変更するなりで対応します。

今回はデータを捨てて対応したので、マイグレーション自体はあっさり終わりましたが、これでもログインはまだ行えません。試すとログインエラーになってしまいます。

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

DB のマイグレーションは問題ないのですが、最新の Core Identity は新しく追加された以下のカラムを利用してログイン処理を行うため、データを詰めてあげないと絶対にログインが失敗します。

  • NormalizedUserName
  • NormalizedEmail

このカラムには UserNameEmail を大文字化した値を入れる必要があります。

マイグレーションのスクリプト内で UPPER を行っても良いですし、私みたいに後からこの仕様変更に気が付いた場合は UPDATE を流せば良いです。

UPDATE [AspNetUsers] SET [NormalizedUserName] = UPPER([UserName]), [NormalizedEmail] = UPPER([Email])

この対応後、ようやく Core Identity でのログインが正しく行えるようになりました。長かったですね。

一応は公式で Identity と Core Identity の互換性レイヤーが提供されていますが、今回は完全なる Core Identity への移行が目的だったので使いませんでした。

しかし、このレイヤーのコードを読むことで互換性がない部分を理解できたので助かりました。

ASP.NET / IIS で X-Forwarded-Proto の値を正しく扱いたい

リバースプロキシやロードバランサーで SSL offload を行った場合には、後ろにいる Web サーバーにクライアントの IP などを伝えるために X-Forwarded-* を付けますが、ASP.NET は上手く扱ってくれません。

X-Forwarded-For に関しては ARR Helper を入れることで対応できますが、X-Forwarded-Proto は完全に未対応なので自力で何とかする必要があります。

ARR Helper がインストールされている場合には X-ARR-SSL というヘッダーを渡してあげれば HTTPS = on としてくれますが、非標準なヘッダーなので ARR と組み合わせた場合しか上手く動作しません。

HTTP モジュールで上書きする

ASP.NET の世界だけで扱えれば良いのであれば、専用のマネージ HTTP モジュールを用意して HTTPS / SERVER_PORT サーバー変数を書き換えてあげれば解決します。

適当に書いたコードですが、大体こんな感じになるのではないかと思います。

public class HttpForwardedModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += OnBeginRequest;
    }

    public void Dispose()
    {
    }

    private void OnBeginRequest(object sender, EventArgs e)
    {
        var app = (HttpApplication)sender;

        if (app.Context.Request.Headers["X-Forwarded-Proto"] == "https")
        {
            app.Context.Request.ServerVariables["HTTPS"] = "on";
            app.Context.Request.ServerVariables["SERVER_PORT"] = "443";
        }
    }
}

このモジュールを PreApplicationStartMethod を使って IIS に登録してあげれば有効になります。

[assembly: PreApplicationStartMethod(typeof(WebApplication17.ModuleInitializer), "Start")]

public class ModuleInitializer
{
    public static void Start()
    {
        HttpApplication.RegisterModule(typeof(HttpForwardedModule));
    }
}

実際のところは BeginRequest のタイミングで上書きの処理が出来ればよいので、Global.asax でも同様に動作します。これで実行して X-Forwarded-Proto 付きのリクエストを投げてあげれば、ASP.NET は HTTPS なリクエストだと判断します。

URL Rewrite で上書きする

ASP.NET の世界で閉じていればマネージ HTTP モジュールで上手くいきますが、ASP.NET 側で変更したサーバー変数は IIS 側には反映されないので、後ろに PHP や HttpPlatformHandler が居る場合には使えません。

要はサーバー変数を書き換えてあげれば良いので、URL Rewrite でも同じことが実現できます。

<system.webServer>
  <rewrite>
    <!-- unlock が必要なので少し面倒 -->
    <allowedServerVariables>
      <add name="HTTPS" />
      <add name="SERVER_PORT" />
    </allowedServerVariables>
    <rules>
      <rule name="HttpForwarded">
        <match url=".*" />
        <conditions>
          <add input="{HTTP_X_FORWARDED_PROTO}" pattern="https" />
        </conditions>
        <serverVariables>
          <set name="HTTPS" value="on" />
          <set name="SERVER_PORT" value="443" />
        </serverVariables>
      </rule>
    </rules>
  </rewrite>
</system.webServer>

やっていることは HTTP モジュールと同じです。allowedServerVariables はデフォルトではロックされているので、書き換えるには applicationHost.config を直接弄るか unlock してから追加する必要があります。

上の例ではアプリケーションの Web.config で書くことを想定してますが、globalRules に入れてマシン全体で使うことも出来るので、考えることが減って少し楽になるかも知れません。

リダイレクト時に https にならない問題

ARR の時には発生しないのですが、それ以外のリバースプロキシや LB を使っている場合には、URL Rewrite でリダイレクトを行うと http として返される問題があります。

何故 ARR の時には発生しないかというと、ARR にはレスポンスヘッダーを自動で書き換える機能があるからです。デフォルトで有効になっているので、ARR / ARR Helper / ASP.NET の場合は何も考えずに済みます。

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

しかし、それ以外の組み合わせになると一気に破綻するので、リダイレクトする際には https 付きの絶対 URL として URL Rewrite を書く必要があります。地味に面倒です。

Outbound Rule を使って一括で Location ヘッダーを書き換える方法も使えると思いますが、事故りやすいのであまりお勧めは出来ないと思ってます。

App Service を使う

例によって App Service を使う場合には ARR と ARR Helper という組み合わせになるので、いい感じに HTTPS = on としてくれます。リダイレクト時の書き換え処理も走るので、ちゃんと https としてクライアントには返ります。

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

App Service で実行する場合には、特に必要な作業はありません。お手軽ですね。ちなみに App Service 以外でも ARR と ARR Helper の組み合わせであれば、同じように動作します。

根本的な対応は IIS が X-Forwarded-Proto に対応するか、専用のネイティブ HTTP モジュールを書くしかないと考えてます。ARR Helper がもっと汎用的になって Proxy Helper とでもなれば最高なのですが。

.NET Framework / .NET Core 向け Docker Image のリポジトリが変更された話

多分 .NET Framework 4.7.2 と .NET Core 2.1 のリリースを狙ったタイミングだと思ってるのですが、この 2 つを利用するための公式 Docker Image のリポジトリが変更されています。

地味に同じようなのにイメージが異なっていた .NET 系の Docker Image が統合されて、シンプルなタグで使えるようになっているので便利です。古い方はもう更新されないので注意。

.NET Framework 向け

これまでは microsoft/dotnet-frameworkmicrosoft/dotnet-framework-build で用途によってイメージ自体が分けられていましたが、現在は microsoft/dotnet-framework 側に統合されています。

  • 4.7.2-sdk
  • 4.7.2-runtime

SDK のイメージには Visual Studio Build Tools がインストールされているので、MSBuild を使ってアプリケーションをコンテナ内でビルドできるようになっています。*1

基本的には最新の .NET バージョンを対象にしているでしょうし、4.7.2 を使っておけばよいと思いますが、上のタグは LTSC 2016 の Server Core イメージが使われているのでサイズが大きいです。

必要に応じて 1709 や 1803 のイメージを使うと半分ぐらいで済むようになります。タグの詳細は Docker Hub を参照すればすぐにわかるはずです。

https://hub.docker.com/r/microsoft/dotnet-framework/

とはいえ、現時点で Hyper-V Containers を使った CI SaaS は無いみたいなので、大人しく LTSC 2016 を使うことになるのではないかと。実際に AWS CodeBuild がそうでした。

ちなみに ASP.NET を使うためのイメージはこれまでと同じ microsoft/aspnet です。

.NET Core 向け

.NET Framework 向けは Windows しかなかったので分かりやすいですが、.NET Core は複数のディストリ向けに公開されてるだけじゃなく、Nano Server のイメージもあるのでちょっと数が多いです。

しかし、ちょっと前から multi-arch なイメージに対応しているので、基本は以下の 3 つだけ知っておけば良いです。後は Docker が勝手に Ubuntu と Nano Server で切り替えてくれます。

  • 2.1-sdk
  • 2.1-aspnetcore-runtime
  • 2.1-runtime

.NET Framework 向けとは異なり、ASP.NET Core 向けのイメージも microsoft/dotnet に統合されるようになったので、今後は microsoft/aspnetcoremicrosoft/aspnetcore-build は更新されません。

例を挙げると Docker を使った CI SaaS では microsoft/dotnet:2.1-sdk を使い、ASP.NET Core を動かすための Dockerfile では microsoft/dotnet:2.1-aspnetcore-runtime を使えば良いです。

特に理由がなければシンプルなタグを使っておけば良いですが、.NET Core に関しては Ubuntu のバージョン違いだけで数種類出ているうえに、Nano Server も 1709 / 1803 / LTSC 2016 が用意されています。

https://hub.docker.com/r/microsoft/dotnet/

更に .NET Core 2.1 からは Alpine Linux のイメージと ARM32 向けイメージも追加されているので、間違わないように注意したいところです。ちなみに Alpine は microsoft/dotnet:2.1-sdk-alpine といった形で指定できます。基本的に OS 名は最後につきます。

Alpine Linux のイメージは今後主流になっていくのではないかと思ってます。

ASP.NET アプリケーションを CodePipeline と CodeBuild を使って Elastic Beanstalk にデプロイする

AWS CodeBuild が Windows にも対応してやりたいことは、やはりビルドしたアプリケーションを何処かにデプロイすることでしょう。少なくとも私はそうだったので、CodePipeline を組み合わせて試しました。

CodeBuild の Windows 対応はこないだ書いたので説明はしません。

ASP.NET アプリケーションをデプロイ用にビルドする際は、やはりビューをプリコンパイルしておきたくなるので、発行プロファイルを作成して管理することにしました。

buildspec.yml で発行プロファイルを使うように書けば、勝手にプリコンパイルされて便利です。

version: 0.2

phases:
  pre_build:
    commands:
      - nuget restore
  build:
    commands:
      - msbuild WebApplication12.sln /p:Configuration=Release /m /v:m /t:Package /p:PublishProfile=CustomProfile
artifacts:
  type: zip
  files:
    - artifacts/*
  discard-paths: yes

今回は artifacts の設定を変更して、指定したディレクトリ以下をアップロードするようにしました。前の方法では CodePipeline を組み合わせた時に 2 重に zip されてしまい、Elastic Beanstalk へのデプロイが上手くいかなかったからです。

artifacts 以下には予め Elastic Beanstalk にデプロイするためのマニフェストを入れておきました。ASP.NET Core 用みたいに紹介されてますが、MSDeploy 向けにも使えます。

{
  "manifestVersion": 1,
  "deployments": {
    "msDeploy": [
      {
        "name": "Web app",
        "parameters": {
          "appBundle": "WebApplication12.zip",
          "iisPath": "/",
          "iisWebSite": "Default Web Site"
        }
      }
    ]
  }
}

実際のリポジトリ内では以下のように配置してあります。msbuild がビルド結果の zip をこのディレクトリに保存してくれるので、良い感じのパッケージが作れるというからくりです。

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

コードの準備は出来たので CodePipeline を新しく作成します。

特に説明することはなく、GitHub からソースを取得して、CodeBuild を使ってアプリケーションをビルド、そして最後は Elastic Beanstalk にデプロイという、シンプルなパイプラインです。

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

パイプラインを実行すると、CodeBuild で ASP.NET アプリケーションがビルドされて、Elastic Beanstalk にアーティファクトがデプロイされます。ビルドログもおまけで載せます。

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

そしてしばらくすると Elastic Beanstalk へのデプロイが完了するので、無事に ASP.NET アプリケーションが見れるようになります。

ちょっとアーティファクト周りの扱いが嫌な挙動でしたが、パイプライン自体は簡単に作れました。

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

デプロイ先としては Windows の場合は Elastic Beanstalk ぐらいしか、簡単に扱えるものが無さそうなのが少し残念ですね。CodeDeploy を組み合わせれば、もうちょっと自由度は上がりそうですが EC2 の管理はあまりしたくないですね。

EKS が Windows に対応したり、CodeBuild が Windows でも Docker Image が作れるようになれば、もうちょっと楽しくなりそうだと感じました。

Visual Studio 2017 に .NET Framework 4.7.2 SDK をインストールする

通常は Visual Studio Installer から .NET Framework の SDK はインストール出来るはずですが、何故か 4.7.2 はまだ落ちてこないので手動でインストールしようとしましたが、何故か入りませんでした。

地味にはまったのでメモとして残しておきます。

.NET に関しては以下のページから .NET Core / Framework を含め SDK や Runtime をダウンロード出来ますが、少なくとも日本語でダウンロードリンクを踏むと言語パックのみ落ちてきます。これが罠でした。

ダウンロードリンクからは Developer Pack が落ちてくるように読み取れますが、実際には言語パックなので何回インストールしても当然ながら 4.7.2 向け開発が出来るようになりません。

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

本物の Developer Pack は以下のページにあるリンクから落としておけば良いです。こっちをインストール後に言語パックをインストールすれば、正しい順番となります。

What's new in the .NET Framework | Microsoft Docs

インストールすれば Visual Studio から .NET Framework 4.7.2 が選べるようになります。

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

地味に 4.7.2 はパフォーマンス周りの取り組みや、ASP.NET 周りでは DI に対応したり SameSite Cookie 対応など機能が多いので、出来れば早めに移行したいと思っています。

ASP.NET の DI はサンプルコードだと動かなかったので、ちょっと検証後にまとめようかと思います。

SignalR のメトリクスを Application Insights に直接送信する

まともに触ったのは数年振りという感じがしますが、前々から SignalR のメトリクスはパフォーマンスカウンターに書き込まれるようになっていて、App Service などから扱いにくいなと思ってました。

最近は ARM 経由で App Service のパフォーマンスカウンターが取れるようになってますが、一部の値だけなので扱いにくいことには変わりないです。

特に SignalR のカウンターはインストールが必要なので、この時点で App Service では無理です。

そもそも Application Insights を使っているなら、パフォーマンスカウンターを経由せずにテレメトリを送信すればよいのではと思ったので、Telemetry Module を書いてみました。

インストールして、ApplicationInsights.config に TelemetryModule を追加すれば良いです。

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  <TelemetryModules>
    <Add Type="SignalR.AppInsights.SignalRPerformanceCollectorModule, SignalR.AppInsights" />
  </TelemetryModules>
</ApplicationInsights>

アプリケーションをデプロイすれば、SignalR のメトリクスが Application Insights で確認できるようになります。今は 60 秒間隔でカスタムのパフォーマンスカウンターとして送信しています。

Application Insights で確認できるようになるまで少しかかりますが、しばらくすると選択肢に SignalR のパフォーマンスカウンターが出てくるようになります。

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

実際に Web App を 3 インスタンス用意して、Backplane として Redis を設定したアプリケーションに対して、トランスポートを切り替えて 200 接続ぐらい行った時の値です。

接続毎にメッセージを 100ms 間隔で送信しているので、秒間のメッセージ数がかなり多いことが把握できます。Backplane のメトリクスはパフォーマンスカウンターがないと把握出来ない値でしたが、Application Insights に送信すれば一目で分かりますね。

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

今は SignalR のパフォーマンスカウンターの値を全て送信していますが、この辺り設定可能にしておいても良いのかなと少し思っています。

余力があれば config から変更可能にしようかなという気分です。

ASP.NET でも Cookie Policy への対応を行えるか試した

ASP.NET Core 2.1 で入った Cookie Policy が面白かったのと、ASP.NET というか IIS でも HttpModule を使うことで対応出来そうだったので実装して試すことにしました。

行うことは非常に単純なので、HttpModule を書いたことがあれば大体は想像がつくはずです。

  • トラッキングを許可するクッキーがあるか確認
  • ある場合
    • 何もしない
  • ない場合
    • 確認のメッセージを出す
    • クライアントにクッキーを送信しない

別に Global.asax に書いても良いですが、分けた方が好きなので HttpModule として分けました。ただし DynamicModuleHelper を使って動的に追加するようにしています。

何はともあれ、今回でっち上げたコードは以下のような感じです。難しいことはしていません。

using System;
using System.Web;

using Microsoft.Web.Infrastructure.DynamicModuleHelper;

[assembly: PreApplicationStartMethod(typeof(WebApplication4.CookiePolicyModule), "Register")]

namespace WebApplication4
{
    public class CookiePolicyModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.BeginRequest += OnBeginRequest;
            context.PreSendRequestHeaders += OnPreSendRequestHeaders;
        }

        private void OnBeginRequest(object sender, EventArgs e)
        {
            var application = (HttpApplication)sender;

            var cookie = application.Request.Cookies[CookiePolicy.CookieName];

            application.Context.Items[CookiePolicy.CanTrackKey] = cookie?.Value == CookiePolicy.CookieValue;
        }

        private void OnPreSendRequestHeaders(object sender, EventArgs e)
        {
            var application = (HttpApplication)sender;

            if (CookiePolicy.CanTrack)
            {
                return;
            }

            // Clear all cookies
            application.Response.Cookies.Clear();
        }

        public void Dispose()
        {
        }

        public static void Register()
        {
            DynamicModuleUtility.RegisterModule(typeof(CookiePolicyModule));
        }
    }
}
using System.Web;

namespace WebApplication4
{
    public static class CookiePolicy
    {
        public static string CookieName { get; set; } = "ASP.NET_CookiePolicy";

        internal const string CookieValue = "yes";

        public const string CanTrackKey = "CanTrack";

        public static bool CanTrack => (bool?)HttpContext.Current.Items[CanTrackKey] ?? false;

        public static string CreateConsentCookie()
        {
            return $"{CookieName}={CookieValue}; path=/";
        }
    }
}

難しいことはしていないですが、Web.config に HttpModule の追加設定を書くのが面倒だったので、DynamicModuleUtility を使って実行時に HttpModule を追加するようにしました。こっちの方が楽ですが使っていいのかわかってません。

とりあえず Web Forms で画面を作ってブラウザで実行してみます。初回なのでクッキーはありません。

f:id:shiba-yan:20180307004527p:plain:w400

Page_Load でセッションに書き込むようにしたので、通常であればセッション用のクッキーが出力されるはずですが、ブラウザで確認しても存在しないです。ちゃんと HttpModule で削除されています。

f:id:shiba-yan:20180307004641p:plain:w550

accept cookie を押して、専用のクッキーを書き込むとセッションクッキーも書き込まれるようになりました。ちなみに専用のクッキーは JavaScript で書き込む必要があるので少し注意が必要ですね。

f:id:shiba-yan:20180307004653p:plain:w550

ASP.NET Core と同じように ASP.NET / IIS でも実現出来ました。試していませんが IIS の HttpModule として実装しているので、原理的には PHP などのアプリケーションでも同様に動作するはずです。

こういうプライバシーの問題は対応が難しいですね。

ASP.NET Web Forms のコンパイル周りの挙動を調べる

最近は流石に Web Forms を新規開発で使うことは少なくなってきていると信じたいところですが、基本的に Web Forms から MVC への移行はフルスクラッチになってしまうという現状もあり、どうにか Web Forms のままアーキテクチャを改善したいというケースは多いのではないでしょうか。

ASP.NET アプリケーションのモダナイゼーションを行っている立場としても、ここで Web Forms についてさらに深いところまで知っておくべきだと思ったので、まずは基本的な部分を確認しておきました。

コードビハインドと実体のクラスは別

今更言うまでもない感じもしますが Inherits で指定されている通り、コードビハインドで書いたクラスと実際にコンパイルされた ASPX のページクラスは別物です。

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

あくまでも継承元のクラスを書いているにすぎません。ASPX の実体はランタイムによって実行時にコンパイルが行われるか、予め aspnet_compiler を使ってプリコンパイルすることで生成されます。

コードビハインドは必須ではない

Visual Studio で Web Forms を追加するとコードビハインドがセットになりますが、必要ない場合は削除することが出来ます。この場合は自動的に Page クラスから派生されることになります。

Web Pages や Razor Pages のようにインラインでコードを書くことも出来ますが、基本的に書くべきではありません。あくまでも ASPX を純粋なテンプレートエンジンとして使う場合に限った方が良いでしょう。

<%@ Page Language="C#" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <div>
        <%: DateTime.Now %>
    </div>
</body>
</html>

懐かしの ASP.NET MVC 3 より前で良く使われていた書き方です。

ちなみに Inherits を追加すれば、カスタムクラスから派生させることも出来るので、応用範囲としては広いと思います。必要ないクラスを作ることはありません。

ASPX 単位では C# / VB の共存が可能

普通はやらないと思いますが、ASP.NET はビューがアプリケーションとは別にコンパイルが行われるので、C# プロジェクトであっても VB で書かれた ASPX を追加できます。ただしコードビハインドは無理です。

Language に VB を指定して、Visual Studio でファイルを開き直せば VB のコードが書けるようになります。

<%@ Page Language="vb" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <%: Me.Context.Request.RawUrl %>
        </div>
    </form>
</body>
</html>

ちゃんと ASPX は VB ファイルとしてコンパイルされていることが確認できます。

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

中身を確認しておきましたが、ちゃんと VB のコードになっています。

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

C# プロジェクトで VB を使って ASPX を書くメリットは無いと思いますが、逆に VB プロジェクトでは C# を使って ASPX を書けた方が良い場合はありそうです。

余談 : IHttpHandler について

ASP.NET の仕様では IHttpHandler がプリミティブな処理単位となっています。ASHX で書くジェネリック HTTP ハンドラーと同じで、ASPX もコンパイルされた結果は IHttpHandler を実装したクラスになります。

ちなみにコンパイルされた結果のクラスは BuildManager.GetCompiledType を呼び出すと取得できます。

// コンパイルされた About.aspx の型を取得
var type = BuildManager.GetCompiledType("~/About.aspx");

// About.aspx の実体をインスタンス化
var page = (Page)Activator.CreateInstance(type);

そして HttpContext には RemapHandler という IHttpHandler を入れ替えるメソッドがあります。

HttpContext.RemapHandler(IHttpHandler) Method (System.Web) | Microsoft Docs

ASP.NET Routing は IHttpModule と RemapHandler を組み合わせて、ASP.NET MVC と同じようなルーティングを実装しています。ASP.NET MVC も BuildManager 周りを上手く抽象化してテンプレートエンジンとして実装しています。

最終的に IHttpHandler にしてしまえば、後は ASP.NET ランタイムに食わせて処理が可能ということです。