Quantcast
Channel: @jsakamoto
Viewing all 146 articles
Browse latest View live

EntityFramework Core の ver.3.0 から、"所有されているエンティティ型"に関したテーブル構造が変わってしまった

$
0
0
EFCore の "所有されているエンティティ型"
.NET プログラミングにおけるデータベースアクセスライブラリの定番である、EntityFrame Core (以下、EFCore)。


EFCore を用いたデータベースを読み書きするプログラム開発において用いられる技法のひとつとして、"Code First" と呼ばれる作り方がある。


すなわち、EFCore を基盤とした C# ソースコードとしてデータベースのテーブル構造を記述してプログラムをビルドし、そのプログラムの実行によって、データベースとその構造を自動生成するというものだ。


自分は趣味/仕事の双方で、この "Code First" スタイルのプログラム開発を行なっている。


さてそのような "Code First" スタイルでの EFCore プログラミングにおいて、"所有されているエンティティ型" という機能が EFCore に用意されている。


これは何か、ちょっとサンプルコードを示してみよう。


まず、データベース上で People テーブルにマップすることにする、Person クラスというのを C# ソースコードとして記述する。
public class Person {
  
  public int Id { get; set; }
  
  [Required]
  public string Name {get; set; }


  public double Height { get; set; }


  public double Weight { get; set; }
}

ところで、このプログラム開発において、Height と Weight の 2つのプロパティのペアが、この Person クラスに限らず他の多数のクラス (=テーブル) でも頻出するとしよう。


その場合、Height と Weight の 2つのプロパティを持つ独立したクラスを "所有されているエンティティ型" として作成し (ここでは Metric クラスとする) 各所で再利用できるようになる。
public class Person {
  
  public int Id { get; set; }
  
  [Required]
  public string Name {get; set; }


  public Metric Metric { get; set; }
}


[Owned] // <--  "所有されているエンティティ型" であることを記す
public class Metric {
  
  public double Height { get; set; }


  public double Weight { get; set; }
}

こうして実装したプログラムを実行し、データベースを自動生成されると、次のような People テーブルを作成してくれるのである。


d0079457_19544646.png



"所有されているエンティティ型" を使った実装について、このサンプルコードだと今ひとつ効能がわかりにくいかもしれない。


しかし、同じセンサーデータ構成をセンサーの数だけ同一レコード上に列として並べたい場合など、効率良く且つミスなく実装できるので便利なのである。


なお、"所有されているエンティティ型" は他にもいくつかその自動生成されるデータベース構造にバリエーションがあるのだが、自分はもっぱら上記サンプルコードで示したようなパターンで利用している。


その他、"所有されているエンティティ型" について詳しくは、公式ドキュメントを参照されたし。


EFCore の v2.x と v.3.0 とで生成されるテーブル構造が異なる!
...と、まぁ、このような "所有されているエンティティ型" を活用したプログラム開発を、EFCore の ver.2.1~2.2 ベースで行なってきた。


そしてあくる日。 


EFCore の次バージョン、ver.3.0 がリリースされたので、手持ちのプロジェクトの EFCore のバージョンを、ver.2.x から ver.3.0 に更新した。


そして動作を確認していたところ、"所有されているエンティティ型" に関して、自動生成されるテーブル構造が変わっていることに気がついた。


まずおさらいだが、EFCore v2.x においては、最初に示したサンプルコードだと、Height および Weight は、C# コード上は double 型なので、自動生成されるデータベース上の型は float NOT NULL というように非 null 許容となっている。
(以下にデータベース構造の図を再掲する)


d0079457_19544646.png


これは少なくとも自分にとっては直感と相違ない振る舞いである。
もしデータベース上の型を null 許容としたければ、C# コード側を double? と記述することになる。


ところが、である。


EFCore v3.0 だと、同じコードからデータベースを自動生成するようにすると、なんと、C# コード上、"所有されているエンティティ型" に含まれる double 型が、データベース上では float NULL というように null 許容になってしまったのだ(下図)。


d0079457_19544610.png



これが、"所有されているエンティティ型" に含まれるプロパティではなく、テーブルにマップされるクラス直属のプロパティであれば、これまでと変わらない振る舞いとなる。
すなわち、C# コードの double 型はデータベース上の float NOT NULL に、double? 型は float NULL となるようにデータベース構造が生成される。


この "所有されているエンティティ型" に含まれるプロパティが null 許容になってしまう振る舞いは、現時点での最新バージョン (但し安定版ではない) である v.3.1 Preview 3 や、安定版の最新パッチバージョン v.3.0.1 でも同じであった。
破壊的変更があるのは仕方がないが...
まぁ、EFCore も v.2 から v3 へとメジャーバージョンがあがるくらいなので、破壊的変更があるのは仕方がないとは思う。


また、もう既にこの振る舞いで ver.3.0 として安定版リリースされていることもあるだろうから、今更 ver.2.x と同じ動作に戻すことも無理があろう。


しかしである。


ちょっと問題視してる点、というかホントに困った点は、いろいろ調べた限りでは、どうも、"所有されているエンティティ型" に含まれるプロパティを、明示的に非 null 許容としてデータベース生成する方法がなさそうなのだ。


あちこちに Required 属性をつけたり、OnModelCreating() メソッド内で fluent API であれこれモデルの指定を行なっても、 "所有されているエンティティ型" に含まれるプロパティが null 許容になってしまう動作は変えられなかった。


唯一の回避策としては、データベースマイグレーションの仕組みを使ってデータベース自動生成の処理を C# ソースファイルとして生成し、その自動生成されたマイグレーション用の C# ソースファイルを手で編集して、nullable: true になってる箇所を nullable: false に書き直してくれ、というのである (出典は下記)。


d0079457_19544615.png



これはちょっとあんまりではないかと個人的には思われるのだが、どうだろうか。


EFCore は Apache License 2.0 のオープンソース製品であり、リポジトリは GitHub にある。


そして EFCore の GitHub リポジトリ上には本件に関する Issue があがっており(下記 #12100)、いちおう EFCore の開発チームはこの問題(?)を承知しているようではある。


また、必要な人はこの Issue (#12100) に投票しよう、との呼びかけもある。


d0079457_19544652.png


とはいえ、1,000 を超える Issue が開かれたままであり、また、この問題についての Issue についての Vote (投票) 数も目立って多いとはいえず、近い将来の内にも何かしらの対応がなされるのかどうかはあまり期待が持てないでいる。
最後に
この問題(?)を再現するソースコード一式は下記にて公開している。


EFCore の ver.3.x は、ver.2.x に比べていろいろ魅力的な改善が施されており、早く移行したいところではある。

しかし自分がたずさわっているプロダクトではこの件がネックになって、未だに EFCore ver.2.x を使用したまま足踏みしている。


同じ悩みを抱えている方々はいないのだろうか、あるいは、どのように回避されているのであろうか、気になる次第。

Visual Studio 2019や dotnet CLI での "発行"処理時に、発行したファイルを Zip ファイルにまとめる方法

$
0
0
背景
Visual Studio や dotnet CLI を使っての、C# による ASP.NET Core アプリ開発における話。


一般的な ASP.NET Core Web アプリケーションは、インターネット上に配置して利用してもらうものだ。


しかし今回の案件はちょっと変わってて、開発した Web アプリを、クライアント台数が数十台程度の閉鎖系のローカルエリアネットワーク内で稼働させることとなった。
(ちなみに、開発した ASP.NET Core Web アプリを稼働させるサーバーPCは Windows OS が採用された)


そこで、この案件で開発する ASP.NET Core Web アプリは Windows サービス として開発。
サーバーPC上への最初のセットアップ (Windows サービスとしての登録) を済ませた以後は、xcopy 配置にて簡易にバージョンアップできるようにすることとした。


さてこのような形態だと、開発した ASP.NET Core Web アプリは、Visual Studio 上で開発中ならば「フォルダへ発行」のプロファイルで、ファイルシステム上に成果物を吐き出すようにすることもあろうかと思う。


但し、そうして吐き出されるファイルはかなりの本数に上る。
今回の案件だと、発行された成果物のファイル本数は 184 ファイルに達した。


このように発行された成果物のファイル群を、そのままやりとりするのは無茶がある。
通常は Zip などのアーカイブファイル x 1つにまとめて、そのアーカイブファイルをやりとるすることであろう。


ということで本案件でも実際、発行された成果物のファイル群は、単一の Zip ファイルに固めて持ち運ぶこととなった。


プロジェクトファイルにちょっと書き足して、自動で Zip 化!
さて、MSBuild 15.8 からは、「指定されたフォルダ内のファイルを、ディレクトリ構造もそのままに、サブフォルダのファイルも含めてまるっと Zip アーカイブファイルにまとめてしまう」という、"ZipDirectory" タスクが追加されたそうだ。




(そう、それまでは、MSBuild 標準では、Zip アーカイブにまとめる機能は備えられておらず、MSBuild 内で Zip アーカイブを作成するためには MSBuild 拡張を導入する必要があった。)



そこで、今回案件では、Visual Studio から「発行」を行なったら、 (発行された成果物のファイル群を収録した) Zip アーカイブファイルを自動で生成するよう、MSBuild スクリプトを仕込むことにしてみた。


MSBuild スクリプトへの仕込みはあっけない。


対象の MSBuild スクリプトファイル = プロジェクトファイル (.csproj) 内に、下記内容のような <Target> ノードを追記するだけだ。


<!-- これは .csproj ファイル -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <!-- ↓この Target 要素を記述 -->
  <Target Name="MakeZipPackage" 
          AfterTargets="AfterPublish">
    <MakeDir Directories="$(ProjectDir)..\_release" />
    <ZipDirectory
      SourceDirectory="$(ProjectDir)$(publishUrl)"
      DestinationFile="$(ProjectDir)..\_release\$(AssemblyName)-v.$(Version).zip"
      Overwrite="true" />
  </Target>
  ...
</Project>
もう少し解説を加えよう。


まず Target 要素の Name 属性を "MakeZipPackage" としているが、このビルドターゲットの名前は重要ではない (衝突しなければ/他のターゲットから依存されていなければ、名前は何でも良い)。


それよりも肝心なのは、AfterTargets 属性で指定している、"AfterPublish" というターゲット名。


すなわちこの指定により、発行が行なわれたあとのタイミングで、上記の「Zip アーカイブを作成する」ターゲットが実行されるようになる仕掛けだ。


あとは MakeDir タスクで、作成する Zip アーカイブファイルの配置先フォルダを作成。
そして最後に満を持して ZipDirectory タスクによって、発行先フォルダの中身を Zip アーカイブファイルにまとめて完了だ。


発行先フォルダのパスは、"$(ProjectDir)$(publishUrl)" で参照できるので、これを "ZipDirectory" タスクの "SourceDirectory" パラメータ (Zip 化対象のフォルダの指定) に渡している。


作成する Zip アーカイブファイルの配置先フォルダは概ねどこでも構わない。
ただしプロジェクトフォルダの直下にするのだけは、ややこしいことになりかねないので止めておくのが良さそうだ。
出力先フォルダ (~/bin) より下のサブフォルダか、あるいはプロジェクトフォルダの兄弟フォルダがいいのではないかと思われる。


上記例では、プロジェクトフォルダの兄弟フォルダのレベルで、"_release" というサブフォルダを作成して、そこを Zip アーカイブファイルの配置先としている。


作成する Zip アーカイブファイルのファイル名も、これまた何でも良い。


上記の例では、ビルドで生成されるアセンブリ (.dll) の名前を "$(AssemblyName)" で参照できるので、、これを Zip アーカイブファイルのファイル名としつつ、さらにパッケージバージョンの指定が "$(Version)" で参照できるので、これを利用して、作成する Zip アーカイブファイルのファイル名の後半にバージョン番号を含めるようにしてみた。


上記例のようなビルドターゲットの MSBuild スクリプトをプロジェクトファイル内に仕込むことで、発行された成果物のファイル群を単一の Zip アーカイブファイルにまとめることができるようになった。


dotnet CLI であれば、下記のように、Release 構成でのビルドを指定しつつ、発行を実行することで、発行成果物ファイル群を収録した Zip アーカイブファイルが生成される。


> dotnet publish -c:Release
複数ある発行プロファイルの内ひとつで Zip 化したい場合
もしも、複数の発行プロファイルを使い分けつつ、そのうちのひとつの発行プロファイルで発行する場合のみ、Zip アーカイブファイルを作成したい場合はどうするか。


いくつかやり方はあるのだが、ひとつの方法として、発行プロファイル内に Zip アーカイブファイルを作成する MSBuild スクリプトを仕込むという方法がある。


先の例で記述した <Target Name="MakeZipPackage" ...>...</Target> 要素をいったん .csproj 内からは削除し、代わりに、対象の発行プロファイル、すなわち .pubxml ファイル内に記述するのだ。


<!-- これは Zip 作成を伴いたい発行プロファイル (.pubxml) -->
<Project ...>
  ...
  <Target Name="MakeZipPackage" AfterTargets="AfterPublish">
    <!-- 中身は先の例と同じ -->
    ...
  </Target>
  ...
</Project>
発行プロファイル (.pubxml) を指定しての発行の場合は、発行対象のプロジェクトファイル (.csproj) と発行プロファイル (.pubxml) を合成しての MSBuild 実行に等しい。


なので、ある特定の発行プロファイルでのみ何かさせたい場合 (今回の例では Zip アーカイブファイルの作成を行ないたい) は、発行プロファイル (.pubxml) 側に MSBuild スクリプトを仕込むことで実現可能である。


ちなみに、dotnet CLI で、発行プロファイル (.pubxml) を指定しての発行を行なうには、下記のように、"PublishProfile" MSBuild プロパティに、発行プロファイル (.pubxml) へのプロジェクトからの相対パスを設定して実行すればよい。


> dotnet publish -c:Release -p:PublishProfile={ここに.pubxmlへの相対パス}
複数ある発行プロファイルの内いくつかで Zip 化したい場合
もうちょっと込み入ったケースでは、複数の発行プロファイルがあって、しかしそのうちのひとつだけではなく、いくつかの発行プロファイルで Zip アーカイブファイルを作成したい、というケースも想定される。


ではということで、Zip アーカイブファイルの作成スクリプトを、複数の .pubxml にベタで書いても、まぁ、それで実現はできる。


しかし、できるのではあるが、"DRY 原則" というか、同じスクリプトがコピペで複数のファイルに散在するのは気持ち悪い。
書いていた Zip 作成のスクリプトに手直しが必要になったら、すべての発行プロファイル (.pubxml) で修正作業をするのもどうかと思うわけである。


このようなケースに対処するにはどうするか。
これまたいくつかやり方があるが、一例として、Zip 化する処理はプロジェクトファイル (.csproj) に記載しつつ、「発行後に Zip アーカイブファイルの作成を行なうかどうか」を MSBuild スクリプト内のプロパティでフラグ表現する方法がある。


まず、Zip アーカイブファイルを作成するターゲットは最初の紹介のとおり、(発行プロファイル (.pubxml) ではなく) プロジェクトファイル (.csproj) 内に記述しておく。


但し最初の紹介例に加えて、"Condition" 属性を追加して、「発行後に Zip アーカイブファイルの作成を行なう」フラグが立てられていたときだけ、Zip アーカイブファイル作成ターゲットを有効とするよう書き加える。


なお、「発行後に Zip アーカイブファイルの作成を行なう」フラグを表現する MSBuild プロパティの名前は何でもよく、スクリプトの実装者が決めればよい。
ここでは "MakeZipAfterPublish" というプロパティ名とすることとし、このプロパティに "true" と設定されていた場合にのみ Zip アーカイブファイルを作成する仕様としよう。
その場合、.csproj 内のターゲットの記述は下記要領となる。
<!-- これは .csproj ファイル -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <Target Name="MakeZipPackage" 
    AfterTargets="AfterPublish"
    Condition="'$(MakeZipAfterPublish)' == 'true'"><!-- ←これを追加 -->
    <!-- 中身は先の例と同じ -->
    ...
  </Target>
  ...
</Project>
ここまでの段階ではまだ、発行しても、MSBuild プロパティ ”MakeZipAfterPublish" の値は "true" ではない (既定だと空文字になる) ため、Zip アーカイブファイルは作成されない。


最後の仕上げとして、発行後に Zip アーカイブファイル作成を行ないたい各発行プロファイル (.pubxml) 中で、MSBuild プロパティ "MakeZipAfterPublish" の値に "true" を設定してやればよい。
<!-- これは Zip 作成を伴いたい発行プロファイル (.pubxml) -->
<Project ...>
  ...
  <PropertyGroup>
    <!-- どこでもいいので MakeZipAfterPublish プロパティを "true" で定義 -->
    <MakeZipAfterPublish>true</MakeZipAfterPublish>
    ...
  </PropertyGroup>
  ...
</Project>
こうすることで、Zip アーカイブファイル作成を行なう発行プロファイル (.pubxml) を使って発行を実行すると、プロジェクトファイルの内容 + その発行プロファイルの内容 が合成されて MSBuild が実行されるので、結果、以下の流れで Zip アーカイブファイルが生成されることとなる。


    当該発行プロファイル (.pubxml) 内で MakeZipAfterPublish プロパティに true が設定されているプロジェクトファイル (.csproj) 内に仕込んでおいた Zip アーカイブファイル作成ターゲットの Condition 属性による条件を満たすZip アーカイブファイル作成ターゲットが実行され、Zip アーカイブファイルができあがる



他の実現方法としては、Zip アーカイブファイル作成を行なうターゲット定義を、独立した外部の MSBuild スクリプトファイル (拡張子に決まりはないっぽいが .targets ファイルとか?) に書いておいて、発行プロファイル (.pubxml) からインポートするといった方法も思い浮かぶ。


補足
本投稿では割愛するが、今回紹介したのと同じような要領で、「発行」時ではなく、「ビルド」時に、ビルド成果物のファイル群を自動で Zip アーカイブファイルにまとめるよう、MSBuild スクリプト (.csproj) を記述することも可能だ。


なお、ちょっとした注意事項だが、この MSBuild 標準の "ZipDirectory" タスク、拡張子やファイル名パターンを指定しての、Zip アーカイブファイルへの収録を除外することはできないようだ。
指定したフォルダの中身を、一切合切、Zip アーカイブファイルに収録してしまう他ないらしい。


拡張子やファイル名パターンを指定しての Zip アーカイブファイルへの収録を除外するには、MSBuild 標準の "ZipDirectory" タスクに頼らずに、MSBuild.Extension.Pack NuGet パッケージをプロジェクトに追加して、"MSBuild.ExtensionPack.Compression.Zip" タスクを利用するのがよさそうだ。
NuGet Gallery | MSBuild.Extension.Pack


まとめ
以上、Visual Studio や dotnet CLI を使っての、ASP.NET Core Web アプリケーション開発における、ファイルへの「発行」時に、自動で、発行成果物ファイル群を単一の Zip アーカイブファイルにまとめる、MSBuild を使った方法の紹介であった。


さてさて、しかしながら昨今であれば、何らかの CI/CD のパイプラインで、このような Zip アーカイブファイルを作成するのかな、と想像する (自分はよく知らない)。


また、Docker イメージを構築して、コンテナ上で稼働させるようなケースでは、単一の Docker イメージとして持ち歩けるので、このような Zip アーカイブファイルにまとめるような需要はそもそも発生しないことだろう。


ということで、今となっては、このような方式で、発行成果物の Zip アーカイブファイルを作成するケースはあまりないかもしれない。


...ところで、MSBuild の v.15.8 って、いつリリースされたんでしたっけ??


ASP.NET Core + Angular の組み合わせの Web アプリで、Angular 側にもアプリ自身のバージョン番号を埋め込む

$
0
0
背景
サーバー側実装に ASP.NET Core、クライアント側実装に Angular (本投稿時点で v8) の組み合わせによる Web アプリ開発の話。


昨今は micro service 化であるとか、サーバー側実装は Server less プラットフォームを使うとかも多いのかもと思う。


しかし今回は上記のとおり、サーバー側実装と Angular 側実装とが、Visual Studio / dotnet CLI 開発における単一プロジェクトファイルで集約され、サーバー側もクライアント側も同時並行で開発を進めるような、モノシリックな種類の Web アプリ開発の話である。


サーバー側実装のアプリケーションバージョン番号
さて、サーバー側実装は ASP.NET Core ということで、アプリケーションのバージョン番号の管理 (?) は簡単だ。


方針としては、当該 ASP.NET Core Web アプリプロジェクトの、主たる出力アセンブリのアセンブリバージョンが、そのアプリのバージョン番号であるとする。


実際のバージョン番号の指定方法は、とりあえずプロジェクトファイル (.csproj) 内の <Version> プロパティで、アプリケーションのバージョン番号を記述しておけば無難だろう。


<!-- これは .csproj ファイル -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    ...
    <Version>1.2.3.4</Version>
    ...
  </PropertyGroup>
  ...
</Project>

Visual Studio で開発中であれば、プロジェクトファイル (.csproj) を直接エディタで編集するだけではなく、プロジェクトのプロパティ画面から、"パッケージ" セクションの GUI 経由で、"パッケージバージョン" の欄に記入して <Version> プロパティを設定することも可能だ。
_d0079457_13462702.png



なお、上記 Visual Studio の GUI 経由での編集でもわかるように、<Version> プロパティは厳密にはパッケージ化する際のパッケージのバージョン番号である。
別途明示しなかった場合にアセンブリバージョンもパッケージバージョンに倣うこととなっている。


ちゃんと区別して取り扱う場合は、<AssemblyVersion> プロパティに、アプリケーションのバージョン番号を記載するとよい。
もちろん、Visual Studio の GUI 上からも設定可能だ。
_d0079457_13485257.png


こうしてプロジェクトファイルに記載したアセンブリバージョンは、サーバー側実装 (今回は C#) のコードからは、下記コードで取得可能だ。


// これはサーバー側実装の任意の C# コード。
// 変数 appVer に System.Version オブジェクトが入る。
// (上記 .csproj の例だと、ver.1.2.3.4 を示す System.Version オブジェクト)
var appVer = typeof(Program).Assembly.GetName().Version;

なお、プロジェクトファイル (.csproj) 中に記載するアプリケーションのバージョン番号を、どのように加算していくか、CI/CD なども絡めたバージョン番号管理とその自動化などについては、本投稿では割愛する。


クライアント側にも、サーバー側実装と同じバージョン番号を埋め込みたい!
さて、サーバー側実装でこのように管理することとしたアプリケーションのバージョン番号であるが、同じバージョン番号を、クライアント側実装である Angular のコードでも埋め込みたくなった。


というのも、今回案件は前述のとおり、サーバー側実装とクライアント側実装とが割と密接につながっているモノシリック構造である。
そのため、ブラウザに古いバージョンのクライアント側実装がキャッシュされて残ってて、しかしサーバー側実装が新しくなっている、という、サーバー側とクライアント側との間での "アプリケーションのバージョン番号の不一致" が問題になるのである。


その点、ビルド時のタイミングで、クライアント側実装にサーバー側アプリケーションバージョン番号を埋め込むことができれば、あとはサーバー側アプリケーションバージョン番号を返すような Web API をサーバー側に設け、これをクライアント側から呼び出すことで、クライアント側実装が、自身のバージョン番号がサーバー側と食い違っていないか、セルフチェックできるわけだ。




ということで、プロジェクトファイル (.csproj) はすなわち MSBuild スクリプトであるわけなので、プロジェクトファイル (.csproj) 内に MSBuild スクリプトを書き足してこれを実現してみた。




方針としては、「<Version> プロパティの内容を、TypeScript コード表現にして .ts ファイルを生成する」としてみた。


生成する .ts ファイルは、"~/ClientApp/src/app/app.version.ts" とし、生成されるこの .ts ファイルの内容は、以下の TypeScript コードとする。


export const appVersion = "1.2.3.4"

サーバー側とクライアント側とでバージョン番号の不一致がないかチェックする処理などでは、この "app.version.ts" を import して、文字列定数 "appVersion" を参照すればよいようにする。




以上の仕様・要件を実現する、プロジェクトファイル (.csproj) 内に書き足す MSBuild スクリプトは下記のとおりとなった。


<!-- これは .csproj ファイル -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <Target 
    Name="GenerateAppVersionFileForSPA" 
    BeforeTargets="PreBuildEvent">
    <WriteLinesToFile 
     File="$(SpaRoot)src/app/app.version.ts"
     Lines="export const appVersion = &quot;$(Version)&quot;"
     Overwrite="true" 
     WriteOnlyWhenDifferent="true">
    </WriteLinesToFile>
  </Target>
  ...
</Project>

まず、(ターゲット名は何でも良いのだが) BeforeTargets 属性として "PreBuildEvent" を指定し、ビルド開始時点で実行されるターゲット枠を用意する。


そして、MSBuild には標準で "WriteLinesToFile" という、テキストファイルへの書き出しを行なうタスクが装備されているので、これを用いて "app.version.ts" ファイルの作成・内容の書き込みを行なっている。


MSBuild スクリプトの仕組みとして、<Version> プロパティの内容は、"$(Version)" の記法で参照、該当部分が置換される。
これを利用して、.ts ファイルの内容としてアセンブリバージョン (厳密にはパッケージバージョン) を書き込んでいる次第。


MSBuild スクリプトファイルは XML 記法なので、ダブルクォーテションの記述などに XML 実体参照記法で記載している点が少し注意すべきところであろうか。


あとは、上書きを許可 (Overwrite="true") しつつ、内容に変化がなければいたずらに書き込んでタイムスタンプを更新してライブリロードが走ったりしないよう、WriteOnlyWhenDifferent="true" の指定を付け加えるなど、微調整して完成である。


以上の仕込みでビルドを実行すると、めでたく、プロジェクトファイル (.csproj) 中で指定したバージョン番号が、TypeScript ソースファイルとしても自動生成されるようになった。


補足
なお、この app.version.ts ファイルであるが、このような MSBuild スクリプトによる自動生成であることを、コメントで明記したほうがよいだろう (下記例)。


// このファイルは "GenerateAppVersionFileForSPA" MSBuild タスクによって自動生成されました。
export const appVersion = "1.2.3.4"

このように複数行にわたるテキストファイルを、"WriteLinesToFile" MSBuild 標準タスクで作成するには、同タスクの "Lines" 属性に指定するテキスト内容を、1行ごとにセミコロン (;) 区切りで記述すればよい。


<!-- これは .csproj ファイル -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <Target 
    Name="GenerateAppVersionFileForSPA" 
    BeforeTargets="PreBuildEvent">
    <WriteLinesToFile 
     File="$(SpaRoot)src/app/app.version.ts"
     Lines="// このファイルは~;export const appVersion = &quot;$(Version)&quot;"
     ...
</Project>

まとめ
以上、サーバー側とクライアント側との双方に、プロジェクトファイル (.csproj) に記載されたバージョン番号を埋め込む方法であった。


あとは前述のとおり、サーバー側に自身のバージョン番号を返す Web API を設けつつ、これをクライアント側から呼び出すことで、サーバー/クライアント間のバージョン番号不一致を検出できる。


バージョン番号の不一致を検出したらどうするか (UI 上にその旨通知するとか、問答無用でハードリロードを実行するとか) は、アプリケーションの仕様・要件次第だろう。


また、クライアント側実装にアプリケーションのバージョン番号が埋め込まれているので、気軽にアプリケーションのバージョン番号表示をクライアント側実装のみで行なうことも可能だ。


Twitter 認証を実装した ASP.NET Core 3.1 Web アプリで、Twitter 認証時にキャンセルをしたら、アプリがクラッシュした件

$
0
0
背景
ASP.NET Core 3.1 による、C# で実装した自作 Web アプリでの話。


昨今の Web アプリ用フレームワークの例に漏れず、ASP.NET Core においても、いわゆる "Twitter アカウントでサインイン" といった外部認証を実装することは容易である。


例えば Twitter での認証の仕組みを組み込みたい場合は、下記公式ドキュメントなどを参考に実装すればよい。


上記公式ドキュメントを参照することでわかるとおり、他にも、Google アカウントや Microsoft アカウント、Facebook アカウントなどなど、様々な外部アカウントを使った認証が容易に組み込み可能だ。



実装例
実際、自分が開発しているとある Web アプリ (ソースコードは下記) でも、Twitter による認証を実装している。


実装についてポイントだけかいつまむと、まずは自作の Web アプリのソースコード中、DI 機構へのサービス登録を行なっている、Startup クラスの ConfigureServices() メソッド内にて、標準的な Cookie ベースの認証用のサービスに加えて、Twitter 認証のためのサービスを "AddTwitter()" 拡張メソッドで登録してやる。



このタイミングで、Twitter 側で作業を済ませておいた、コンシューマー API キーとシークレットを、Twitter 認証構成オプションに設定するよう実装しておく。
// Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
  ...
  services.AddAuthentication(...)
    .AddCookie(...)
    // Twitter アカウントでのサインイン機構を組み込み
    .AddTwitter(options =>
    {
      options.ConsumerKey = ...;
      options.ConsumerSecret = ...;
    });
    ...
続けて、HTTP 要求を処理するパイプライン中に、認証の処理を挿入するため、同じく Startup クラス内の Configure() メソッド内にて、"UseAuthentication()" 拡張メソッドをしかるべき順序で呼び出し・ミドルウェアの挿入を行なっておく。
// Startup.cs
public void Configure(IApplicationBuilder app, ...)
{
  ...
  app.UseAuthentication();
  ...

最後に、Twitter アカウントでの認証を開始するための MVC アクションや API エンドポイントを実装し、これを呼び出してやれば良い。
// 何かコントローラなど
...
[HttpGet("/auth/signin")]
public IActionResult SignIn()
{
  // 認証スキーマとして Twitter による外部認証を指定している
  return Challenge(TwitterDefaults.AuthenticationScheme);
}

詳細はかなり端折っているが、概ね上記のような実装にて、この Web アプリの ~/auth/sigin へ遷移すると、Twitter の認証画面が開き、Twitter アカウントによる認証ができるようになる。


_d0079457_18564335.png



実装の "手抜き" が発覚!
さて、そんな自作 Web アプリにおいて、ちょっと手抜きしていた点が発覚してしまった。


自作 Web アプリから認証を開始し、Twitter 側のページに遷移したあと、この Twitter の認証画面で「キャンセル」された場合を考慮していなかったのだ。


で、実際に Twitter の認証画面でのキャンセルを試してみたところ、あえなく、この自作 Web アプリは HTTP 500 Internal Server Error を吐いてしまった。


ローカル開発環境にて確認してみると、下記のような例外が発生していた。
System.Exception:
  An error was encountered while handling the remote login.
System.Exception:
  Access was denied by the resource owner or by the remote server.
 at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()

解決方法
では、外部認証先でのキャンセルの操作に対応するにはどうしたらよいのか?


某氏に教えてもらったところ、下記 Stackoverflow のスレッドに答えがあった。


すなわち、Twitter 認証のためのサービスを DI 機構に登録する際、そのオプション構成指定時に「Twitter による認証が失敗した」ときに呼び出されるコールバック関数を指定することができる。
具体的には、オプション引数の Events > OnRemoteFailure プロパティがそのコールバック関数となる。


というか、コールバック関数を指定してアプリケーション開発者が外部認証失敗時の処理を明示しないと、前述の例外発生となってしまうようだ。


さて、OnRemoteFailure で指定するコールバック関数、その引数には RemoteFailureContext オブジェクトが渡される。
このコンテキストオブジェクトには、Exception 型の Failure プロパティが公開されている。
これを参照することで、同じ「認証失敗」であっても、キャンセルなのかもっと他の要因で失敗したのか、処理の場合分けができそうだ (※自分は未確認)。


また、このコールバック関数の引数 = RemoteFailureContext オブジェクトには Response プロパティが提供されている。
この Repsonse オブジェクトを使うことで、外部認証失敗時における、お好みの HTTP 応答を返すことができる。


自分のアプリの場合は、何があろうと雑に、しれっとアプリのトップページに戻すこととした。
具体的には下記のとおり実装してみた。
...
.AddTwitter(options =>
{
  ...
  options.Events.OnRemoteFailure = context =>
  {
    var appUrl = $"{context.Request.Scheme}://{context.Request.Host}/";
    context.Response.Redirect(appUrl);
    context.HandleResponse();
    return Task.CompletedTask;
  };
})
...

まとめ
以上で Twitter の認証画面でキャンセルされた場合でも、HTTP 500 ページになることなく、それなりの動作 (今回の例では単にアプリケーションのトップページに戻るだけ) に収めることができた。


自分は Twitter アカウントによるサインインでしか確認していないが、おそらくは他の外部認証の場合でも、この Events.OnRemoteFailure による外部認証失敗時のハンドリングの仕組みは同じことだろう。

MSBuild の WriteLinesToFile で、改行、あるいはセミコロンを含むテキストを、ファイルに書き出す方法

$
0
0
ビルドスクリプト "MSBuild"
C# プログラミングなどで使われるビルドスクリプト MSBuild のお話。


すなわち、C# プログラミングなどにおいて、"プロジェクトファイル" と呼ばれる、例えば拡張子 .csproj の、XML 書式のあのファイルが、実は MSBuild (エムエスビルド) スクリプトファイルである。



最近は .NET Core の進出により、プロジェクトファイル (MSBuild スクリプトファイル) の中身が、"SDK スタイル" と呼ばれる随分とすっきりした内容に進化している。  


また、Visual Studio を使って開発中の場合、この "SDK スタイル" のプロジェクトファイルを、Visual Studio のソリューションエクスプローラからのダブルクリックで、Visual Studio 内のエディタで中身が開かれる (※Visual Studio の設定によって動作は変更可能)。


このようなこともあり、最近はプロジェクトファイル (MSBuild スクリプトファイル) をエディタで直接編集して、ビルド時のカスタム処理などを MSBuild スクリプトとして書くことも多くなった。


MSBuild スクリプトでテキストファイルを書き出す
さてそんな MSBuild スクリプトであるが、つい先日、ビルド時のカスタム処理のひとつとして、とあるテキストファイルの生成を、MSBuild スクリプト内で実装したい要件が発生した。
このようなケースで使える MSBuild スクリプトの標準タスク (プログラミング言語で言うところの標準ライブラリ関数の MSBuild 版) として、WriteLinesToFile タスクがある。



上記公式ドキュメントにあるとおり、使い方は難しくない。
書き込み先のファイルのパスと、書き込む内容の文字列を、それぞれしかるべき属性に指定してやれば良い。


例えば以下のような XML ファイルを、例えば foo.msbuild とかいうファイル名で保存しておき、
<Project>
  <Target Name="test">
    <WriteLinesToFile 
      File="foo.txt" 
      Lines="Hello World" />
  </Target>
</Project>

.NET Core SDK がインストールされている環境 (Windows でも macOS でも各種 Linux ディストリでも OK) で、この foo.msbuild ファイルがあるディレクトリ上で以下のように dotnet コマンドを実行すれば、
dotnet msbuild test.msbuild -t:test

同ディレクトリ直下に、"foo.txt" というファイル名で、中に "Hello World" と書かれたテキストファイルが生成される。


なお、既に指定されたファイル名のファイルが存在している場合は、そのファイルに行追加されるのが既定の動作。


追記ではなく上書きとしたい場合は Overwrite 属性を書き足して true を指定してやればよい。
そうすることで、追記ではなく、上書きの動作となる。


また Overrite="true" を設定している場合に、これから書き込もうとしている内容と、既に存在する書き込み先ファイルの内容とが合致している場合は何もしない、すなわち、書き込み先ファイルの最終更新日時を変更しないようにすることもできる。  
そのためには WriteOnlyWhenDifferent 属性に true を設定してやるとよい。


改行を含むテキストファイルを生成するには?
さてそんな WriteLinesToFile 標準タスクであるが、さて、内容が単一行のテキストファイルではなく、複数行の内容のテキストファイルを書き出したい場合はどうすればよいだろうか?


まずひとつめのやり方だが、そもそもとして、MSBuild スクリプトは XML 書式で記述する。
なので、WriteLinesToFile 標準タスクの Lines 属性に指定する内容に、改行を意味する文字コードを XML の文法に従ってエンコードして指定 (実体参照) することで実現可能だ。


LF での改行を行なうなら、以下のように "&#xa;" を指定するとよい。 
<WriteLinesToFile 
  File="foo.txt" 
  Lines="line1&#xa;line2&#xa;line3"
  Overwrite="true"/>
そして、他にも MSBuild 特有の方法がある。


実は、MSBuild では、「複数の項目 (リスト)」を即値で記述するには、セミコロンで区切る、という文法となっている。


強引にざっくり例えると、JavaScript であれば  "[1, 2, 3]" というように「複数の項目」を表現するところを、これを MSBuild で示すと "1;2;3" という記法になる、ということだ。


そして、WriteLinesToFile 標準タスクにおける、書き出すテキスト本文を指定する属性の名前をよくよく見ると、"Lines" というように複数形になっている。


そう、実は WriteLinesToFile 標準タスクは、書き出すテキスト本文の指定に「複数の項目 (リスト)」を渡せる仕様なのだ。


ということで、改行を含むテキストファイルの生成は以下のように記述できる。
<WriteLinesToFile 
  File="foo.txt" 
  Lines="line1;line2;line3"
  Overwrite="true"/>
※こういった MSBuild の書き方は、下記書籍で勉強させていただきました。

...じゃぁセミコロンを書き出すにはどうすれば?
さてここで注意が必要となるのは、「改行がしたいんじゃなくて、セミコロンを含むテキストを書き出したい」場合だ。


実体参照方式で "&#x3b;" などとセミコロンの文字コードで指定しても、これは効果はない (改行になる)。
よく考えればわかることではあるが、実体参照方式で記述しようとしまいと、その XML を読み込んだ側、すなわち MSBuild エンジンの身にしてみれば、セミコロンはセミコロンである。  
つまり、元の XML 側での表現方法が違っても、パース (読み込み) した結果には違いがない訳だ。


このケースで必要なのは XML の書き方の知識ではなく、MSBuild における特殊文字のエスケープ表記方法の知識だ。


幸い、その方法は公式ドキュメントに明記されている。


上記にあるとおり、「% に続く、ASCII 文字コードを意味する16進数表記」という記法で、MSBUild 上における特殊文字、すなわち今回のケースではセミコロンを、表現可能だ。



セミコロンについては、具体的には `%3b` と記述すればよい。


ということで、下記のような C# ソースコードを生成することも可能である。
<WriteLinesToFile 
  File="foo.cs" 
  Lines="var foo = &quot;bar&quot;%3b"
  Overwrite="true"/>

おわり
以上。


XML の実体参照による特殊文字の記述と、MSBUild 固有の特殊文字をエスケープする記法とで、つい頭の中がこんがらがってしまうことがあったので、改めて本記事にまとめてみた次第。



MSBuild スクリプトで、ビルド時に同時に C# ソースファイルを自動生成しようとして、「行末のセミコロンが書き出されない!」と焦っていたのは内緒だ。

(エディタのXML自動整形でレイアウトが崩れてしまわないよう) プロジェクトファイル外に NuGet パッケージリリースノートを書く

$
0
0
プロジェクトファイルをエディタで編集するのは容易
昨今の C# プログラミングにおける話。
とくに Windows OS 上で Visual Studio を使っての開発場面を想定している。


最近では、XML 形式である C# プロジェクトファイル (.csproj) を、Visual Studio のプロジェクトのプロパティ表示 GUI 経由ではなく、テキストエディタで直接 XML を編集する機会が増えているように思う。


というのも、"SDK スタイル" と呼ばれる今風の形式のプロジェクトファイルであれば、その内容はきわめて簡素だからだ。


加えて既定の設定だと、Visual Studio のソリューションエクスプローラにてプロジェクトのノードをダブルクリックしたときに、そのプロジェクトが "SDK スタイル" であるならば、プロジェクトのプロパティ GUI ではなく、XML テキストエディタで開かれたりもする。


しかしリリースノートの編集は酷いことに...
さてところで、NuGet パッケージライブラリを開発しているような場合は、パッケージに記載されるリリースノートを記述することと思う。



パッケージのリリースノートを、Visual Studio のプロジェクトプロパティ GUI にて編集している間は何の問題もない。
しかしひとたび、Visual Studio のテキストエディタにて、プロジェクトファイルの XML を直接編集し始めると状況は一転する。


というのも、Visual Studio は親切にも、XML 形式であるプロジェクトファイルを保存するたびに、その書式を自動整形してくれるからだ。


そしてこの自動整形が、なんと、プロジェクトファイル内に記載された、改行を含むリリースノートのレイアウトをぶち壊してくれるのである!



_d0079457_21171768.gif



ご覧のとおり自動整形が適用されることで、リリースノートに、無用な空白や改行が大量に追加されてしまうのだ。



もちろん、自動整形の機能を無効に設定すれば、このような悲劇は防ぐことが出来る。
とはいえ、今度は、自動整形なしでプロジェクトファイルの編集 (例えば、カスタムビルド処理を書く場合とか) が厄介なことになってしまう。



さてどうしたものか。



解決策
こんなことで悩んでいたある日、ようやく解決策を思いついた。


その方法は、リリースノートをプロジェクトファイル外に記述し、代わりにプロジェクトファイル内からその外部リリースノートファイルを取り込む、というものだ。


以下、その手順を記す。



Step 1. プロジェクトファイル外にリリースノートを記述
まず始めに、"RELEASE-NOTES.txt" といったファイル名のテキストファイルとして、パッケージリリースノートを記述する。
例えばこんな感じ。
v.1.0.1
- Support to Blazor v.3.2 Preview 3.


v.1.0.0
- Initial release.

"RELEASE-NOTES.txt" はどこに配置しても構わないが、以降の説明では、ソリューションがあるフォルダに配置したものとする。
Step 2. プロジェクトファイルからこれを取り込む
次の手筈として、数ステップの MSBuild スクリプトをプロジェクトファイル内に実装し、"RELEASE-NOTES.txt" ファイルの内容を取り込むようにする。


この目的で、MSBuild ターゲットをひとつ作成し (ターゲット名は既存のターゲットと衝突しなければ何でも良い)、"GenerateNuspec" ターゲットよりも前に実行されるようにする。



"GenerateNuspec" ターゲットが実行される直前は、パッケージリリースノートの準備をするのに最適なエントリポイントとなっている。



この MSBuild ターゲットでは、MSBuild に標準で備え付けの "ReadLinesFromFile" タスクを使って "RELEASE-NOTES.txt" ファイルの各行を読み出し、適当なアイテム名 (ここでは "ReleaseNoteLines" とした) の MSBuild アイテムに、読み込んだ各行を出力する。



そしてこれら読み込んだ結果の MSBuild アイテムを使って、"PackageReleaseNotes" MSBuild プロパティを上書き設定すればよい。



なお、既定では、MSBuild はアイテムの連結時、区切り文字をセミコロン (";") で連結する。
しかしここでは、リリースノートを改行区切りで連結する必要がある。
なので、下記のように区切り文字として LF (ラインフィード) を明示的に指定して、MSBuild アイテムの参照を記述する必要がある。
@(ReleaseNoteLines, '%0a')
("%0a" は、MSBuild スクリプトファイル内では "LF" コードを意味する。)


MSBuild ターゲットの全体像は下記のとおりとなる。

<Target Name="BuildPackageReleaseNotes" BeforeTargets="GenerateNuspec">
<ReadLinesFromFile File="../RELEASE-NOTES.txt" >
<Output TaskParameter="Lines" ItemName="ReleaseNoteLines"/>
</ReadLinesFromFile>
<PropertyGroup>
<PackageReleaseNotes>@(ReleaseNoteLines, '%0a')</PackageReleaseNotes>
</PropertyGroup>
</Target>



Step 3. 同僚や他の開発者に向けて、コメントを残す
最後に仕上げとして、もとのリリースノート記述場所に、「パッケージリリースノートは "RELEASE-NOTES.txt" に書いて下さい」というコメントを記載して完成だ。


_d0079457_21170860.png



おわり
こうしてようやく、プロジェクトファイルをバリバリと直接エディタで編集もしつつ、安心してパッケージリリースノートも記述できる環境を整えることができた。



ASP.NET Core で静的ファイルを認証で保護する

$
0
0
"Microsoft Docs" サイトに記載のやり方は好きになれず...
C# 等による Web アプリ開発フレームワーク "ASP.NET Core" を用いた、サーバー側実装での話。


数週間ほど前から、新しい ASP.NET Core Web アプリ開発の案件が始まった。


この案件では、この Web アプリ上に配置される静的ファイルの一部を認証で保護、すなわち匿名アクセスは拒否する要件があった。


実のところ、ASP.NET Core 上に配置された静的ファイルを認証で保護する方法についてはわかっていなかった。
そこで、インターネット上で "protect static files, authorization, asp.net core" といったキーワードで検索するところから始めた。


相当数の検索結果がヒットしたが、その多くは "stackoverflow.com" に投稿された質問とその回答だ。


それらの回答は、以下のいずれかの解決策を提案しているケースが多かった。


(A) 自分で ASP.NET Core ミドルウェアのカスタム実装を作り、静的ファイルミドルウェアより前の HTTP 処理パイプラインにそのミドルウェアを差し込み、認証されていない要求をそのミドルウェアで拒否する。(B) 保護したい静的ファイルを "wwwroot" フォルダの外部に配置し、代わりに、[Authorized] 属性付きの自作の ASP.NET Core MVC コントローラでその保護したい静的ファイルを応答で返すようにする。


特に上記解決策のうち (B) については、下記 URL の "Microsoft Docs" サイトでも公式に説明が掲載されている。


しかしながら、詳細はここでは割愛するが、今回案件特有の要件や背景事情をはじめ、いくつかの要因から、上記解決策 (A) (B) はちょっと採用したくなかった。



そこでもう暫し検索結果を読み進めた結果、幸いなことに、「これはよさそう!」と思われる解決案を stackoverflow.com 内で発見した。


その解決策とは、
「静的ファイルミドルウェアが提供している、"OnPrepareResponse" というコールバックポイントをフックする」
という方法だ。


ASP.NET Core 静的ファイルミドルウェアが提供しているフックポイント
静的ファイルミドルウェアを HTTP 処理パイプラインに登録するとき、すなわち Statrtup クラスの Configure メソッド内で UseStaticFiles() メソッドを呼び出すときだが、この際、StaticFileOptions 型のオプション指定をその引数に渡すことができる。


この StaticFileOptions クラスに、静的ファイルを処理する直前に自前の処理を差し込みすることができる、あつらえたプロパティが用意されていたのだ。


そのプロパティの名前は OnPrepareResponse。


この OnPrepareResponse プロパティに自前の関数を設定すると、静的ファイルをブラウザに返す直前に、この設定した関数を呼び出してくれるのだ。


この自前の関数内で、要求が認証済みであるか否かに応じて、ブラウザへの応答を変更できる。


早速に、"stackoverflow.com" の下記回答にあるサンプルコードをコピーしてきて試してみた。




なお、もちろんのこと、認証ミドルウェアが静的ファイルミドルウェアよりも先に HTTP 処理パイプラインに登録されている必要がある。静的ファイルミドルウェアが要求を処理するより前に、認証済みであるか否かを判定済みにしておく必要があるからだ。



ということで自分のサンプルコードは下記のようになった。


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  ...
  app.UseAuthentication();
  app.UseStaticFiles(new StaticFileOptions
  {
    OnPrepareResponse = ctx =>
    {
      // 現在の要求が認証済みでないなら...
      if (!ctx.Context.User.Identity.IsAuthenticated)
      {
        // "HTTP 401 Unauthorized" を返す.
        ctx.Context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
      }
      ...



...なのに秘密のファイルが丸見え!
続けて先のサンプルコードをビルド、実行してみた、のだが...


_d0079457_21463503.png



期待に相反し、保護したはずの静的ファイルが、匿名アクセスでも見えてしまった!


たしかに自分が設定したコールバック関数は、認証されていない要求がきたら、 "401 Unauthorized" HTTP ステータスをブラウザに返せてはいるようだ。


しかしながら、静的ファイルミドルウェアがブラウザにコンテンツを返す処理に対し何の処置もしていなかったので、こんなことになったらしい。


応答全体を阻止する
HTTP ステータス 401 を返すことに加えて、静的ファイルミドルウェアによる応答本文をどうにかして破棄する必要がある。


これを実現するために、あともう2行、先のサンプルコードに追加した。


...
// "HTTP 401 Unauthorized" を返し...
ctx.Context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;


// さらに下記の2行を追加して、応答ボディを破棄!
ctx.Context.Response.ContentLength = 0;
ctx.Context.Response.Body = Stream.Null;
..



この追加の2行で、静的ファイルミドルウェアによる応答ボディをすべて破棄することができる。


というのも、応答オブジェクトの Body ストリームを System.Null に差し替えているためだ。


System.Null への書き込みはすべて単純に破棄され、また、読み取りは空を返すので、静的ファイルミドルウェアは応答ボディに対して何の影響も及ぼさなくなる。


こうすることで、めでたく、秘密のファイルを保護することに成功した。


_d0079457_21463908.png

なお、さらなる考慮事項として、ブラウザキャッシュの挙動にも気を付けた方が良さそうだ。


というのも、自分が試した範囲だと、いちど認証を済ませて保護された静的ファイルを開いたことがある場合、その後サインアウトして未認証の状態でも、ブラウザキャッシュから当該静的ファイルを見ることができた場合があったからだ。


自分は "Cache-Control" 応答ヘッダに "no-store" を指定し、キャッシュに保存することを一切禁止とブラウザに知らせることで、この挙動を回避した。


ctx.Context.Response.Headers.Add("Cache-Control", "no-store");



リダイレクトさせたい場合
未認証の状態で保護された静的ファイルの URL が要求されたときに、HTTP 401 応答を返すのではなく、"サインイン" ページなどにリダイレクトさせることも可能だ。


コードは次のようになるだろう。




public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  ...
  app.UseAuthentication();
  app.UseStaticFiles(new StaticFileOptions
  {
    OnPrepareResponse = ctx =>
    {
      if (!ctx.Context.User.Identity.IsAuthenticated)
      {
        // 任意の URL にリダイレクト可能.
        ctx.Context.Response.Redirect("/")
      }
      ...



まとめ
静的ファイルミドルウェアを HTTP 処理パイプラインに登録するとき (UseStaticFiles() 呼び出し) のオプション引数の OnPrepareResponse プロパティを活用することで、静的ファイルを認証で保護することができる。但し、UseAuthentication() 呼び出しを、UseStaticFiles(...) 呼び出しよりは前に済ませておくこと。また、HTTP 401 応答を返して要求を拒否する場合は、加えて静的ファイルミドルウェアによる応答ボディ書き込みも阻止すること。完璧に保護するためには、キャッシュ制御についても考慮が必要。もちろん、HTTP 401 応用を返すのではなく、特定の URL にリダイレクトさせることもできる。


サンプルコード全体は下記 URL の GitHub リポジトリで公開している。


実際に動作するライブデモサイトも下記 URL で稼働中だ。

https://protect-staticfiles-with-auth-on-aspnetcore.azurewebsites.net/



以上!




認証が有効な Blazor Wasm アプリで、アクセストークンなしで匿名アクセス可能なエンドポイントに HTTP リクエストを送信する

$
0
0
C# で Single Page Application (SPA) を実装できるフレームワーク "Blazor (ブレイザー)"、その WebAssembly 版を使っての SPA 開発中の話。


プロジェクトテンプレートから認証有効 な Blazor Wasm アプリを作るのは簡単
ある日、ASP.NET Coreサーバーでホストされている、認証対応の Blazor WebAssembly アプリを作ることになった。


過去にも、Twitter 認証する Blazor WebAssembly アプリを作ったことはある。
そのときは認証後のセッション情報はブラウザの Cookie を使う方式で実装していた。
そのため、ユーザー情報を内蔵した、また、アクセストークンを使用する方式の Blazor WebAssembly アプリを実装したことはこれまではなかった。


なお、自分は普段は Windows OS の Visual Studio を使って Blazor アプリを開発している。
そのため、下図のとおり、Visual Studio のプロジェクトテンプレートから認証機能付き Blazor WebAssembly アプリを作るのはとても簡単なことだ。


_d0079457_15265582.png



こうして実装した Blazor WebAssembly は認証後のセッション情報はアクセストークンとしてクライアント側に保持される。



この Blazor WebAssembly アプリでは、想定どおり、[Authorizaition] 属性を付与した Web API エンドポイントは匿名アクセスから保護されるようになっていた。


次は公開 API エンドポイントを追加
続けてこのアプリに、"最近の更新" ニュースフィードを一覧表示する機能を実装することになった。


"最近の更新" ニュースフィードのリストは、サーバー側で生成、Web API エンドポイントを経由してクライアント側に提供するように実装。


なお、要件として、このニュースフィードは、ログインしていないユーザーでも公開される必要があった。


そのため,匿名アクセスを許可する目的で,"最近の更新" リストを提供する API エンドポイントには [Authorization] 属性を付与しないよう実装した。

[ApiController]
[Route("[controller]")]
public class RecentlyUpdatesController : ControllerBase
{
  [HttpGet]
  public IEnumerable<string> Get()
  {
    ...

この API 実装を、資格情報なしの cURL コマンドでテストしてみたところ、もちろん問題なく動作た (下図)。


_d0079457_15265992.png

だがしかし... 捕捉されない例外が発生!
サーバ側の実装が完了、続けてクライアント側の実装に取り掛かった。


DI 経由で注入された「HttpClient」オブジェクトを使い、その匿名アクセス可能な API エンドポイントから "最近の更新" リストを取得するコードを記述。


そこまで実装してアプリを実行してみた。


しかし残念ながら、予期せぬことに、画面上に "最近の更新" リストが表示される代わりに何かのエラーが発生してしまった。

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
  Unhandled exception rendering component: ''
  Microsoft.AspNetCore.Components.WebAssembly.Authentication.AccessTokenNotAvailableException: ''
  at Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthorizationMessageHandler.SendAsync(
    System.Net.Http.HttpRequestMessage request, 
    System.Threading.CancellationToken cancellationToken)

_d0079457_15265063.png



この例外の原因は...
Visual Studio のプロジェクトテンプレートから「認証」オプションを有効にして作成された Blazor WebAssembly アプリのコードをよく読んでみた。


するとどうやら、HTTP リクエストの送信時は、ユーザがサインインしているかどうかに関わらず、また、リクエスト先の URL に関係なく、その HTTP リクエストに常にアクセストークンを添付するよう、起動時の初期化コードで構成されているようであった。


しかしユーザーがサインインしていないときは、アクセストークンを HTTP 要求に添付したくても、(まだサインインしていないので)アクセストークンを入手前なので添付できない。


以上が原因で AccessTokenNotAvailableException 例外が発生してしまったようだ。


解決方法
この問題を解決する方法は、自分が思いついた範囲だと3つほどある。


解決策 その 1
解決策その 1 は、AuthorizationMessageHanlder を設定して、指定されたサブパス以下のエンドポイントにのみアクセストークンを添付できるようにすることである。


この方法を選択するには、まず、サーバ側のエンドポイント (保護したいエンドポイント) の URL を、同じサブパス (例: "/authorized/...") の下になるように配置し直す必要がある。

[Authorize]
[ApiController]
// [Route("[controller]")]
[Route("authorized/[controller]")]
// 👆 このAPIのURLを "authorized "サブパスの下になるように変更。
public class WeatherForecastController : ControllerBase
{
...

次に、クライアント側で AuthorizationMessageHandler をカスタムオプションで設定する必要がある。


これはクライアント側プロジェクトの Program クラスの Main メソッドで行う。



public static async Task Main(string[] args)
{
  var builder = WebAssemblyHostBuilder.CreateDefault(args);
  builder.RootComponents.Add<App>("app");


  // 👇 この、AuthorizationMessageHandler を構成する
  //    コードを追加。
  builder.Services.AddTransient<AuthorizationMessageHandler>(sp =>
  {
    // 👇 作業に必要なサービスを DI から入手。
    var provider = sp.GetRequiredService<IAccessTokenProvider>();
    var naviManager = sp.GetRequiredService<NavigationManager>();


    // 👇 新しい「AuthorizationMessageHandler」インスタンスを作成、
    //    設定後にそれを返す。
    var handler = new AuthorizationMessageHandler(provider, naviManager);
    handler.ConfigureHandler(authorizedUrls: new[] {
      // 👇 ここに、HTTP 要求送信時には
      //    アクセストークンを添付したい URL を列記。
      naviManager.ToAbsoluteUri("authorized/").AbsoluteUri
    });
    return handler;
  });
  ...

最後に、HttpClientFactory に対し、こうして自分で設定したAuthorizationMessageHandler を使用するように設定する。

  ...
  // 👇 BaseAddressAuthorizationMessageHandler ではなく、
  //   上記で設定した AuthorizationMessageHandler を使用。
  builder.Services.AddHttpClient("BlazorWasmApp.ServerAPI", client => ...)
    // .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
    .AddHttpMessageHandler<AuthorizationMessageHandler>();


  // Supply HttpClient instances that include access tokens when making requests to the server project
  builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("BlazorWasmApp.ServerAPI"));
  ...

以上で、"/authorized/..." サブパス以下の URL に HTTP 要求を送信する場合にのみ、アクセストークンが添付されるようになる。


解決策 その 2
解決策その 2 は、認証が必要なエンドポイントに HTTP リクエストを送信する際に、「IHttpClientFactory」からアクセストークンを添付するように設定された,
名前付きの HTTP クライアントを明示的に取得する方法だ。


まず、クライアント側プロジェクトの Program クラスの Main メソッドで HttpClient を DI に登録する際、IHttpClientFactory サービスから HttpClient を取得するのではなく、ベースアドレスを設定しただけのプレーンな HttpClient を DI に登録するようにする。

  public static async Task Main(string[] args)
  {
    ...
    // 👇 DI には、プレーンな HttpClient サービスを登録。
    // builder.Services.AddTransient(sp => ...CreateClient("BlazorWasmApp.ServerAPI"));
    builder.Services.AddTransient(sp => new HttpClient { 
      BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });
    ...

そして、保護された API エンドポイントに HTTP 要求を送信する際には、DI から直接HttpClient を取得するのではなく、IHttpClientFactory から名前付きの HttpClient オブジェクトを明示的に指定して取得すればよい。

@* @inject HttpClient Http *@
@inject IHttpClientFactory HttpClientFactory
...
@code {
  protected override async Task OnInitializedAsync()
  {
    ...
    // 👇 保護された API にアクセスするためには、
    //    DI から直接 HttpClient を取得しない。
    // forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");


    // 👇 代わりに IHttpClientFactory サービスから
    //    明示的に名前を指定して HttpClient を取得して使用。
    var http = HttpClientFactory.CreateClient("BlazorWasmApp.ServerAPI");
    forecasts = await http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}

解決策 その 3
解決策その 3 は、解決策 その 2 とは逆の解決方法である。


この解決策は、匿名アクセスを許可されたエンドポイントに HTTP リクエストを送信する際に、IHttpClientFactory からプレーンな HTTP クライアントを、名前を指定して明示的に取得する方法だ。


これを行うためには、クライアント側プロジェクトの Program クラスの Main メソッドにて、IHttpClientFactory サービスの構成処理に対し、プレーンな HttpClient を名前付きで登録しておく。

public static async Task Main(string[] args)
{
  ...
  // 👇 プレーンな HttpClient を、
  //    IHttpClientFactory サービスに名前を付けて追加する。
  builder.Services.AddHttpClient("BlazorWasmApp.AnonymousAPI", client => {
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
  });


  builder.Services.AddHttpClient("BlazorWasmApp.ServerAPI", ...)
      .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
  ...

そうした上で、公開 API のエンドポイントに HTTP 要求を送信する際には、DI から HttpClient オブジェクトを直接取得するのではなく、IHttpClientFactory から名前を指定して明示的にプレーンな HttpClient オブジェクトを取得して使うとよい。

@* @inject HttpClient Http *@
@inject IHttpClientFactory HttpClientFactory
...
@code {
  protected override async Task OnInitializedAsync()
  {
    ...
    // 👇 公開 API にアクセスするためには、
    //    DI から直接 HttpClient を取得しない。
    // RecentlyUpdates = await Http.GetFromJsonAsync<string[]>("RecentlyUpdates");


    // 👇 代わりに IHttpClientFactory サービスから
    //    明示的に名前を指定して HttpClient を取得して使用。
    var http = HttpClientFactory.CreateClient("BlazorWasmApp.AnonymousAPI");
    RecentlyUpdates = await http.GetFromJsonAsync<string[]>("RecentlyUpdates");
}

まとめ
Visual Studio のプロジェクトテンプレートから "認証 "オプションを有効にして作成した Blazor WebAssembly アプリは、ユーザがサインインしているかどうかに関わらず、常にアクセストークンを HTTP 要求に添付しようとする。


この実装では、たとえ匿名のエンドポイントへのアクセスであっても、ユーザがサインインしていない場合は "AccessTokenNotAvailableException" 例外が発生してしまう。


この問題は、以下のいずれかの方法で回避することが可能だ。


解決策 1. - 指定されたサブパス以下の URL のみにアクセストークンを添付するように制限を設定する。解決策 2. - 保護された API にアクセスする場合に、アクセストークンを添付するよう構成した HttpClient を明示的に名前を指定して取得して使用する。解決策 3. - 匿名でアクセス可能な API にアクセスする場合に、プレーンな HttpClient を明示的に名前を指定して取得して使用する。


今回の問題の再現と、上記 3 つの解決策を収録したサンプルコードを、下記 GitHub リポジトリで公開してある。


以上


ASP.NET Core Blazor アプリで Razor コンポーネントパッケージを使用時、「HTTP 404 '_content/Foo/Bar.css' not found」が発生したら

$
0
0
C# で Single Page Application (SPA) を実装できるフレームワーク "Blazor (ブレイザー)" を使っての SPA 開発中の話。

はじめに ― "Razor Class Library" パッケージについて
Blazor アプリ開発においては、Blazor コンポーネントを含むクラスライブラリプロジェクトを作成し、他のプロジェクトから参照してそれらを再利用することができる。


これらのライブラリは「Razor クラスライブラリ」と呼ばれ、NuGet パッケージとして単一のファイル (.nupkg ファイル) にパッケージ化することもできる。


「Razor クラスライブラリ」 NuGet パッケージには、JavaScript、CSS、グラフィックス、フォントなどの静的 Web アセットを含めることもでる。


「Razorクラスライブラリ」を作成してパッケージ化する方法
Windows OS で Visual Studio IDE を使用している場合、プロジェクトテンプレートの「Razor クラスライブラリ」を選択して開始することができる。
_d0079457_21591971.png


dotnet CLI を使用する場合は、「dotnet new razorclasslib」コマンドが同等のことを行なう。

_d0079457_21591930.png



テンプレートから新しい「Razorクラスライブラリ」プロジェクトが生成されると、そのプロジェクトには、Razor コンポーネント (.razor)、JavaScript、CSS、および PNG 画像ファイルが含まれていることが確認できる。
_d0079457_21591982.png



そして、このプロジェクト上で必要なものを実装したら、この Razor クラスライブラリを NuGet パッケージファイル(.nupkg)としてパッケージ化可能だ。


Visual Studio IDE を使っている場合は、ソリューションエクスプローラーでプロジェクトのノードを右クリックし、コンテキストメニューの [パッケージ] メニュー項目をクリックすればよい。
_d0079457_21591935.png


dotnet CLI を使用する場合、「dotnet pack」コマンドによって同じことができる。

_d0079457_21591986.png


パッケージ処理が完了すると、NuGet パッケージ(.nupkg)ファイルが生成される。



そして、プロジェクトに含まれている静的 Web アセットファイルも、NuGet パッケージファイル内に同梱される。
_d0079457_21591932.png

パッケージを再配布して使用するには?
「Razorクラスライブラリ」を NuGet パッケージ化した以降、さまざまな方法でこの NuGet パッケージを再配布できる。


最も簡単な方法の 1 つは、NuGet パッケージファイルを、ただただ単純に、ファイルシステム上に保管することだ。


NuGet パッケージを置いてあるフォルダーの場所を、パッケージソース設定に追加しておけば(以下を参照)、
_d0079457_21592065.png



それ以降、そのフォルダに配置した NuGet パッケージへのパッケージ参照を、Blazor アプリプロジェクトに追加することができるようになる。
_d0079457_21592009.png



また、パッケージ参照を追加した以降、index.html または _Host.cshtml などのルートドキュメントから、パッケージ参照している NuGet パッケージに含まれている、静的 Web アセットを参照することができるようになる。


具体的には、以下の命名則の URL で、これらの静的 Web アセットを参照できる。


"_content/"で始まり、続けて、パッケージの名前、最後に、アセットファイルのファイル名


以上の要領で、静的 Web アセットを含む再利用可能な Blazor コンポーネントライブラリパッケージを、実に簡単に、作成、再配布、使用することができる。
_d0079457_21592076.png



ところで。静的 Web アセットファイルはどこ?
さてところで、アプリケーションプロジェクトの出力フォルダを見てみると、そこには、NuGet パッケージによって提供される静的 Web アセットファイルは一切収録されていない。
_d0079457_21592036.png


ではいったい、どうやって NuGet パッケージに含まれている静的 Web アセットがブラウザに読み込まれるのだろうか?



重要なのは ".StaticWebAssets.xml" ファイル
答えは、
「これらのファイルは NuGet パッケージキャッシュフォルダーから取得される」
である。


出力フォルダを探してみると、"{OutputName}.StaticWebAssets.xml" というファイル名の XML 形式のファイルを見つけることができるはずだ。


この .StaticWebAssets.xml ファイルは、ビルドプロセスで生成され、このファイルの中に、静的 Web アセットを示す URL パスから、物理的なファイルシステム上の所在への、マッピング情報が記述されている。
_d0079457_22035480.png


この .StaticWebAssets.xml ファイルは、

ASP.NET Core に備え付けの "StaticWebAssetsFileProvider" ファイルプロバイダ
(Blazorサーバー、または Blazor WebAssembly で ASP.NET Core ホストの場合)または Blazor 開発サーバー
(Blazor WebAssembly の場合)
によって読み取られる。



そして "StaticWebAssetsFileProvider" ファイルプロバイダーは、NuGet パッケージキャッシュフォルダーにある静的 Web アセットコンテンツを、.StaticWebAssets.xml ファイルによって提供されるURL マッピング情報を使用して Web ブラウザーに返答する、という仕掛けである。


ASP.NET Core のこの「静的アセット」機能は、ビルド時間を短縮化し、ディスク容量のファイル使用量を削減するのに役立つ。


なお、この機能はアプリケーション開発時のために設計されている。


プロジェクトを発行した後は、発行されたフォルダーには、NuGet パッケージに含まれるすべての静的Web アセットファイルが静的に配置されているのがわかる。
_d0079457_22035578.png




やっと本題 ― ある日、コンポーネントのスタイルが失われた!
さて、とある Blazor アプリの開発作業中に、その問題が発生した。


なんと、NuGet パッケージ参照先のコンポーネントのスタイル設定が失われてしまったのだ。
_d0079457_22035511.png


Web ブラウザーの開発者ツールを開いて「ネットワーク」タブで再確認してみたところ、静的 Web アセット (この場合、NuGet パッケージにバンドルされた .css ファイル) への HTTP GET 要求が、HTTP 404「リソースが見つかりません」で失敗していた。

_d0079457_22035535.png



最初に思い浮かんだ原因は、.StaticWebAssets.xml ファイルが何らかの理由で生成されなかった、又は破損したのではないか、という考えだ。


しかしいざ調べてみたところ、.StaticWebAssets.xml ファイルはちゃんと実在しており、その中身についても何の問題も見当たらなかった。


ではいったい何がこの現象を引き起こしたのだろうか。


真の原因は ...
実はこの現象に遭遇する直前に行なっていたとある作業内容が関係していた。


リリース環境にデプロイしたときにより近い状況でのデバッグを行なう必要があったため、"ASPNETCORE_ENVIRONMENT" 環境変数を、"Development" ではなく、"Release" に設定変更していたのだ。
_d0079457_22035568.png




実は、静的 Web アセット機能を実現している StaticWebAssetsFileProvider ファイルプロバイダは、なんと、"Environment" 構成が "Development" の場合にのみアプリケーションに組み込まれるのであった...!(下記のASP.NET Core ソースコードを参照。)


実際、"ASPNETCORE_ENVIRONMENT" 環境変数の設定を、"Development" に戻したところ、先の HTTP 404 エラーは解消され、目的の .css ファイルが正しくブラウザに読み込まれるようになった。



別の回避策
どうしても "Development" 以外の "ASPNETCORE_ENVIRONMENT" 環境変数設定にて、静的 Web アセット機能を有効にする必要がある場合、手動で有効にすることはできる。


前述のとおり、"StaticWebAssetsFileProvider" ファイルプロバイダは、Environment 構成が "Development" の場合に限って、"Program.Main()" 内で呼び出される、"ConfigureWebHostDefaults()" 拡張メソッドの内部でアプリケーションに組み込まれることが、ASP.NET Core のソースコードから読み取れる。


そのソースコードを参考に、次のような実装をすることで、Environment 構成が "Development" 以外に設定されている場合でも、"StaticWebAssetsFileProvider" ファイルプロバイダをアプリケーションに組み込むことが可能だ。
// Program.cs
...
public class Program
{
  ...
  public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
      .ConfigureWebHostDefaults(webBuilder =>
      {
        webBuilder.UseStartup<Startup>();


        // 👇 この "ConfigureAppConfiguration(...)" メソッド呼び出しを追加
        webBuilder.ConfigureAppConfiguration((ctx, cb) =>
        {
          // 👇 このアプリが開発環境で動作していることを示す
          //    何らかの条件式を決めて実装する。
          //    Environment 構成が "Development" の場合は除外することに注意
          if (!ctx.HostingEnvironment.IsDevelopment() &&
               /* ここに条件式 */)
          {
            // 👇 この呼び出しによって "StaticWebAssetsFileProvider" が
            //    静的ファイルミドルウェアに使用されるようになる。
            StaticWebAssetsLoader.UseStaticWebAssets(
              ctx.HostingEnvironment, 
              ctx.Configuration);
          }
        });
        ...



まとめ
Blazor Server アプリ、および Blazor Wasm ASP.NET Core ホストでは、Environment 構成が "Development" の場合にのみ、静的 Web アセット機能を実現している "StaticWebAssetsFileProvider" ファイルプロバイダが組み込まれる。


Razor コンポーネントライブラリパッケージを使用している ASP.NET Core Blazor アプリを開発している最中に、"HTTP 404 '_content/Foo/Bar.css' not found" といったエラーが発生した場合は、"ASPNETCORE_ENVIRONMENT" 環境変数が "Development" になっているかどうかを確認するのがよいかもしれない。


"Development" 以外の値を "ASPNETCORE_ENVIRONMENT" 環境変数に設定しつつ、その状態で Visual Studio IDE または「dotnet run」で実行する必要がある場合は、Program.cs 中に "StaticWebAssetsLoader.UseStaticWebAssets(...)" メソッドの呼び出しを追加することで回避することも可能だ。





EntityFramework Core のエンティティを名前変更したら、テーブル削除/新しい名前でテーブル新規作成のマイグレーションコードが生成されてしまった

$
0
0
C# による、リレーショナルデータベースにアクセスするプログラムを実装していての話。



SQL Server などのリレーショナルデータベースにアクセスする C# プログラムを作成する際、採用しているデータベースアクセスライブラリは、もうここ数年は EntityFramework Core (以下 EFCore) ばかりである。


さてそんな、EFCore を使っての、データベース構造を変更していく作業を進めていた中で、それは起きた。


開発途中、エンティティ名変更の要件が発生
まずとあるプログラムの開発着手当初、下記のようなエンティティを作成していた。


まずエンティティ型として以下のような "Person" クラスを用意し、
public class Person {
  public int Id { get; set; }
  public string Name { get; set; }
}

そしてこの "Person" クラスをエンティティ型としたデータセットプロパティを、データベースコンテキストクラスに追加しておく。
public class MyDbContext : DbContext {
  public DbSet<Person> People { get; set; }
  ...

しばしこの構造で開発を進めていたが、その後、このエンティティ型 "Person" を "BusinessPerson" に名前変更するのが良かろう、ということになった。


そこでまずクラス名を "Person" から "BusinessPerson" 変更し、
public class BusinessPerson {
  ...

データセットプロパティ名も "People" から "BusinessPeople" に変更。
public class MyDbContext : DbContext {
  public DbSet<BusinessPerson> People { get; set; }
  ...

あとは、以上のコードの変更にデータベース構造を追随させるマイグレーションコードを生成すれば OK だ。


生成されたマイグレーションコードが期待と違う!
ということで、(Visual Studio 上で作業していたので、"dotent ef" コマンドの実行ではなく) Visual Studio のパッケージマネージャコンソールウィンドウ内にて、マイグレーションコード生成のコマンド "Add-Migration ..." を実行した。
PM> Add-Migration RenamePersonToBusinessPerson

よしこれで完了、と思ったのだが、生成されたマイグレーションコードを見ると、なんと、既存テーブル ("People") を削除 ("DROP TABLE") してしまい、それから変更後の名前のテーブル ("BusinessPeople") を新規作成 ("CREATE TABLE") するコードになっていたのだ (以下、生成されたマイグレーションコードの抜粋)。
...
migrationBuilder.DropTable(
    name: "People");


migrationBuilder.CreateTable(
    name: "BusinessPeople",
...

これでは、既に People テーブルに格納されていたレコードが全滅である。


期待していたのは、あくまでもテーブル名の変更を行なうマイグレーションコードの生成である。
これはどうしたことか。


EFCore は悪くない
冷静になって考えてみると、こんなマイグレーションコードが生成されるのも無理からぬ事がわかってきた。


EFCore がエンティティ周りのコードの変更を追跡するにあたり、名前の変更であると認識するには、変更の前後での、当該エンティティの "つながり" がわかっていることが必要だ。


実際に EFCore のソースコードを確認したわけではないが、今回の振る舞いから推測するに、EFCore がコード変更の前後での "つながり" を認識するヒント要素は、以下の2つがあるのではないかと思われた。
エンティティ型のクラス名データセットプロパティ名
つまり、コード変更の前後で、上記いずれか片方でも一致するデータセットプロパティは "つながり" がある、と認識されるのではないか、ということだ。



それが今回は、上記2項目をいずれも変更してしまい、コード変更の前後でつながりがないように見えてしまうことになったので、それで EFCore は、"名前の変更" ではなく "削除""新規作成" と認識してしまっただと考えられた。


ということで、以上の推測に基づき、マイグレーションコードの生成を二段階に分けて実行することでやり直してみた。


    まず、エンティティ型のクラス名 "Person" を "BusinessPerson" に変更ここで一旦、"Add-Migration ..." 実行次に、データセットプロパティ名 "People" を "BusinessPeople" に変更最後にもう一度、"Add-Migration ..." 実行



こうすればマイグレーションコードの生成を実行するタイミングでは、"つながり" を認識するためのヒント要素のいずれか片方は一致し続けるので、今度は大丈夫なはずだ。


結果は予想どおり、テーブル名変更のデータベースマイグレーションコードが生成された。(以下抜粋)
...
migrationBuilder.RenameTable(
    name: "People",
    newName: "BusinessPeople");
...

なお、上記の手順は、先にエンティティのクラス名を変更して、次にデータセットプロパティ名の変更という順序としたが、この順序は逆にしても、各マイグレーションコード生成時に "つながり" を認識するためのヒント要素のいずれか片方は一致しているのは変わらない。


つまり、データセットプロパティ名を変更してから、エンティティのクラス名を変更しても、テーブル名変更として認識されるはずだ。


ということで、念のため、上記逆順でも試してみたが、これまた予想どおり、ちゃんとテーブル名変更のデータベースマイグレーションコードが生成された。


まとめ
ということでちょっと手間取ってはしまったが、EFCore の気持ちになって考えてみることで、どうにか解決に至ることができた。


最初は「えっ、なんでテーブル名の変更にならないの!?」とか思ってしまったが、
「プログラムは思ったとおりに動くわけではない。作られたとおりに動くだけだ。」
みたいなどこかで聞いたような格言 (?) のような顛末となった。

EntityFramework Core で、スカラー値を取得する SQL 文を実行して結果を得る

$
0
0
C# による、リレーショナルデータベース (今回は SQL Server が対象) にアクセスする、.NET Core 3.1 アプリケーションを開発していての話。



今回案件も、C# でリレーショナルデータベースを読み書きする際の定番ライブラリ EntityFramework Core (以下 EFCore) を採用。


EFCore のバージョンは 3.x だ。
EF Core のありがたさ
EFCore を使ったデータベースを読み書きするプログラムを書いていて大変助かると思う点のひとつは、データベース上のテーブルや列を、対応するクラスやそのプロパティとして記述するため、


「このテーブルは、プログラム中のどこで参照されているか?」


とか、


「この列は、プログラム中のどこで更新されているか?」


といったことが容易にわかることだ。  


もちろんそれには、Visual Studio などのような、開発環境の支援 (例えば "Code Lens" 機能とか) があってのことでもある。


とにもかくにも上記のような機能・特性のおかげで、たとえばプログラムの不具合発覚時に修正作業を行なうときや、プログラムの変更などを行なうときに、大変効率良く短時間で済ませることができる。
生の SQL 文を書きたいときもある
さてさて、そうはいっても、希とはいえ、文字列として記述された生の SQL 文を実行したいケースは発生する。


そのようなケースは、EFCore が内部で生成する SQL 文では非効率なってしまうようなクエリを実行するためといったことも多いだろう。


今回自分が遭遇したのは、「大量のレコードを一括挿入したい」といったケースだった。


本来 (?) であれば、このようなケースでは、(対象が SQL Server なので) EFCore ではなく BULK INSERT 機能を使うべき、とかいろいろ対処のしようがあろうかと思う。


また、EFCore を使うとして、Add() して SaveChanges() するのを繰り返すにしても、適宜、オブジェクト追跡を切り離しながら消化していく方法も考えられる。


※これをやらないで愚直に Add() ~ SaveChanges() を繰り返すと、1件挿入の処理時間が数十秒かかるようになったりする (いちどやらかしました)。


そもそも SQL Server データベースを格納先にしないという選択肢だってあるだろう。


ただ今回は、詳細はここでは明かせないが、なかなかに込み入った理由から、格納先は SQL Server 縛りで、EFCore でやるしかなく、また、オブジェクト追跡の切り離しもなかなか面倒であるという、レアケースにはまってしまった。


上記シチュエーションとなってしまったので、泣く泣く (?)、生の SQL 文 (INSERT 文) を実行することでレコード追加する実装とすることにした。
ExecuteSqlInterpolatedAsync
このようなケースに対応できるよう、EFCore には、"ExecuteSqlInterpolatedAsync" というメソッドが用意されている。


このメソッドを使うことで、C# の "string interpolation" (文字列補間) 機能を活用して SQL インジェクションにならないようパラメータ指定しつつ、生の SQL 文を実行することが可能だ。



例えば以下のように任意の SQL 文を実行可能だ。
var name = "Taro";
await dbContext
  .Database
  .ExecuteSqlInterpolatedAsync(
  $"INSERT INTO People(Name) VALUES({name})");

もっとも、生の SQL 文を使ってしまうと、冒頭で書いたような静的型付による恩恵を得られなくなる。
そのため、あくまでも泣く泣くであるが、"ExecuteSqlInterpolatedAsync" メソッドのお世話になることでどうにか事なきを得た。
SQL 文を実行した結果のスカラー値が欲しい!
これで一件落着...と思ったのだが、さらなる要件が発生してしまった。


先の INSERT 文の実行なのであるが、その INSERT 文実行の結果の、払い出された Id 値の取得が必要となってしまったのだ。


これを実現するためには、まずは SQL 文を変更する必要がある。


昨今の SQL Server における SQL 文では、INSERT 文実行時に、払い出された Id 値が返るようにするには、以下のように "OUTPUT INSERTED.Id" という記述を書き足せば良いそうだ。
$"INSERT INTO People(Name) OUTPUT INSERTED.Id VALUES({name})"

ここまではよい。


では上記 SQL 文を "ExecuteSqlInterpolatedAsync" に渡して実行すればそれよいのかというと、そうは上手くいかない。


"ExecuteSqlInterpolatedAsync" メソッドが返す戻り値は、SQL 文実行結果のスカラー値ではなく、SQL 文実行によって影響を受けたレコード件数であるからだ。


このケースで必要なのは、ADO.NET の層でいうところの ExecuteScalar に相当するその EFCore 版の機能・メソッドである・




ところが、ところがである。




自分が探した範囲では、どうにも、"ExecuteSqlInterpolatedAsync" に相当する、ただし SQL 文実行結果のスカラー値を返す版というメソッド・機能が、EFCore 内に見つけることができなかったのだ。


はてどうしたものか。
ADO.NET で書くの...?
ちなみに、EFCore を使用していても、その下位層では ADO.NET が動いている。


そんなこともあり、直接 ADO.NET を使用したコードでこの課題を解決することも可能だ。


可能だ... のだが、しかし、そうするとなかなかにダルいコードになってしまう(下記)。
var conn = dbContext.Database.GetDbConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO People(Name) OUTPUT INSERTED.Id VALUES(@p0)";
var p0 = cmd.CreateParameter();
p0.ParameterName = "p0";
p0.Value = "Foo";
cmd.Parameters.Add(p0);
var id = (int)await cmd.ExecuteScalarAsync();

パラメータをせっせっと自分で組み立てるしかないとか、正直、気分が乗らない。
ExecuteSqlInterpolatedAsync の実装を見てみよう
振り返って、"ExecuteSqlInterpolatedAsync" メソッドをよくよく見てみると、こいつはインスタンスメソッドではなかった。


拡張メソッドだったのである。


ということは、である。


"ExecuteSqlInterpolatedAsync" は、EFCore 深層部のプライベートな要素に依存せず、EFCore のパブリックな要素だけを相手に実装できているのではないだろか?


ということは、もしかして、"ExecuteSqlInterpolatedAsync" のスカラー値返す版の拡張メソッドを自作するのは、実は容易なのではないか?


そう思いついた。


そして幸い、EFCore は Apache 2.0 ライセンスの、GitHub 上にリポジトリを置くオープンソース製品である。


ということで、GitHub の EFCore のリポジトリを開き、"ExecuteSqlInterpolatedAsync" のソースコード (C#) を見てみた (下記リンク先)。


上記リンク先を見るとわかるように、予想どおり、"ExecuteSqlInterpolatedAsync" の実装は EFCore の非公開要素は使わずに、かなり少ない行数でできていた。


そしてその実装コード内で、IRelationalCommand インターフェースの ExecuteNonQueryAsync メソッドを呼び出して終了しているのを見つけた。


さらに調べてみると、IRelationalCommand インターフェースは、ExecuteNonQueryAsync のみならず、ExecuteReaderAsync や、そして ExecuteScalarAsync も用意されていたのだ!


以上のことから、ExecuteSqlInterpolatedAsync の実装コードをコピーしつつ、最後に ExecuteNonQueryAsync を呼び出しているところを ExecuteScalarAsync 呼び出しに差し替えた、自作の拡張メソッドを作れば、それで ExecuteSqlInterpolatedAsync のスカラー値を返す版が手に入るということだ。
オレオレ Execute"Scalar"SqlInterpolatedAsync
ということで実際に作ってみたのがこちら。
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Storage;

namespace ExecuteScalarInterpolatedSample
{
  public static class EFCoreExtensions
  {
    public static async Task<T> ExecuteScalarSqlInterpolatedAsync<T>(this DatabaseFacade database, FormattableString sql, CancellationToken cancellationToken = default)
    {
      var databaseFacade = database as IDatabaseFacadeDependenciesAccessor;
      var facadeDependencies = databaseFacade.Dependencies as IRelationalDatabaseFacadeDependencies;
      var concurentDetector = facadeDependencies.ConcurrencyDetector;
      var logger = facadeDependencies.CommandLogger;

      using (concurentDetector.EnterCriticalSection())
      {
        var rawSqlCommand = facadeDependencies.RawSqlCommandBuilder
          .Build(sql.Format, sql.GetArguments());

        return (T)await rawSqlCommand.RelationalCommand
          .ExecuteScalarAsync(
            new RelationalCommandParameterObject(
              facadeDependencies.RelationalConnection,
              rawSqlCommand.ParameterValues,
              null,
              databaseFacade.Context,
              logger),
            cancellationToken);
      }
    }
  }
}

ざっくりこれくらいの C# コードで拡張メソッドを書くことで、生の SQL 文を (C# の文字列補間を活用しつつ) 渡して実行し、実行した結果のスカラー値を戻り値として手に入れることができるようになった (下記はこの拡張メソッドを使用する例)。
var name = "Foo";
var id = await db.Database.ExecuteScalarSqlInterpolatedAsync<int>($@"
  INSERT INTO People(Name)
  OUTPUT INSERTED.Id 
  VALUES({name})");

警告が出てしまうのがちょっと残念であるが...
なお、上記コードであるが、すこうし残念なことがある。


アクセス指定子が public ではあるものの、"Internal" 名前空間に存在する要素を参照していることだ。


このせいで、EFCore の NuGet パッケージに含まれる Analyzer によって、下記 EF101 警告が発生してしまう。
EF1001: Microsoft.EntityFrameworkCore.Internal.... is an internal API that supports the Entity Framework Core infrastructure and not subject to the same compatibility standards as public APIs. It may be changed or removed without notice in any release.

警告文にもあるように、"Internal" 名前空間にある要素は、予告なく仕様変更や削除されることがありえるよ、という訳だ。


言い換えると、先の自作の拡張メソッドは、EFCore の将来バージョン、それもたかだかパッチバージョンが変わっただけ程度でも、動作しなくなるかも知れない、そういうリスクがある、ということにある。


さてところで、本記事執筆時点では、EFCore は .NET 5 リリース候補第1版 と同じ 5.0.0-rc.1.20451.13 がリリースされている。


ということで、先のプログラムを、使用フレームワークを .NET Core 3.1 から .NET 5 に変更し、EFCore も、v.3.1 ではなく v.5.0.0-rc.1.20451.13 をパッケージ参照するに書き換えて、再ビルド・再実行してみた。


すると、これは嬉しい誤算!


なんと、先の自作の拡張メソッドで参照していた、"Internal" 名前空間に棲息していた各要素は、EFCore v.5 RC1 では、いずれも独立した NuGet パッケージに切り出されて public API 扱いになったようなのだ!


つまり、少なくとも EFCore のメジャーバージョンが上がるまでは、先の自作の拡張メソッドも安泰と考えてもよいだろう、ということだ。


これで晴れて・大手を振って、"ExecuteSqlInterpolatedAsync" のスカラー値を返す版の拡張メソッドを自作・活用してもよさそうだ。


実際に完動するコンソールアプリケーションのサンプルコードも、GitHub 上に上げておいたのでご参考までに。




数 GB のファイルを HTTP で POST できるか? - 2020年 ASP.NET Core 版

$
0
0
実は11年前(!)にも試してみてたテーマ
今回は C# による Web アプリケーション開発、とりわけサーバー側実装の話。
いわゆる ASP.NET Core における話だ。


で、ASP.NET Core による Web サーバー実装で、実際のところ、いったいどれくらいのサイズのファイルをアップロード可能なのか? というのが今回のテーマだ。


実はこのテーマ、なんと 11年前 (!) に ASP.NET (Core ではない!) で試したことがあって、本ブログサイトに記事が残っている。


このテーマを、ちょっとした要件があって、2020年、ASP.NET Core で改めてやってみることになった。



で、先に結論書いておくと、

たいした工夫なしで 10 GB 超のファイルを HTTP POST できてしまった。


以下はその顛末。


ごくごく、実直な実装
さて「ブラウザ経由で、ファイルを Web サーバーにアップロードする」という命題なわけだが、今どきはブラウザもずいぶん進化した。


先の 11 年前のブログ記事だと、IE のバージョンが 8 (!) で、そもそもブラウザが 2GB までしか要求送信できないっぽいという、そんな時代だったが、近代ブラウザではそんなこともなくなっていることだろう、と想像。


加えて、JavaScript によるクライアント側実装も組み合わせるなど、いろいろなやり方があることだろう。


しかし今回は前回のブログ記事と同様、至極シンプルに、input type=file を用いた form でファイルを HTTP POST するというシナリオでやってみる。


まず、ブラウザ側に読み込まれる HTML コンテンツは以下のような感じだ。
<form method="post" action="/file" enctype="multipart/form-data">
  <input type="file" name="file" />
  <input type="submit" value="Upload" />
</form>
なんてシンプル。


で、C#、ASP.NET Core によるサーバー側実装だが、今回は MVC/API Controller 方式でいく。
Razor Pages 方式でもいけるのだろうが、いずれ取り組んでみたいということに留め、今回は扱わない。


ということで、サーバー側コントローラーを実装する。


なお、今回の興味はアップロードできるファイルサイズの大きさだけなので、アップロードされたファイルの保存方法については頓着しない。
なので、受信した内容を Stream.Null に書き捨ててもよい。

しかしそうはいっても、いちおう動作確認しやすいよう、そのままサーバー側のファイルシステムに保存するだけとしよう。



ざっくり、正味だけ抜き出すと以下のような感じだ。
public class FilesController : ControllerBase
{
  ...
  [HttpPost("/file")]
  public async Task<IActionResult> OnPostAsync(IFormFile file)
  {
    ...
    var savePath = Path.Combine(saveDir, file.FileName);
    using var fileStream = System.IO.File.Create(savePath);
    await file.CopyToAsync(fileStream);
    ...
サーバー側実装も、何の工夫も変哲もなし。
アクションメソッドにバインドされたファイルオブジェクトを、そのままファイルのストリームにコピーするだけだ。


実行してみた
さてこうして実装した ASP.NET Core Web アプリをいよいよ実行する。


dotnet CLI で開発中なら、単純に「dotnet run」で問題ない。


但し、とりわけ Windows 上で Visual Studio を使って開発している場合、ここで、この ASP.NET Core Web アプリの前段に IIS が噛んでいると、IIS 側の制約とかややこしいことになる。
まずは、IIS などのフロント側サーバーなしで、ASP.NET Core アプリ内の Kestrel サーバー直で動作するようにしよう。

Visual Studio であれば、ツールボタンで、IIS 上で動かすか、プログラムを直接実行するか選ぶことになると思うので、適宜選択されたし。



_d0079457_20534729.png




で、こうして実行した Web フォームを介して、大きめのファイルを送信してみる。


手元に 200MB 程のファイルがあったので試してみたところ、これはあっけなくエラー。
既定の設定ではファイルが大きすぎる (たかだか 200MB 程だが) と、アクションメソッドにバインドされる引数が null となってしまうようだ。


まぁ、想定内の挙動である。


リミッタ解除!
しかしこれが ASP.NET Core の限界というわけではない。
どちらかといえばセキュリティ的な観点で設定された、既定の限界値設定の結果だ。


公式ドキュメントにも記載があるが、ちょこっとコードに属性指定を書き足してやることで、この "リミッター" を明示的に解除することが可能である。


具体的には、以下のように、2つの属性指定をアクションメソッドに追記する。
  ...
  [HttpPost("/file")]
  [DisableRequestSizeLimit]
  [RequestFormLimits( MultipartBodyLengthLimit = long.MaxValue, 
      ValueLengthLimit = int.MaxValue)]
  public async Task<IActionResult> OnPostAsync(IFormFile file)
  {
    ...
さぁ、これでコード上の限界値指定は long.MaxValue、すなわち、8,192 ペタバイト (8 エクサバイト) まで広がった。


メモリがめきめき消費され... ない!?
とはいえ、実際には物理的なコンピューター資源の限界がある。


とりわけ、物理メモリ量や、そもそものメモリ空間上の限界は小さいはずだ。


そして限界値も気になるが、たかだか 1GB や 2GB 程度のファイルのアップロードだとしても、プロセスのメモリ消費量はめきめき上昇するだろう。
きっと .NET Core ランタイムのメモリマネージャが悲鳴を上げるのではないだろうか?


ということで、まずは都合良く 1.6 GB 程のファイルが手元に転がっていたので、これをアップロードしてみた。


ところが、である。


すこうし処理時間がかかったとはいえ、あっけなく処理が正常完了したのである。


さらにである。
自分はこれを Windows 10 上で実行し、タスクマネージャを開いてメモリ消費量を眺めていたのであるが、一向にメモリ消費量が増えないのである。


_d0079457_20534770.png




当初の予想を裏切られ、肩透かしを食らってしまった格好だ。


怪訝に思いながらも、じゃぁ、どれくらいのファイルサイズをアップロードしたら影響がでるのか、ファイルサイズのより大きいファイルを選んで試験を続行した。


3GB でも問題ない。


4GB でもびくともしない。


ついに 10GB でもアップロードできてしまった。


しかもタスクマネージャのメモリ消費量も一定のままである。


一体どうなっているのか??


何かファイルに猛烈に書き込みしてる...!
よくよくタスクマネージャのグラフを表示を眺めていたところ、メモリではなく、ディスクの負荷状況が興味深い動きをしていることに気がついた。


前述の C# コードが動き出すより前から、ずっと、ディスクの書き込み処理が行なわれているっぽいのだ。


アップロードする元のファイルの読み取りや、あるは前述の C# コードによる、受信したファイル内容のファイルへの書き込みであればわかる。
が、どうもそれ以外のディスク書き込みが発生しているようなのだ。


これはいったいどんなファイル書き込みが発生しているのか気になり、リソースモニターを開いて調べてみた。


そうしたところ、なんと、テンポラリフォルダの "ASPNETCORE_...." というファイルに猛烈に書き込みが発生しているのを発見した。


_d0079457_20534760.png




デバッグ実行などしながら確認してみると、どうやら、ブラウザから送信された巨大な POST 要求は、まずはこの一時ファイルに保存されるようだ。



そしてブラウザからの POST 送信がすべて着信終了した・この一時ファイルへの保存が完了してから、その次に、コントローラーのアクションメソッドの実行が始まるようなのである。


すなわち、前述の C# 実装における、IFormFile から読み取れるストリームは、先の一時ファイルにつながっているようなのだ。


何でもメモリではなく、一時ファイルを活用しているっぽい
自分は Web Forms の頃から ASP.NET に触っていたため、ついうっかり、アクションメソッドにバインドされる引数オブジェクト類は、すべてオンメモリ上にバッファリングして構築されてからアクションメソッドに引き渡されると思い込んでしまっていた。


しかしながら、ASP.NET Core では、オンメモリ上に何でもバッファリングしてしまうのではなく、ファイルシステムを使って一時ファイルを作ることで、巨大な要求を処理可能としているようだ。


ということで、ファイルシステム上の一時ファイルが肩代わりしてくれるおかげで、処理速度的には難ありとは思うが、とにかくクラッシュしたりメモリをバカ喰いすることなく、10GB 超のファイルを、何の工夫もないシンプルな Web フォームおよび ASP.NET Core コントローラー実装にて捌くことができることがわかった。


IIS のリミッタも緩和
さて、Windows 上の Web サーバーサービスである IIS が絡む場合。


とくに Azure Web Apps の Windows プラットフォーム上へのこのような ASP.NET Core アプリを配置する場合は、IIS による制限も避けられない。


Windows 上で Visual Studio を使って開発している場合は、Visual Studio のツールバー上から、IIS 上での実行を選択してから実行すれば、IIS 上での実行を確かめられる。


_d0079457_20534768.png




すると、C# 側実装は前述のように [DisableRequestSizeLimit] 等のリミッタ解除属性を指定していても、その前段の IIS の限界値に触れて、IIS のエラーになってしまう。


_d0079457_20534833.png




IIS 側の限界値設定は、web.config ファイルをコンテンツルートフォルダに配置することで、設定変更可能だ。


具体的には下記内容の web.config ファイルを配置する。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="4294967295"/>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>
こうすることで、試してみたところ、4GB までのファイルをアップロードすることができるようになった。


なお、上記 web.config の設定上、maxAllowedContentLength = 受信できる要求サイズの限界値は、32bit の符合無し整数値による指定が限界らしい。


すなわち、IIS 経由の場合のアップロード可能なファイルサイズは最大サイズは 4GB までということだ。
(もちろん、単純な HTTP POST x 1発ではなく、何かしらのストリーミングやチャンク分割しての送信など他の手法であればこの限界は回避できるだろうが、このトピックでは HTTP POST x 1発縛りということで。)


さらなる課題...
この IIS の制約をさらに緩和できるのか、IIS の (現状の "統合パイプラインモード" ではなく) "クラシックモード" だったどうなるのか、などは今のところ未確認である。


その他、実際にこのアプリを Azure Web Apps に載せたらどうなるのかも気になるところだ。


さらには、このように ASP.NET Core ランタイムによしなにお願いするだけではなく、それらランタイムの処理は迂回して、時前の C# コードでブラウザからの要求送信ストリームを扱ったらどうなるのだろう、という点も気になる。


いろいろとさらに調べたいことが山積みのままであるが、とにかく、何の工夫もない素の ASP.NET Core MVC/API コントローラースタイルのサーバー側実装であっても、属性指定してリミッタ解除するだけで、これくらいのサイズの要求処理ができるということはわかった。


引き続き、残された興味課題を消化できたら、またここで情報共有できたらいいなと思っている。
とりあえず、今回作成した ASP.NET Core プログラム一式は下記に公開してあるので、ご参考までに。




ということで、来月に続く。(続かない?)




続・数 GB のファイルを HTTP で POST できるか? - 2020年 ASP.NET Core 版

$
0
0
10GB 超でも POST できた、けど、効率はよくない前回のブログ記事にて、何の工夫もない input type=file な form から、これまた何の工夫もない ASP.NET Core コントローラーへ、10GB を超えるファイルを HTTP POST 送信し、ファイル保存できたことを記した。


その仕掛けとしては、POST されてきた内容をいったんサーバー側一時ファイルとして保存したうえで、サーバー側コントローラーへ、その一時ファイルからのストリームを引き渡すことで実現されていた。


これはこれでよくできた仕掛けではある。しかし効率の観点からは、ASP.NET Core ランタイムによって一時ファイルにいったん保存されるのは無駄が多い。


つまり、クライアント (ブラウザ) からネットワーク越しに送信されてきた要求内容が、サーバー側でいったん一時ファイルに保存されたあと、その一時ファイルを再びサーバー側内で読み直して、同じサーバー上の別のファイルに複製している格好となるからだ。


クライアントから送信されたファイルを、サーバー上のファイルシステムではなく、例えば Azure Storage Blob などに保存する場合も考えても、クライアントからの要求をそのまま Blob に注ぎ込めば良さそうなところを、いったんサーバー側一時ファイルに保存する処理が挟まるのでやはり非効率だ。


当然、いったん一時ファイルに保存し終えるまでの時間が余計にかかることになる。


ということで、HTTP POST 要求をストリーム処理してみよう幸い、この非効率さを回避することはそんなに難しくない。


前回記事と同じく API コントローラー方式で実装する場合であっても、実はそのものズバリの解説が、下記、公式ドキュメントに掲載されている。


要点としては、以下の二点だろう。



メモリや一時ファイルにバッファリングさせずに自前で要求ストリームを読み取るため、モデルバインディングをしないようにする
(そのためのフィルタ属性を自前で実装し、コントローラーのアクションメソッドに付記する)
 multipart/form-data な HTTP 要求ストリームをパースし、同要求中のセクションごとの Stream を得る
(ASP.NET Core 提供の MultipartReader クラスが使える。但しバウンダリ検出などの一部機能は自前でゴリゴリと実装する必要はある)



多少は自前でコードを書かないといけないが、幸い、上記公式ドキュメントに掲載のコードをコピー & ペーストで済ませることができる。



実際に、前回のブログ記事で作成したサンプルプログラムを、上記ストリーム方式に改造してみた結果を、streming-edition ブランチとして公開してある。


以上のとおり、公式ドキュメントに掲載の手順に従うだけで、一時ファイル方式ではなく、要求ストリームを直接読み取る方式に改善できた。



Azure App Services に配置し、1.6GB のアップロードに成功前回のブログ記事を含め、ここまではローカル開発環境での話だ。


ではこのような ASP.NET Core アプリを、クラウド上の PaaS に配置したらどうなるだろうか。


今回はパブリッククラウドサービスのひとつ、Microsoft Azure の、App Services という PaaS に配置してみた。


Azure App Services への配置にあたり、アップロードされたファイルの保存先として、当該 PaaS の OS インスタンス内のファイルシステムではなく、同じく Microsoft Azure の Blob Storage サービスに保存するようにプログラムを変更した。(azure-blob-edition ブランチとして公開)




また、配置先 App Services の OS プラットフォームは (今回は Linux ではなく) Windows を選択した。


こうして Azure App Services 上に配置したこのサンプルプログラムを稼働させ、実際にインターネット越しに、まずは 1.6 GB 程の適当なファイルをアップロードしてみた。


結果、無事アップロード成功。


アップロードしたファイルは期待どおり Blob ストレージに格納されたことも確認できた。


3GB は失敗、サイズよりも時間が問題?これに気をよくして、では、3GB 少々のファイルのアップロードを試してみることにした。


なお、Windows 版の Azure App Services であれば、ASP.NET Core Web アプリの前段に IIS が居るはず。  
で、前回のブログ記事でも触れたように IIS 側での要求サイズの制限があり、今のところ自分の知る範囲では 4GB までしか拡張できないっぽい。


とはいえ、今試そうとしているのは、その IIS の限界である 4GB より小さい、3GB ほどのファイルなので、アップロードは成功するはずだ。


ということで、実際にやってみたのであるが...


まず、自分が利用しているインターネット接続サービス回線上の問題なのか、なかなかアップロードが完了しない。


そして待つこと 40 分 (!) にして、TCP 接続が絶たれてしまうというエラーになってしまった。
HTTP ステータス 5xx が返るのでもなく、ERR_CONNECTION_RESET なのである。


_d0079457_15175055.png


回線上の問題かと思い、もう一度実行。


再び待つこと 40 分 (!)。
今度は IIS による HTTP 502 エラーページが表示されてしまった。

_d0079457_15175448.png



結局、4GB 未満であるにもかかわらず 3GB 少々のファイルアップロードに失敗してしまう原因・理由は掴み切れていない。
ただ、どうにも、IIS による 4GB 制限ではなく、もっと別の理由がありそうな雰囲気である。
とりわけ自分が使っている回線ではずいぶんと時間がかかってしまっている点が気になるところである。


まとめコントローラー方式による HTTP POST 要求処理実装であっても、要求をメモリや一時ファイルにバッファリングさせることなく、自前のコードで要求ストリームを読み取り、そのままファイルシステムや外部ストレージに書き出すことができることがわかった。


もちろん、コントローラー方式ではなく、要求ハンドラ (Startup クラスの Configure メソッド内で、app.Map(...) みたいに登録するやつ) を実装することでも実現可能なことだろう。


なお、そのような巨大な HTTP POST 要求を処理可能な ASP.NET Core Web アプリであっても、Microsoft Azure などのパブリッククラウド上に配置する場合は、それらクラウドサービスのプラットフォーム上の制約を受けることにる。
例えば IIS による 4GB 制限や、処理時間の上限 (Azure App Services だと 230秒) といった制約があることだろう。


そのようなプラットフォーム由来の制約があったり、それほど巨大な HTTP POST 要求には時間もかかり、実際問題として送信失敗した実績を考えると、実際上としては、こんな HTTP POST 要求 x 1発の Web フォーム送信はあまり現実的ではないことだろう。


どうしても巨大な何かをサーバーに送り込まなくてはならない場合は、クライアント側スクリプトを駆使してチャンクに小分けしながら送信するべきだろう。
できることなら、チャンク送信失敗時には再送信もできるようにまで作り込みするのが、より現実的なアプリケーションだと思われる。


ということで、では前回・今回のブログ記事はなんだったんだ、意味がないのかというと、そうでもないだろう。
まぁ、GB 級の要求送信となれば上記のとおりしっかり作り込むべきと思う。
そのいっぽうで、数百 MB 程度のファイルアップロード的な案件であれば、本ブログ記事のとおりの HTTP POST x 1 発の実装でも充分なこともあるだろう。


相変わらずニッチなテーマ、シナリオであるが、以上共有までに。


自己完結 + 単一ファイル生成 を指定して発行した .NET 5 Windows Forms アプリに、ビジュアルスタイルが適用されない

$
0
0
配置先に .NET のインストールが不要の「自己完結」配置モード
Windows OS 上で Visual Studio を使っての、Windows Forms アプリの開発における話。


ついに .NET 5 が公式リリースとなったことを踏まえ、手持ちの .NET Framework ベースで作成されている Windows Forms アプリを .NET 5 に移植を開始した。


そして、.NET Core 3.1 以降、さらに改善された単一ファイルへの発行を試すことにした。


さらについでに、配置先での事前の .NET ランタイムのインストールが不要である、「自己完結」の配置モードでの発行とすることにした。


Visual Studio 上で発行プロファイルを設定している様子は下図のとおり。


_d0079457_21180657.png



見た目がなんだか古くさいぞ
これで発行を実行してみると、すんなり終了。


では発行された実行ファイル (.exe) を実行してみると...


あれ?


なんか、見た目がいつもと違わないですか? (下図)


_d0079457_21180766.png



20年くらい前にはこんな外観をよく見てた気もするが...


あらためて、さっきまでデバッグ実行していた実行ファイルで実行してみると、たしかに見た目が違う! (下図)


_d0079457_21180783.png



この外観は、そう、Windows Forms アプリの Main 関数の冒頭で「Application.EnableVisualStyles()」を呼び出さなかった場合と同じ、ビジュアルスタイルが適用されなかったときの外観である。


しかしこれは一体どういうこと?


Windows Forms のソースコードを追ってみた
一体どうしてこんなことになるのか、Windows Forms のソースコードを追ってみた。


それでわかったのは、「Application.EnableVisualStyles()」が行なっているのは、System.Windows.Forms.dll に Win32 リソース形式で埋め込まれているアプリケーションマニフェストファイル (.manifest) のコンテンツを取得し、Win32 API 呼び出しを実施して、ビジュアルスタイルを適用している、ということだった。


ところが、自己完結と単一ファイル生成を組み合わせて出来上がった .NET 5 Windows Forms アプリケーションでは、System.Windows.Forms.dll も .exe ファイルに埋め込まれてしまっている関係で、目的のアプリケーションマニフェストファイルの Win32 リソースにたどり着けないらしい。


それでビジュアルスタイルの有効化に失敗する模様だ。


このような仕掛けなので、
単一ファイル生成であっても、配置モードが「フレームワーク依存」、つまり、System.Windows.Forms.dll が実行ファイル (.exe) に埋没していない場合
("C:\Porogram Files\..." のどこかに配置されているハズ)

あるいはまた、配置モードが「自己完結」であっても、単一ファイル生成が OFF、つまり、System.Windows.Forms.dll が発行先フォルダに独立して存在する場合
 は、ちゃんとビジュアルスタイルが適用された。


暫定回避策
それではということで、ビジュアルスタイルを有効化するアプリケーションマニフェストファイルを自分で作成して、これを明示的に自分のアプリに埋め込むよう設定することにした。


まず、プロジェクトのフォルダに、「App.manifest」ファイル (※拡張子は .manifest がいいだろうけど、ファイル名は何でも良いはず) を作成し、中に以下の XML を記載する。

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <description>Windows Forms Common Control manifest</description>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32"
        name="Microsoft.Windows.Common-Controls" version="6.0.0.0"
        processorArchitecture="*"
        publicKeyToken="6595b64144ccf1df"
        language="*" />
    </dependentAssembly>
  </dependency>
</assembly>

次に、Visual Studio で開発中なら、プロジェクトのプロパティ画面から [アプリケーション] カテゴリの [アイコンとマニフェスト] の [マニフェスト] のドロップダウンを開き、そこに、今作成した「App.manifest」が選択肢にあがっているはずなので、これを選択する。


_d0079457_21180725.png



あるいはまた、Visual Studio のプロジェクトプロパティ画面を経由せずとも、.csproj を直接編集して、自作の「App.manifest」ファイルの使用を指定しても良い。

<Project Sdk="Microsoft.NET.Sdk">


  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>


    <!-- 👇 この行を追加 -->
    <ApplicationManifest>App.manifest</ApplicationManifest>


  </PropertyGroup>


</Project>

以上でビルド・発行すると、無事、自己完結 + 単一ファイル生成を指定して生成された実行ファイルでも、ちゃんとビジュアルスタイルが適用されて実行されるようになった。


_d0079457_21180783.png



次の修正リリースで直るっぽい?
...というように、とりあえず暫定回避できて良かった、としたその翌日。


さてこれは Issue でも挙げなきゃだめだろうか、あるいはもうすでに Issue あがっているのでは、と思いながら、Windows Forms の GitHub リポジトリを漁ってみたところ、案の定、既にこの問題で Issue があがっていた。


当該 Issue を見ると、既に、.NET 5 のリリース候補第2版の時点で、この問題が認識されていた模様。



残念ながら 2020年 11 月の、.NET 5 初回公式リリースには修正を盛り込むのが間に合わなかったのだろうか。


もしこの先、.NET 5 の修正リリースが出ることがあれば、そのときにはきっと直っていることを期待して良さそうだ。



.NET の単体テストを並列実行する - テストフレームワークごとの違い

$
0
0
C# など .NET プログラム開発における、単体テストプロジェクトの実装・実行の話題。



自分が抱えているとある案件の単体テストプロジェクトにおいて、単体テストプロジェクト内の複数のテスト (テストメソッドや、テストパラメータ) を、1つずつ順番に実行するのではなく、並列実行したい状況が発生した。


そこで、単体テストフレームワークごとに、テストの並列実行のやり方についてどのような違いがあるか、確認してみることにした。


なお、自分が扱える単体テストフレームワークは以下の3つ。
xUnit.netNUnitMSTest
上記 3 つの単体テストフレームワークごとの、テストの並列実行の違いを、Windows OS 上の Visual Studio 2019 のテストエクスプローラからの実行で調べてみた。



調査用に作成した C# による単体テストフレームワークごとプロジェクト一式は、下記 GitHub リポジトリに公開してある。


そして下記の表は、調べてみた結果を雑にまとめてみたもの。
★ 印はそれが既定の振る舞いであること、○ 印はその並列実行をサポートしていることを示している。


 xUnit.netMSTestNUnit並列実行なし○★★クラススコープでの並列実行★○○メソッドスコープでの並列実行○○パラメータスコープでの並列実行○
以下、上記表の並列実行のスコープについて、もう少し説明してみる。
並列実行なしこれは単純に、すべてのテストメソッド、テストパラメータが、順番にひとつずつ実行される形態である。


MSTest および NUnit では、特になにも指定しない場合の並列実行に関する動作はこれである。
クラススコープでの並列実行異なるテストクラスのテストメソッドが並列実行される形態である。
すなわち、
  テストクラス A にテストメソッド X があり、
  テストクラス B にテストメソッド Z があるとき、
  別々のクラスにあるテストメソッド X と Z は並列実行される



というものだ。


ただし、テストクラス A 内のテストメソッド X と Y は、(これらのテストメソッドは同じクラス内なので) 並列実行されない。


xUnit.net では、このクラススコープの並列実行が既定の動作であるようだ。
および、MSTest と NUnit でも、追加の指定が必要であるが、クラススコープでの並列実行が可能である。
メソッドスコープでの並列実行異なるテストクラスももちろん、同一テストクラス内のテストメソッドも並列実行される形態である。
すなわち、
テストクラス A にテストメソッド X と Y があり、
テストクラス B にテストメソッド Z があるとき、
これらすべてのテストメソッド X, Y, Z は並列実行される
というものだ。


xUnit.net ではメソッドスコープでの並列実行はサポートされていないらしい。
いっぽう、MSTest と NUnit では、追加の指定で、メソッドスコープでの並列実行が可能だ。


パラメータスコープでの並列実行今回評価したいずれの単体テストフレームワークにおいても、「テストメソッドに引数を設け、引数に渡す値 (テストパラメータ) を変えながらテストメソッドを実行する」という、いわゆる "データ駆動テスト" に対応している。


パラメータスコープでの並列実行は、このデータ駆動テストにおいて、引数に渡す値 (テストパラメータ) 違いのテスト実行が並列実行されるというものだ。


このスコープでの並列実行は、NUnit のみが対応している模様。
フレームワーク別にもう少し補足xUnit.net並列実行の "スコープ" はクラスレベルまでしかできないっぽい。
その他の並列実行に関する構成は、[Collection] 属性をテストクラスに付与して "テストコレクション" という並列実行の "単位" を個別にくくりだしたり、アセンブリ属性 [CollectionBehavior] にて並列実行のスレッド数を指定したりなどができるようだ。
詳細はいずれも公式ドキュメント (下記) に記載がある。


NUnit並列実行のスコープを、テストケースやテストメソッドに [Parallelizable] 属性を付記することで制御できる。


ただ、[Parallelizable] 属性の指定を細やかに組み合わせようとすると、どれとどれが並列実行されるのか事前の予想がしにくかった。 (慣れの問題?)


MSTest並列実行のスコープを アセンブリ属性 [Parallelizable] で指定できる。

指定はアセンブリ単位だが、[DoNotParallelize] 属性をテストクラスまたはテストメソッドに付けることで、テストクラスやテストメソッドごとに個別に並列実行しない指定が可能だ。



Visual Studio の [テストを並列で実行する] の効果は?ところで、Visual Studio IDE のテストエクスプローラの設定には、[テストを並列で実行する] というチェック項目がある (下図)。


_d0079457_20044151.png



しかしながら、前述のテストフレームワークごとの並列実行に関する振る舞いには、このチェック項目の設定は何ら影響しない様子である。


ではこのチェック項目はどういう効能があるかというと、現在開いているソリューションに、複数のテストプロジェクトが含まれていた場合に、それらテストプロジェクトのテスト実行を並列実行するかどうかを制御している模様だ。


言うなれば、"アセンブリ" スコープでの並列実行を選択できる項目ということだ。


まとめ
以上、ざざっとテストフレームワークごとの並列実行の指定の方法やその性質について調べてみた。


自分の確認不足や誤認などで、本投稿の内容には誤りもあるかもしれないので、その点は注意していただきたい。


なお、テストは何でもかんでも並列実行すればよいというものではないだろう。


いくらマルチコアが一般的な今日現在とはいえ、テスト対象のコードが搭載コアをフル活用するような CPU 計算中心なコードであった場合などは、かえって並列実行するとトータルのテスト実行時間は長くなってしまう可能性もある。
もしかしたら、CPU の温度が上がりすぎてサーマルスロットリングが発動し、かえって遅くなった、なんてこともあるかもしれない。


他方、I/O 処理待ちなどの非同期処理を含むようなケースでは (※単体テストでそのようなコードを対象としたテストは希だと思うが)、テストを並列実行することで、トータルのテスト実行時間を短縮化できる可能性が高くなる。


自分のケースだが、データ駆動テストのテストケースが大量な xUnit.net で実装したテストプロジェクトがあって、これのトータルのテスト実行時間が 125秒 かかっていた。
これを  (xUnitではパラメータ違いのテストを並列実行できないっぽかったので) NUnit に載せ替えて並列実行化したところ、73秒にまで短縮できた。


上記はテストの並列実行によってトータルのテスト実行時間を短縮できた成功例ではあるが、当然、すべての単体テストプロジェクトでこのとおりうまくいくとは限らない。
また、本稿では端折ったが、並列実行できるようなテストコードを実装する必要も出てくる。
テストの並列実行は、よくよく考えて、そして実行結果を測定しながら進めるのがよろしかろう。



.NET 5.0 (あるいは .NET Core) における、DateTime.Now の分解能を測ってみた

$
0
0
事の発端
.NET 5.0 実行環境における、C# を使ったプログラミングでの話。


とある案件で、1桁ミリ秒台ごとに発生するイベントを、その発生日時とともに記録する必要に迫られた。


当方、10年来の C# プログラマの感覚だと、ある基準時点からの経過時間で記録するだけであれば、System.Diagnostics.Stopwatch クラスを使うところだ。


しかしながら今回の要件としては、
「そのイベントが、現実世界における何時何分何秒に起きたか」
をログ記録する必要があった。


このようなケースなら、お手軽に、System.DateTime クラスの Now (ないしは UtcNow) 静的プロパティでイベント発生時点の日時を記録するだけにして、安易に済ませたいものだ。


とりわけ、System.DateTime.Now であれば、NTP による OS の時刻同期の恩恵にもあずかれる。
補足すると、今回案件は、原子時計レベルの精度 (Accuracy) を要求しているわけではなく、むしろ多少の揺らぎがあっても、NTP による自動時刻調整が機能していたほうが好ましいケースであった。


分解能 (Resolution) が焦点に
しかしである。


イベントの発生間隔は1桁ミリ秒ごとである。


前述のように、あまり正確性 (Accuracy) は要求されないのではあるが、しかし、System.DateTime.Now の分解能 (又は解像度、Resolution) が低すぎると (おおざっぱ過ぎると)、今回案件の要求を満たすことができなくなってしまう。


例えば、System.DateTime.Now の分解能が、もしも 15ミリ秒単位だったとすると、今回案件に System.DateTime.Now をそのまま使うにはちょっと厳しいのではないか、という状況であった。


公式サイトには明記されていない...
ちなみに、System.DateTime が表現できる "精度 (Precision)" は 100 ナノ秒単位であることが、仕様として公式ドキュメントサイト (下記) に掲載がある。
(今回案件ではここまでの精度 (Precision) は使わないが)

(引用) 時刻値は、ティックと呼ばれる100ナノ秒単位で測定されます。

しかしいっぽう、System.DateTime.Now の分解能 (Resolution) については「実行環境に依存する」ということだけ記されていて (下記)、具体的にいかほどの分解能になるかは記載を見つけられなかった。


(引用) このプロパティの解決策は、基になるオペレーティングシステムに依存するシステムタイマーによって異なります。

ネットで見つけた記事は古い...
それではということで、ネットで日本語で検索してみると、古い記事がいくつかヒットした。
それら記事に依れば、System.DateTime.Now の分解能はせいぜいが数十ミリ秒との記載であった。


だがしかし、それら記事が言及していたのは .NET Framework 上における話であった。


今は .NET 5.0 の時代である。
PC 本体の造りも当時とは随分異なっていることだろう。


では .NET 5.0 における System.DateTime.Now の分解能はいかほどのものだろうか?


実際に測ってみた
.NET 5.0 でも、System.DateTime.Now の分解能は実行環境に依存するのは変わりないとのことで、また、ネットで検索してもすぐには答えを得られなかったので、ここはひとつ、ささっと小さなプログラムを書いて実行し、自分の周りの環境ではどうなのか、「実測」してみた。


計測のために書いた C# プログラムは、ざっくりこんな感じ。


using System;
using System.IO;
using System.Linq;


const int SampleSize = 30000;


// 計測
var buff = new DateTime[SampleSize];
for (var i = 0; i < buff.Length; i++)
{
    buff[i] = DateTime.UtcNow;
}


// 計測結果をテキストファイルに出力
var outputPath = Path.Combine(AppContext.BaseDirectory, "result.txt");
using var f = File.CreateText(outputPath);
var prevGroup = default(IGrouping<DateTime, DateTime>);
foreach (var group in buff.GroupBy(d => d))
{
    var delta = prevGroup != null ? (group.Key - prevGroup.Key) : TimeSpan.Zero;
    prevGroup = group;
    var line = $"{group.Key:HH:mm:ss.fffffff}, {delta.TotalMilliseconds,3}, {group.Count()}";
    f.WriteLine(line);
}
このプログラムを実行 (dotnet run -c:Release) すると、プログラムファイルがあるのと同じフォルダに、"result.txt" というテキストファイルができあがる。


このテキストファイルの内容は、System.DateTime.Now をひたすら 3万回収集した結果を、同値はグループ化して集計したものだ。


このプログラムを、自分の手持ちの PC (Windows 10 64bit) 上で実行した結果の "result.txt" の一部を下記に記す。


ToString した結果, 前行からの経過ミリ秒, 同値が繰り返し計測された回数
11:47:29.8110822, 0.0001, 2
11:47:29.8110835, 0.0013, 1
11:47:29.8110836, 0.0001, 1
11:47:29.8110837, 0.0001, 1
11:47:29.8110838, 0.0001, 2
11:47:29.8110839, 0.0001, 1
...

結論としては、自分が計測した限りでは上記のとおり、.NET 5.0 の System.DateTime.Now の分解能は、おおよそ1桁マイクロ秒台であった。


前述の案件については、もう、充分過ぎる分解能である。


なお、.NET 5.0 ではなく、.NET Core 3.1 でも試してみたが、同じ結果となった。


さらに、.NET Framework 4.5 でも試してみたところ、分解能は概ね 1ミリ秒という結果が得られた。
マイクロ秒台ではなかったとはいえ、ネットで見つけた過去記事よりは、高分解能な結果となっている。
.NET Core ベースか、.NET Framework か、というランタイムの違いからの差ももちろんだが、この測定プログラムを実行した自分の PC ハードウェアの世代がより近代になっていることも関係しているのだろうかと、想像するところである。


まとめ
ということで、要件にもよるし、実行環境によっては違った結果になる可能性あるが、.NET 5.0 の System.DateTime.Now は、かなりの高分解能 (Resolution) で使えるらしい、ということが実際に測ってみてわかった。


なお、自分の手元の PC 環境以外では、今回の計測と同じ程度に高分解能な結果がでるのかどうかは不明。
例えば Linux 環境であるとか macOS とかではどの程度であるか、とか、むしろ OS 種類よりもハードウェア側の対応に影響されそうな気もするので、例えば Raspberry Pi 上しかもそれらの世代間では違いがあるのか、といった事についても、気にはなるが、現状不明である。


ちなみに、上記測定プログラムを、Azure App Services の Windows インスタンス/Free (F1) プランにデプロイして実行してみたところ、今回の測定結果と同じ程度の高分解能な結果が得られた。


最後に。
そもそもこんなプログラムでちゃんとした計測になるのかどうか、あまりこの手のベンチマーク的なプログラミングになじみがないため、正直、自分でもかなり心許ない。
もし何か気づいたところがあれば、本記事へのコメントや Twitter などで指摘いただけるとありがたい。


SkiaSharp を使って .ico を読み込むんだけどその前に、.ico に含まれるアイコン画像のサイズを把握する話

$
0
0
プロローグ
C# によるプログラミングでの話。


今回は、.ico 形式の画像ファイルを、.png 形式の画像ファイルに変換する要件が発生した。


そのような要件では、とりわけ Windows のデスクトップ向けエディション上での実行でよければ、System.Drawing.Icon クラスで実現可能だ。


しかし今回は訳あって、System.Drawing.Icon が使えない縛りが発生。


ということで、今まで使ったことのなかった "SkiaSharp" を使わせてもらうことにした。


SkiaSharp で .ico ファイルを .png に変換
"SkiaSharp" とは、クラスプラットフォームな 2 次元コンピュータグラフィクスライブラリの名前。


そもそもは、現在は Google が開発を継続している "Skia" という C++ によるオープンソースライブラリが本体で (修正 BSD ライセンス。公式サイトは下記)、

この Skia に対する .NET プラットフォーム用のバインディングが "SkiaSharp" である (Microsoft のドキュメントサイトは下記。MIT ライセンス)。



この SkiaSharp を使うことで、C# にて各種 2 次元コンピュータグラフィクスのプログラミングを実装可能となる。



さて、今回の命題は .ico 形式のファイルの、.png 形式画像ファイルへの変換である。
これを SkiaSharp を使って実装するには、下記のような C# コードとなる。
// 別途、プロジェクトに "SkiaSharp NuGet" パッケージへの参照を追加しておくこと
using System.IO;
using SkiaSharp;
...
// .ico ファイルを SKBitmap オブジェクトに読み込み
using var iconStream = File.OpenRead("path/to/file.ico");
using var bitmap = SKBitmap.Decode(iconStream);


// その SKBitmap オブジェクトを、PNG 形式でファイルに書き込み
using var pngStream = File.Creste("path/to/file.png");
bitmap.Encode(pngStream, SKEncodedImageFormat.Png, quality: 100);
...
実にスッキリ、素直な実装になってありがたい。


追加要件発生 - 48 x 48 ピクセルで変換したい!
さてところで、.ico 形式のファイルというのは、独特のバイナリフォーマットによる背景透過画像を保存したファイルなのだが、もうひとつ面白い特徴として、複数のピクセルサイズの画像をひとつの .ico ファイル内に収録している、という点がある。


先の SkiaSharp による実装では、自分で軽く試してみた範囲では、読み込む .ico ファイルに含まれているうち、最大ピクセルサイズの画像が読み取られるようだ。


と、ここで、新たな要件が追加となった。


その要件とは、.ico から .png へのファイル変換にあたり、.ico ファイルから 48 x 48 ピクセルの画像を優先して取り出して、.png に変換せよ、というものだ。


もし .ico ファイル中に、ぴったり 48 x 48 ピクセルの画像が含まれていない場合は、なるべく 48 x 48 ピクセルに近い、それより大きいピクセルサイズの画像を使う。


さらにもし 48 x 48 ピクセルより大きいピクセルサイズの画像が含まれていなかった場合は、48 x 48 ピクセル未満で構わないので、なるべくピクセルサイズの大きな画像を使う。


そしていずれのケースでも、最終的に保存する .png ファイルのピクセルサイズは、48 x 48 ピクセルとする、というものだ。




なんともややこしいが、砕いて言えば、
「任意のピクセルサイズを詰め合わせた .ico ファイルから、48 x 48 ピクセルの .png ファイルへ、なるべく高品質に変換したい」
ということだ。


.ico に含まれるうち、指定サイズの画像を読み取る
では前述の SkiaSharp で、.ico ファイルから指定したピクセルサイズの画像を取得するにはどうするか?


実は、.ico ファイルを SKBitmap オブジェクトに読み込むための SKBitmap.Decode() 静的メソッドは、追加の引数で希望するピクセルサイズを指定できる。


下記は 48 x 48 ピクセルの画像を .ico から読み取る例だ。
var imageInfo = new SKImageInfo(width:48, height:48);
using var bitmap = SKBitmap.Decode(iconStream, imageInfo);
こんな感じで、.ico ファイル中に含まれている画像群のうち、指定のピクセルサイズの画像を SKBitmap オブジェクトに読み取りできる。


だが安心するにはまだ早い。


もしも、上記コード例で、読み取り対象の .ico ファイル中に、(32 x 32 ピクセルや、64 x 64 ピクセルなどの画像は含まれていても) 48 x 48 ピクセルの画像が含まれていなかったらどうなるか?


答えは、SKBitmap.Decode() 静的メソッドの戻り値が null になる、である。


ということは、だ、前述の追加要件のアルゴリズムを実装するには、読み取り対象の .ico ファイルに収録されているすべての画像のピクセルサイズを、事前に把握する必要がある。


.ico ファイル中に含まれている画像に対して最適なピクセルサイズがわかっていないと、.ico 中の画像を読み取れないからだ。




.ico に含まれるすべての画像のピクセルサイズを知りたい
では SkiaSharp で、.ico ファイル中に含まれている画像のピクセルサイズ一式を知るにはどうしたらよいか?


なんと困ったことに、どうも自分が調べた範囲では、SkiaSharp で、.ico ファイル中に含まれている画像のピクセルサイズ一式を収集する手段が公開されていないようなのである。


SKBitmap.Decode() 静的メソッドで、ピクセルサイズ指定で、希望のピクセルサイズの画像を引き出せている以上、SkiaSharp は .ico ファイル内の構造・情報を把握はしているはずである。
だが、どうも、それを SkiaSharp 利用者側には機能公開していないようなのだ。


もしかしたら自分のドキュメントの探し方・読み方を誤っているだけで、ちゃんと方法があるのかもしれない。
しかしながら、小一時間、ドキュメントをうろついたり StackOverflow.com を検索して渡り歩いたりするも、期待する回答に出会えず終いだった。


なお、1 x 1 ピクセルから 256 x 256 ピクセルまで、読み取り希望のピクセルサイズを 1 ピクセルずつ増やしながら SKBitmap.Decode() を呼び出し、戻り値が null か否かで、その .ico ファイル中に含まれる画像ファイルの数と各画像のピクセルサイズを知ることもできるだろう。


できるだろう、が、これはまったくイケてないし、処理性能的にも当然よろしくない。


自分で読み取る!
以上のような状況に行き当たり、さてどうしたものかと少し考えた。


考えた結果、自分の場合、.ico ファイルの内容を普通(?)に読み取って、収録画像のピクセルサイズ一式を収集する C# コードを実装することにした。


参考にしたのは Wikipedia の説明 (下記 URL) である。

上記 Wikipedia の記事に依れば、.ico ファイル中に含まれている画像の枚数と、各画像のピクセルサイズを読み取るだけであれば、.ico ファイル中の先頭を少し読み取るだけで実現できそうだと踏んだ。



ということで、かなり雑だが、ざっくり下記のような C# コードを実装した。
using System.Drawing;
...
// この sizeList に、.ico ファイルに収録されている
// すべての画像のピクセルサイズが溜まる
var sizeList = new List<Size>();


// .icoファイルの先頭5バイト目から2バイトが、収録されている画像の数
iconStream.Seek(offset: 4, SeekOrigin.Begin);
Span<byte> buff = stackalloc byte[2];
iconStream.Read(buff);
var numberOfImages = BitConverter.ToUInt16(buff);


// 収録されている画像数、繰り返し、各画像のピクセルサイズを集める
for (var i = 0; i < numberOfImages; i++)
{
  // 最初の各1バイトが幅と高さ
  // ※但し1バイトしかないので256ピクセルを表現するのに0を使う
  var width = iconStream.ReadByte();
  var height = iconStream.ReadByte();
  sizeList.Add(new Size(
    width == 0 ? 256 : width,
    height == 0 ? 256 : height));
  
  // 残る14バイトはこの用途では参照しないのでスキップ
  iconStream.Seek(offset: 14, SeekOrigin.Current);
}
かなり雑な造りのコードなので、.ico じゃないファイルとかを上記コードに与えると容易にクラッシュしてしまうだろう。
まぁ、このブログ記事中のサンプル実装ということで大目に見て欲しい。


とにかく、上記コードで実際に試してみたところ、.ico ファイル中に含まれているすべての画像のピクセルサイズをちゃんと収集することができた。


これに基づいて、あとは SKBitmap.Decode() を活用して、適切なピクセルサイズの画像を .ico ファイルから読み取って処理できるようになった。


ちなみに、SKBitmap を使って、読み取ったあとの画像のピクセルサイズを変更するのは簡単で、SKBitmap.Resize() メソッドを実行するだけだ。
var resizedBmp = bitmap.Resize(new SKSizeI(newWidth, newHeight), SKFilterQuality.High);

まとめ
ということで、今回は、.ico ファイル中に含まれているすべての画像のピクセルサイズを取得するのに、ちょこっと自前の実装でしのいだ、という話であった。


先にも書いたが、実は自分が SkiaSharp の仕様・機能の理解が甘かっただけで、
「SkipaSharp だけでも要件満たす実装をさくっと書けるよ」
とか、SkiaSharp に限らずとも、もっとマシなやり方があれば、本ブログ記事へのコメントや、Twitter とかでの言及などで、情報共有いただけると嬉しく思う。


C# ソースジェネレータで、対象プロジェクトの既定の名前空間を取得する方法

$
0
0
C# でのプログラミングにおける話。



C# の ver.9、収録される .NET SDK のバージョンでいうと .NET SDK 5.0 からになるが、"ソースジェネレータ (Source Generators)" という機能が使えるようになった。


この "ソースジェネレータ" とは何かというと、いうなれば、C# コンパイラに対するある種の "アドイン" を、我々アプリケーションプログラマが実装・配付できる、という機能だ。


"ソースジェネレータ" = "(C#) ソースコードを生成するもの" という名のとおり、ビルド時に追加の C# ソースコードを動的に作成し、対象プロジェクトに付け加える、そういう "C# コンパイラに対するアドイン" プログラムを C# で作成できる、という仕組みになる。


下記の Qiita 記事などが参考になるかもしれない。




対象プロジェクトの既定の名前空間を知りたい
さてこの C# ソースジェネレータは、いくつかの利用例があるのだが、そのひとつとして
「C# じゃないファイルから C# ソースコードを自動生成する」
というシナリオがある。


このシナリオにおいては、ソースジェネレータが生成する C# ソースコード中に記載の名前空間をどうしたものか、という問題につきあたる。


別のシナリオ、例えば
「partial な C# クラスに機能を付け加える (INotifyPropertyChanged の自動実装とか)」
といった場合は、対象の C# クラスの名前空間でソースコードを生成するしかないので、このような問題はとくには無い。


しかし前述の「C# じゃないファイルから C# ソースコードを自動生成する」というシナリオのときは、せめて対象プロジェクトの既定の名前空間を、ソースジェネレータ側で知る必要があると思う。


Compilation.GlobalNamespace というのを見つけたが...
ということで、実際にソースジェネレータを実装しながら、対象プロジェクトの既定の名前空間を取得できるようなプロパティやメソッドが、ソースジェネレータに提供されるコンテキストオブジェクトにないか、インテリセンス (※自分は Windows OS 上で Visual Studio IDE を使って C# プログラミングをしている) で眺めてみた。


そうすると、Compilation.GlobalNamespace などという、いかにもソレっぽいメンバーが見つかった。


そこで下記例のように、ソースジェネレータを実装して試してみた。
...
[Generator]
public class MySourceGenerator : ISourceGenerator
{
  public void Execute(GeneratorExecutionContext context)
  {
    var rootNamespace = context.Compilation.GlobalNamespace.ToDisplayString();
    ...
しかし残念ながら、この Compilation.GlobalNamespace プロパティ、どうひねくりまわしても、空文字しか返ってこない。
何か前提条件があるのかもしれないが、その点はよくわからず終いだった。


MSBuild プロパティを参照してみる
そこで作戦を変更し、対象プロジェクトの MSBuild プロパティ値を取得してみることにした。


というのも、プロジェクトの既定の名前空間は、そのプロジェクトの "RootNamespace" という名前の MSBuild プロパティ値に設定されているからだ。

プロジェクト作成後に特に何も変更していなければ、この "RootNamespace" MSBuild プロパティ値はアセンブリの出力名と同じに設定されている。
明示的に指定する場合は、当該プロジェクトの .csproj ファイル中に、以下のように MSBuild プロパティ値の記載となって現れる (下記例)。
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    ...
    <!-- 👇 こんな感じ。-->
    <RootNamespace>MyAppNameSpace</RootNamespace>
    ...
さて、まずは GitHub 上で公開されている "Source Generators Cookbook" というドキュメント (下記) を参照してみる。






すると、"Consume MSBuild properties and metadata" というセクションに、そのものズバリの手順が載っていた。


まず、ソースジェネレータ側では、なんでもかんでも、対象プロジェクトの MSBuild プロパティを取得できるわけではないらしい。


対象プロジェクト内で明示的に指定された MSBuild プロパティ値のみが取得できるようだ。


具体的には、対象プロジェクト内の "<ItemGroup>" メンバとして、"<CompilerVisibleProperty>" 項目を記載し、その "Include" 属性値に、ソースジェネレータからの参照を行ないたい MSBuild プロパティの名前を記載する。


今回のお題である、既定の名前空間は、MSBuild プロパティ "RootNamespace" に設定されているので、これをソースジェネレータから参照できるよう、対象プロジェクトに以下のようなノードを追加する。
<Project Sdk="Microsoft.NET.Sdk">
  <!-- これは対象プロジェクトのプロジェクトファイル (.csproj) -->


  <!-- 👇 これを追加 -->
  <ItemGroup>
    <CompilerVisibleProperty Include="RootNamespace" />
  </ItemGroup>
  ...
こうしておくことで、ソースジェネレータ側では、以下のような実装にて、対象プロジェクトの MSBuild プロパティ、すなわち今回の例だと、既定の名前空間を取得することができた。
// これはソースジェネレータの C# ソースファイル (.cs)
...
[Generator]
public class MySourceGenerator : ISourceGenerator
{
  public void Execute(GeneratorExecutionContext context)
  {
    // 👇 これで変数 rootNamespace に既定の名前空間が入る。
    context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(
        "build_property.RootNamespace", 
        out var rootNamespace);
    ...
なお、取得したい MSBuild プロパティ名をそのまま "AnalyzerConfigOptions.GlobalOptions.TryGetValue()" に渡すのではなく、"build_property.~" という前置詞を付ける必要がある。


名前空間に限らず、任意の MSBuild プロパティを取得可能
今回のお題では、ソースジェネレータの対象プロジェクトの、既定の名前空間をいかに取得するか、ということであった。


しかしこの方法は、既定の名前空間の取得に限らず、任意の MSBuild プロパティをソースジェネレータから取得可能であることを意味する。


対象プロジェクト側 .csproj ファイル内で、"<CompilerVisibleProperty Include="MSBuild プロパティ名" />" という ItemGroup 項目を記載して明示的に参照可能にする必要はあるものの、この方法で MSBuild プロパティを参照すれば、他にも以下のような情報を入手可能だ。


対象プロジェクトの出力アセンブリ名対象プロジェクトの各種バージョン対象プロジェクトがあるフォルダの絶対パスetc.


もちろん、上記はあくまで例なので、ソースジェネレータコンテキストの "Compilation" プロパティを参照すればわかるものもあるかもしれない (※自分は詳細は未確認)。


しかしとにかく、対象プロジェクトの MSBuild プロパティを取得できることで、ソースジェネレータの実装の幅が広がることだろう。


NuGet パッケージにして再配布するなら
なお、最終的には、開発したソースジェネレータは NuGet パッケージ化して配付する需要が発生することが多いと思う。


そうしたときには、その NuGet パッケージ内には、ソースジェネレータのビルド後の .dll のみならず、前述の "<CompilerVisibleProperty Include="MSBuild プロパティ名" />" の記載を含む .props ファイルも同梱しておくのがよいかと思う。


というのも、利用者に
「この NuGet パッケージを使うときは、"<CompilerVisibleProperty Include="MSBuild プロパティ名" />" を .csproj に記載してください」
とお願いするのはどうかと思うからだ。


このブログ記事では詳細は割愛しここまでとするが、このように、対象プロジェクト内の MSBuild プロパティ値を使用するソースジェネレータも、NuGet パッケージ化して配付できるよ、ということだ。


以上。


System.Text.Json 時代の読み取り専用プロパティを持つオブジェクトのデシリアライズ

$
0
0
C# でのプログラミングの話。



昨今の C# プログラミングでは、オブジェクトの JSON 文字列へのシリアル化/逆シリアル化を行なうには、System.Text.Json アセンブリおよび同名の名前空間の、JsonSerializer クラスを活用することも多いと思われる。


そして他方、昨今の C# プログラミングでは、オブジェクトのプロパティは、なるべく変更不可・読み取り専用で実装することも多くなったのではないだろうか。


例えば次のような C# クラスである。
class PersonClass
{
  public string? Name { get; }


  public int Age { get; }


  public PersonClass(string name, int age)
  {
    Name = name;
    Age = age;
  }
}

さて、このように読み取り専用プロパティを持つ、コンストラクタでのみ各プロパティ値を初期化するようなクラスであっても、System.Text.Json 名前空間の JsonSerializer クラスでは、ちゃんと、JSON 文字列からのクラスのインスタンスへ逆シリアル化できる。


例えばこんな感じ。
var json = @"{""Name"":""Taro"",""Age"":23}";
var person = JsonSerializer.Deserialize<PersonClass>(json);
// person.Name -> "Taro"
// person.Age -> 23

これは、JsonSerializer.Deserialize<T>() メソッドが、引数が適合するコンストラクタをちゃんと探し当てて、その引数付きコンストラクタを使ってインスタンス化してくれるからだ。


なので、コンストラクタ引数名とプロパティ名が (英字の大小違いは構わないが) 一致しない場合、例えば先の PersonClass クラスのコンストラクタ引数名が、
public PersonClass(string foo, int age)
{
  Name = foo; // コンストラクタ引数名とプロパティ名が一致しない
  ...

のようになっている場合は、JsonSerializer.Deserialize<T>() メソッド実行時に、「これは逆シリアル化できない!」となって例外が発生する。


なお、コンストラクタ引数の出現順序と、プロパティの出現順序は関係ない。
名前と型が適合するかどうかがポイントとなる。


コンストラクタが複数あると逆シリアル化できない!?
さてこのような読み取り専用プロパティで構成されるクラス、希ではあるが、引数違いのコンストラクタをオーバーロード実装する場合がある。


例えば、引数無しデフォルトコンストラクタを必須とするような O/R マッパーライブラリでも同じクラスを使いたい場合、などだ。


では試しに、先の PersonClass に引数無しデフォルトコンストラクタを追加したとしよう。
class PersonClass
{
  ...
  public PersonClass() { } // 👈これを追加
  public PersonClass(string name, int age) {...}
  ...

すると、例外こそ発生しないものの、先のコードの実行結果としては、期待どおりには逆シリアル化されなくなってしまう (下記例)。
var json = @"{""Name"":""Taro"",""Age"":23}";
var person = JsonSerializer.Deserialize<PersonClass>(json);
// person.Name -> null ... "Taro" ではない
// person.Age -> 0 ... 23 ではない

さらには、引数無しデフォルトコンストラクタではなく、引数は持つがシグネチャ違いのコンストラクタオーバーロードがある場合は、JsonSerializer.Deserialize<T>() メソッド実行時に例外が発生するようになる。
class PersonClass
{
  ...
  public PersonClass(string name) { } // 👈これを追加 -> 例外発生
  public PersonClass(string name, int age) {...}
  ...

なお、引数無しデフォルトコンストラクタがある限りは、例外は発生しなくなるようだが、いずれにしても、複数オーバーロードバージョンのコンストラクタがあると、期待どおりの逆シリアル化はされなくなるようだ。


[JsonConstructor] 属性で解決
このように、JsonSerializer.Deserialize<T>() メソッドでは、複数のコンストラクタがある場合、逆シリアル化に最適なコンストラクタの用意があっても、引数無しデフォルトコンストラクタを最優先に使ってオブジェクトをインスタンス化したあとに、そのオブジェクトのプロパティを設定していくようだ。


そして複数のコンストラクタがあるものの、引数無しデフォルトコンストラクタがない場合は、どのコンストラクタを選ぶかを自動では判断せずに諦めるようである。


このように、せっかく逆シリアル化に最適なコンストラクタの用意があっても、複数のコンストラクタがあるせいで、JsonSerializer.Deserialize<T>() メソッドは自動ではその最適なコンストラクタを使うようにはなっていない。


このようなシナリオの場合は、開発者が明示的に、その "逆シリアル化に最適なコンストラクタ" に [JsonConstructor] 属性を付けてやる必要がある。
class PersonClass
{
  ...
  public PersonClass() { }


  [JsonConstructor] // 👈これを追加
  public PersonClass(string name, int age) {...}
  ...

こうすることで JsonSerializer.Deserialize<T>() メソッドは、引数無しデフォルトコンストラクタよりも優先して、[JsonConstructor] 属性で修飾されたコンストラクタを使ってオブジェクトをインスタンス化するようになり、期待どおりの逆シリアル化が実現できる。


補足1 - 初期化専用セッターを持つプロパティの場合
先の例では読み取り専用プロパティについての話であったが、これが初期化専用セッター (init-only セッター) を持つプロパティだったらどうなるか?
class PersonClass
{
  public string Name { get; init; }
  public int Age { get; init; }
  public PersonClass() { }
  public PersonClass(string name, int age) {...}
  ...

これはちゃんと期待どおりの逆シリアル化がされる。


ただし、使われるコンストラクタはあくまでも引数無しデフォルトコンストラクタ。


補足2 - プライベートセッターを持つプロパティの場合
ではプライベートセッターの場合はどうか?
class PersonClass
{
  public string Name { get; private set; }
  public int Age { get; private set; }
  public PersonClass() { }
  public PersonClass(string name, int age) {...}
  ...

この場合は、期待どおりの逆シリアル化にはならない。
各プロパティは、引数無しデフォルトコンストラクタでインスタンス化されたときの初期値のままである。


この場合は、前述のように、適切なコンストラクタオーバーロードの実装、およびそのコンストラクタを [JsonConstructor] 属性で修飾、という手立てで対処するのがよい。


しかし他にも、プライベートセッターを持つプロパティに、[JsonInclude] 属性を付与する、という方法もある。
class PersonClass
{
  [JsonInclude]
  public string Name { get; private set; }


  [JsonInclude]
  public int Age { get; private set; }


  public PersonClass() { }
  public PersonClass(string name, int age) {...}
  ...

こうすると、期待どおりの逆シリアル化がされるようになる。
ただしこの場合も、使われるコンストラクタは、引数無しデフォルトコンストラクタであることに注意。


プロパティに [JsonInclude] 属性を付ける方法のほうがよいというシナリオは、自分には思いつかなかったが、このような方法があることを知っておくと、何かレアケースな実装パターンで役に立つかもしれない。
(実際、何か需要があるからこそ [JsonInclude] などという属性が用意されていることと思うので。)


おわりに
以上、今回は「読み取り専用プロパティを持つオブジェクト」を取り上げたが、このようなクラスの必要があった際は、レコード型 (record) の採用を検討することもお忘れなく。


[2021/05/01 追記]
じんぐるさんから Twitter でコメント頂戴した。
なるほど、こういう観点からの指針決定・判断もあるということで、今後自分の方針も適宜改善していくかもしれない。


まさに最近こればっかりやってました!僕個人の Practice としては

- [JsonInclude] は「常に」付ける
- [JsonConstructor] より private init を好む

という方針にしてました。

[JsonConstructor] はプロパティ名と引数名が異なると動かないのが若干危なっかしい、という理由ですが。— じんぐる (Takaaki Suzuki) (@xin9le) April 30, 2021

Entity Framework Core で「勇者が左右の手に持つ装備を、装備マスタから選択する」モデルを実装する方法

$
0
0
架空のシナリオ -「勇者の冒険ゲーム」を制作中
リレーショナルデータベースへのアクセスに Entity Framework Core を使った、Code First スタイルによる、C# プログラミングの話。


架空のシナリオとして、「勇者の冒険ゲーム」を制作中だとする。
このゲームのモデリングにおいて、まず、以下のような「装備マスタ」のエンティティ型を用意する。
public class Equipment 
{
  public int Id { get; set; }


  [Required, StringLength(20)]
  public string Name { get; set; }
}
そして「勇者 (プレイヤー) 」は、左右の手それぞれに、上記「装備マスタ」に登録されている装備 (レコード) のうち、任意のひとつを持つものとする。
そのような「勇者 (プレイヤー)」のエンティティ型を、以下のように実装した。
public class Player
{
  public int Id { get; set; }


  // 左の手に持つ装備の装備マスタ上の ID
  public int LeftEquipmentId { get; set; }


  public virtual Equipment LeftEquipment { get; set; }


  // 左の手に持つ装備の装備マスタ上の ID
  public int RightEquipmentId { get; set; }


  public virtual Equipment RightEquipment { get; set; }
}
あとは上記2つのエンティティ型を使って、データベースコンテキスト型を構築すればよい。
public class MyGameDbContext : DbContext
{
  public DbSet<Equipment> Equipments { get; set; }
  public DbSet<Player> Players { get; set;}
  ...
}
以上のように実装することで、例えば以下のような感じで、「勇者 (プレイヤー)」のもつ装備を容易に参照可能となる。
var player = myGameAppDbContext.Players
  .Inclide(p => p.LeftEquipment)
  .Inclide(p => p.RightEquipment)
  .First(p=> ...);



// 👇 左手に持つ装備の名前が表示される
Console.WriteLine(player.LeftEquipment.Name);
各プロパティ名の命名則により、Entity Framework Core がよしなにエンティティモデルを構築してくれるおかげで、「装備マスタ」テーブルと「勇者 (プレイヤー)」テーブルとの、この2つのテーブル間の結合を、"ナビゲーションプロパティ" を参照するだけで直感的なコードで辿れるので便利である。


最後に、Code First スタイルなので、上記エンティティ周りの実装からデータベースを構築するよう、プログラムの開始時に以下のコードを実装する。
myGameAppDbContext.Database.EnsureCreated();

さてこれでよしよしと、ビルドもエラーなく成功。


さて、自分の場合はリレーショナルデータベースに SQL Server を使っていたので SQL Server に接続するよう構成して、いざ実行してみると、


あれ?


実行時例外になってしまった...


Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904):
Introducing FOREIGN KEY constraint 'FK_Players_Equipments_LeftEquipmentId' on table 'Players' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.



例外の原因は Entity Framework Core とプログラマとの行き違い!?どうやら先の実装だと、

「装備マスタ上の装備の、いずれかひとつを選んで、勇者 (プレイヤー) に関連付けする」

というよりは、

「この勇者 (プレイヤー) インスタンスが持っている装備は、この装備レコードに記載されている」

というように、生存期間も同じな 1:1 の関連付けとして判断されるようだ。


そのため、そのようなモデリングでは大事な、連鎖削除も同時に構成されるらしい。
ところがそのような前提で「装備マスタ」と「勇者 (プレイヤー)」とを関連付けようとすると、現状の「勇者 (プレイヤー)」は 2 つの「装備マスタ」参照を持っているため、連鎖削除の循環参照が発生し、解決できなくなるらしい。


ちゃんと言わなければ伝わらないことだってあるさてさて、そのような Entity Framework Core との誤解 (?) を解くには、すなわち、既定のモデリング則に当てはまらない場合は、ちゃんとプログラマが「いやいや、ここでやりたいモデリングはこうなんだよ」と明示してやる必要がある。


今回の場合だと、連鎖削除の制約は無用であることを、Entity Framework Core に教えてあげるとよい。
(実際、今回のモデリングでは、「勇者 (プレイヤー)」レコードが削除されることで、そのプレイヤーが持っていた装備も装備マスタから削除されてしまってはよろしくない訳である)


具体的には、データベースコンテキストクラスで OnModelCreating() メソッドをオーバーライドし、その中で指示してやればよい。
public class MyGameDbContext : DbContext
{
  public DbSet<Equipment> Equipments { get; set; }
  public DbSet<Player> Players { get; set;}
  ...
  protected override void OnModelCreating(ModelBuilder builder)

  {
    base.OnModelCreating(builder);
    var playerEntity = builder.Entity<Player>();

    playerEntity.HasOne(t => t.LeftEquipment).WithMany()
      .OnDelete(DeleteBehavior.NoAction);
    playerEntity.HasOne(t => t.RightEquipment).WithMany()
      .OnDelete(DeleteBehavior.NoAction);
  }
}

これで、「勇者(プレイヤー)」と「装備マスタ」との関係は、左右の手ごとに 1:N の関係であることや、連鎖削除は無用であることを明示できた。


解決!以上でプログラム実行すると、ちゃんと期待どおりにデータベースが構築された。


また、「勇者 (プレイヤー)」レコードに対応する Player オブジェクトに対し、LeftEquipment および RightEquipment のナビゲーションプロパティを介して、持っている装備の参照や更新 (装備マスタからの装備の持ち替え) も期待どおり動作するようになった。


ちなみに、自分の知る限り、連鎖削除の有無の制御は、上記のように  Fluent API を使って行なうしかないようだ。
(つまり、属性指定では制御・指定はできないっぽい)


以上
Viewing all 146 articles
Browse latest View live